/* ==========================================================================
   Tokens — exact values from the Figma file (yelimkim.me styles).
   Names in comments are the Figma style names.
   ========================================================================== */

:root {
  --white: #ffffff;          /* yelimkim.me/White */
  --black: #272727;          /* text black — softened from the Figma #000 */
  --christine: #f26113;      /* yelimkim.me/Christine  — accent */
  --mine-shaft: #333333;     /* yelimkim.me/Mine Shaft — secondary text */
  --wild-sand: #f5f5f5;      /* yelimkim.me/Wild Sand  — pills, footer */
  --gallery: #f0f0f0;        /* yelimkim.me/Gallery    — header rule */
  --mercury: #e8e8e8;        /* yelimkim.me/Mercury    — footer table bar */
  --body-text: #666666;   /* all body copy */
  --charcoal: #484848;       /* hero facts */
  --selection-fill: #fff9eb; /* role highlight */
  --selection-edge: #ffd87e; /* role highlight carets */
  --gray: #8f8f8f;           /* yelimkim.me/Gray */
  --dove-gray: #616161;      /* yelimkim.me/Dove Gray */

  --font-sans: "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI",
    system-ui, sans-serif;

  /* Type ramp, straight from the design */
  --fs-body: 16px;
  --lh-body: 20.8px;
  --ls-body: -0.16px;

  --fs-lede: 24px;
  --lh-lede: 31.2px;
  --ls-lede: -0.48px;

  --fs-title: 30px;          /* work card title */
  --lh-title: 36px;
  --ls-title: -0.6px;

  --fs-hero: 38px;
  --lh-hero: 45.6px;
  --ls-hero-lead: -1.14px;   /* "Yelim Kim is a" */
  --ls-hero-role: -0.76px;   /* the cycling role */

  /* Vertical rhythm. Each level is clearly larger than the one nested inside
     it, so grouping reads from spacing alone: section > labelled group >
     block > within a block. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 40px;
  --sp-7: 80px;
  --sp-media: 10px;   /* image grids — the design's own item spacing */

  --header-h: 52px;
  --footer-h: 283px;
  /* One gutter for every section. The Figma header frame is pl-50 pr-60,
     which left the header, hero and work grid starting at three different
     x positions — almost certainly drift in the file rather than intent. */
  --pad-x: 60px;
  --pad-x-header: 60px;
  --gap-cards: 18px;
  --radius-card: 8px;

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast: 180ms;
  --dur: 400ms;
  --dur-slow: 800ms;
}

/* ===== Reset ============================================================= */

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

* {
  margin: 0;
}

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

body {
  background: var(--white);
  color: var(--black);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  -webkit-font-smoothing: antialiased;
}

img,
video,
svg {
  display: block;
  max-width: 100%;
  /* Every image carries width/height attributes to reserve layout space.
     Without height:auto the browser honours that attribute height while
     max-width shrinks the width, which stretches the image vertically.
     Rules that genuinely want a fixed height (the case hero, work-card
     media) are more specific and still win. */
  height: auto;
}

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

h1,
h2 {
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
}

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

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

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

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

.skip-link {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 100;
  padding: 8px 16px;
  background: var(--white);
  border: 1px solid var(--gallery);
  border-radius: 6px;
  transform: translateY(-150%);
  transition: transform var(--dur-fast) var(--ease-out);
}

.skip-link:focus-visible {
  transform: translateY(0);
}

/* ===== Header — rule #F0F0F0 ======================================= */

.site-header {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--white);
}

.site-header__inner {
  display: flex;
  align-items: center;
  height: var(--header-h);
  /* Both sides from the header's own token. The right side used --pad-x,
     which goes fluid on non-home pages — so the nav shifted as you moved
     between the home page and a project. */
  padding-inline: var(--pad-x-header);
  border-bottom: 1px solid var(--gallery);
}

/* Stands in for nametag_new.riv (165x42) — kept as live text so the site
   name is selectable and readable to screen readers and crawlers. */
.site-header__mark {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-weight: 500;
  margin-right: auto;
}

.site-header__mark .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--selection-edge); /* the .riv's own dot colour */
  flex: none;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: 40px;
}

/* Hover leaves the text alone and draws a line under it, left to
   right, like a trim path running 0 to 100%. It is revealed with clip-path
   rather than scaled so the round caps keep their shape while it draws. */
.site-nav a,
.case-side__nav a,
.case-side__back {
  position: relative;
}

.site-nav a::after,
.case-side__nav a::after,
.case-side__back::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 2px;
  border-radius: 1px;
  background: var(--selection-edge);
  clip-path: inset(0 100% 0 0 round 1px);
  transition: clip-path var(--dur) var(--ease-out);
}

.site-nav a:hover::after,
.site-nav a:focus-visible::after,
.case-side__nav a:hover::after,
.case-side__nav a:focus-visible::after,
.case-side__nav a[aria-current="true"]::after,
.case-side__back:hover::after,
.case-side__back:focus-visible::after {
  clip-path: inset(0 0 0 0 round 1px);
}

/* In the case-study side column the line is the grey of the footer's table
   bar rather than the header's yellow. */
.case-side__nav a::after,
.case-side__back::after {
  background: var(--mercury);
}

/* Home is indented by padding, so its line starts where its text does. */
.case-side__back::after {
  left: 50px;
}

/* ----- Phone: the nav folds into a hamburger -----------------------------
   The button is in the markup on every page but only shows on a phone: a
   narrow screen that is also touch-first. A desktop window dragged narrow
   keeps its links in the bar. It also needs JS (which is what opens the
   menu) — without it the links stay in the bar too. */
.nav-toggle {
  display: none;
}

@media (max-width: 40rem) and (hover: none) and (pointer: coarse) {
  [data-js] .nav-toggle {
    /* A 1px bar, and every offset below a whole pixel, so each of the three
       lands on the pixel grid the same way and comes out the same weight. */
    --bar: 1px;
    display: block;
    position: relative;
    width: 40px;
    height: 40px;
    /* The 40px target hangs into the gutter so the bars, not the button's
       box, sit on the page margin. */
    margin-right: -12px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--black);
    cursor: pointer;
  }

  .nav-toggle__bars,
  .nav-toggle__bars::before,
  .nav-toggle__bars::after {
    position: absolute;
    left: 0;
    width: 16px;
    height: var(--bar);
    background: currentColor;
    transition: transform var(--dur-fast) var(--ease-out),
      background-color var(--dur-fast) var(--ease-out);
  }

  /* The middle bar; the other two hang 5px above and below it. */
  .nav-toggle__bars {
    left: 12px;
    top: 19px;
  }

  .nav-toggle__bars::before,
  .nav-toggle__bars::after {
    content: "";
  }

  .nav-toggle__bars::before { top: -5px; }
  .nav-toggle__bars::after { top: 5px; }

  /* Open: the outer bars cross into an x and the middle one drops out. */
  .nav-toggle[aria-expanded="true"] .nav-toggle__bars {
    background-color: transparent;
  }

  .nav-toggle[aria-expanded="true"] .nav-toggle__bars::before {
    transform: translateY(5px) rotate(45deg);
  }

  .nav-toggle[aria-expanded="true"] .nav-toggle__bars::after {
    transform: translateY(-5px) rotate(-45deg);
  }

  /* The links become a panel hanging off the bottom of the sticky header. */
  [data-js] .site-nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding: 6px var(--pad-x-header) 14px;
    background: var(--white);
    border-bottom: 1px solid var(--gallery);
    visibility: hidden;
    opacity: 0;
    transform: translateY(-6px);
    transition: opacity var(--dur-fast) var(--ease-out),
      transform var(--dur-fast) var(--ease-out),
      visibility 0s linear var(--dur-fast);
  }

  [data-js] .site-nav[data-open] {
    visibility: visible;
    opacity: 1;
    transform: none;
    transition-delay: 0s;
  }

  [data-js] .site-nav li {
    padding-block: 10px;
  }
}

/* ===== Hero ============================================================== */

.hero {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 40px;
  padding: 10px var(--pad-x) 15px;
}

.hero__art {
  flex: none;
  width: 100%;
  max-width: 390px;
  aspect-ratio: 390 / 378.27;
  /* The chick doesn't fill its box: in the resting pose the drawing starts
     109px in from the left (and stops 45px short of the right). Centring
     the box therefore left the visible hero — chick plus text — sitting
     54px right of centre. Pulling the box left by its own empty margin
     makes the row centre on the drawing's edge instead. */
  margin-left: -109px;
}

.hero__text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 41px;
}

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

.hero__titlerow {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  /* Design: the lead sits in a 215.73px box (2px wider than its text), then
     4px to the role's 307px frame, whose fill starts 5px in. */
  gap: 0 11px;
  font-size: var(--fs-hero);
  line-height: var(--lh-hero);
  font-weight: 400;
}

.hero__lead {
  letter-spacing: var(--ls-hero-lead);
}

/* The cycling role sits in a text-selection highlight: a pale fill with a
   1px caret down each side and a dot on the top-left and bottom-right
   corners. Built in CSS rather than dropped in as the exported 307px SVG
   because the box follows the type — the hero font scales down on small
   screens and a stretched SVG would distort the dots.

   Dots are 7px, not the design's 7.5: with a 1px caret, 7 is the size that
   is a whole number of device pixels and centres on the caret at both 1x
   and 2x, so they stay round and on their corners. */
.role-slot {
  --role-pad-start: 5px;
  --role-pad-end: 9px;
  --role-wipe-ease: cubic-bezier(0.65, 0, 0.35, 1);
  display: inline-grid;
  letter-spacing: var(--ls-hero-role);
}

/* pre, so a word typed as far as its space ("Visual ") keeps the space's
   width instead of jumping when the next letter lands. Only on the text
   itself — on the slot it would give the markup's own indentation width. */
.role-slot__sizer,
.role-frame__text {
  white-space: pre;
}

/* Every role, invisible, stacked in the slot's one cell: the slot is always
   as wide as the widest role's frame, however much of a word is typed. */
.role-slot__sizer {
  grid-area: 1 / 1;
  visibility: hidden;
  padding: 0 var(--role-pad-end) 0 var(--role-pad-start);
}

.role-frame {
  grid-area: 1 / 1;
  /* Hugs the text that is there, from the slot's left edge. */
  justify-self: start;
  position: relative;
  /* Keeps the fill behind the text without it slipping under the page. */
  isolation: isolate;
  /* 297px fill around the widest role: 5px before the text (the text's own
     indent, below), 9px after. */
  padding-right: var(--role-pad-end);
  transition: padding-right var(--dur-fast) var(--ease-out);
}

/* The text is the mask for the wipe: main.js runs its width down to zero
   and the right-hand line follows, covering the word from the right while
   the letters stay where they are. The gap before the text is an indent
   rather than padding so that it is clipped along with the word. */
.role-frame__text {
  display: block;
  overflow: hidden;
  text-indent: var(--role-pad-start);
}

.role-frame[data-wiping] {
  padding-right: 0;
  transition: padding-right var(--role-wipe, 450ms) var(--role-wipe-ease);
}

.role-frame[data-wiping] .role-frame__text {
  transition: width var(--role-wipe, 450ms) var(--role-wipe-ease);
}

/* While a word is being typed the right-hand line is the caret, tight
   against the last letter; it eases out to the resting gap when the word
   is finished. */
.role-frame[data-typing] {
  padding-right: 1px;
  transition: none;
}

/* Wiped: nothing but a caret — the left line and its dot. */
.role-frame[data-empty] {
  padding-right: 0;
  transition: none;
}

.role-frame[data-empty]::before {
  border-right-width: 0;
}

/* The bottom dot is gone well before the mask closes — it fades over the
   first 40% of the wipe rather than riding the line all the way in — and
   stays gone on the bare caret. */
.role-frame[data-wiping] > .handle::after {
  opacity: 0;
  transition: opacity calc(var(--role-wipe, 450ms) * 0.4) ease-out;
}

.role-frame[data-empty] > .handle::after {
  opacity: 0;
}

/* The fill is shorter than the line box: 41px against 45.6, flush with the
   bottom. */
.role-frame::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0.115em 0 0;
  background: var(--selection-fill);
  border: solid var(--selection-edge);
  border-width: 0 1px;
}

.role-frame > .handle::before,
.role-frame > .handle::after {
  content: "";
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--selection-edge);
}

.role-frame > .handle::before {
  top: calc(0.115em - 3.5px);
  left: -3px;
}

.role-frame > .handle::after {
  bottom: -3.5px;
  right: -3px;
}

.hero__lede {
  font-size: var(--fs-lede);
  line-height: var(--lh-lede);
  letter-spacing: var(--ls-lede);
}

.hero__facts {
  display: flex;
  flex-direction: column;
  gap: 4px;
  color: var(--charcoal);
}

.hero__facts dd {
  margin: 0;
}

/* Hover hint under the hero chick. Position and type come from the design:
   14/18.2 at -0.14 tracking, sitting 84.31% down the art box. */
.hero__art {
  position: relative;
}

.hero__rive {
  position: relative;
  width: 100%;
  height: 100%;
}

/* Out of flow, so the canvas takes the box while the still is still up
   rather than being pushed out underneath it. */
.hero__rive .rive-still {
  position: absolute;
  inset: 0;
}

.hero__hint {
  position: absolute;
  /* Design puts this at 84.31%. */
  top: 80%;
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: 14px;
  line-height: 18.2px;
  letter-spacing: -0.14px;
  color: var(--gray);
  opacity: 0;
  transition: opacity var(--dur) var(--ease-out);
  pointer-events: none;
}

.hero__art:hover .hero__hint,
.hero__art:focus-within .hero__hint {
  opacity: 1;
}

/* ===== Work ============================================================== */

.works {
  padding: 25px var(--pad-x) 60px;
}

.work-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--gap-cards);
  row-gap: 55px;
}

.work-card__link {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.work-card__media {
  position: relative;
  aspect-ratio: 891 / 497.16;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--wild-sand);
}

.work-card__media img,
.work-card__media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* --card-zoom crops further in for sources whose aspect already matches the
     frame, where object-fit:cover alone does nothing. Composes with the hover
     scale rather than being overwritten by it. */
  transform: scale(var(--card-zoom, 1));
  transition: transform var(--dur-slow) var(--ease-out);
}

.work-card__link:hover .work-card__media img,
.work-card__link:hover .work-card__media video {
  transform: scale(calc(var(--card-zoom, 1) * 1.02));
}

.work-card__title {
  font-size: var(--fs-title);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-title);
  font-weight: 400;
}

.work-card--unlinked .work-card__link {
  cursor: default;
}


.work-card__text {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

.tags li {
  padding: 2px 10px 3px;
  background: var(--wild-sand);
  border-radius: 100px;
  color: var(--mine-shaft);
}

/* ===== Footer ============================================================ */

/* Tucked under the page: the footer is pinned to the viewport bottom and the
   content scrolls over the top of it, so it is revealed rather than pushed.
   main carries its own background for that to read — without one the footer
   shows through — and a bottom margin equal to the footer height, which is
   the scroll distance that does the revealing. */
.site-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--footer-h);
  padding: 60px var(--pad-x) 45px;
  background: var(--wild-sand);
}

main {
  position: relative;
  z-index: 1;
  background: var(--white);
  margin-bottom: var(--footer-h);
}

/* The sticky header has to clear the revealed footer too. */
.site-header {
  z-index: 3;
}

.site-footer__inner {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.site-footer__row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  width: 100%;
}

.site-footer__note {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding-bottom: 4px;
  padding-left: 2px;
}

.heart {
  position: relative;
  width: 17px;
  height: 17px;
  flex: none;
}

.heart img {
  position: absolute;
}

.heart__outline {
  left: 0;
  top: 11.76%;
  width: 14px;
}

.heart__fill {
  left: 1px;
  top: 4px;
  width: 12px;
}

.site-footer__art {
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
}

.site-footer__socials {
  width: 112px;
  height: 153px;
  flex: none;
}

.site-footer__chick {
  position: relative;
  width: 214px;
  /* 165, not the design's 152: at --rive-scale 1.2 the art is 182px tall and
     sits 18px below the bar line, so a 152px frame would clip 12px off the
     head. This keeps the crop entirely at the bottom, where the grey bar is
     meant to hide it. */
  height: 165px;
  flex: none;
  overflow: hidden;
}

/* The .riv is deliberately larger than its frame: 260 x 153.39 pushed out
   35px left and 11px right, clipped by the frame. Letting the canvas fill
   the 214px frame instead made Fit.Contain shrink the chick to fit, which
   is why it rendered too small. */
.site-footer__chick .rive-canvas,
.site-footer__chick .rive-still {
  /* Sized so the ART fills the frame, not the artboard. The artboard is
     330x260 but the drawn chick occupies 281x244 of it (insets measured off
     the exported render), so the canvas is set to the artboard's rendered
     size at the scale that makes the art exactly 152px tall — the frame
     height. Contain then fills the canvas with zero crop.
     scale 1 = art fills the frame; raise it to overflow and clip. */
  --rive-scale: 1.2;
  position: absolute;
  width: calc(205.57px * var(--rive-scale));
  height: calc(161.97px * var(--rive-scale));
  left: calc((214px - 175.1px * var(--rive-scale)) / 2 - 15.21px * var(--rive-scale));
  /* --rive-drop pushes the chick further down past the frame edge. */
  --rive-drop: 27px;
  bottom: calc(-0.49px * var(--rive-scale) - var(--rive-drop));
}

/* The bar the chick sits on. */
.site-footer__table {
  width: 100%;
  height: 13px;
  min-height: 13px;
  border-radius: 3px;
  background: var(--mercury);
}

/* ===== Scroll reveal ===================================================== */

[data-js] [data-reveal] {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
}

[data-js] [data-reveal][data-revealed] {
  opacity: 1;
  transform: none;
}

/* ===== Responsive ========================================================
   The Figma frame is 1920w only. Below that the fixed type and padding stop
   working, so these steps scale it down. Desktop values stay exact. */

/* Above 1280px the content gutter goes fluid — clamp(1rem, 10vw, 16rem) —
   so the margin grows with the window and caps at 256px. Pages marked
   .page-fixed-gutter opt out and keep the 60px: the home page and the
   gallery, both of which are edge-to-edge grids rather than reading columns.
   The header is deliberately left out of this: it is shared chrome, and a
   gutter that changed between pages would make the nav jump as you move
   around the site. */
@media (min-width: 80rem) {
  body:not(.page-fixed-gutter) {
    --pad-x: clamp(1rem, 10vw, 16rem);
  }
}

/* Type holds at the design-system values all the way down to 480px; only
   the gutters and the layout change. */
@media (max-width: 68rem) {
  :root {
    --pad-x: 60px;
    --pad-x-header: 60px;
  }

  /* Tighten the hero gap early — every pixel here goes to the text column. */
  .hero {
    gap: 24px;
  }
}

/* The hero stacks at 960, not 768. Side by side, the text column is
   (viewport - 120 gutters - 390 art - gap). Below ~960 that drops under the
   ~376px the longest role ("Visual Storyteller" at 38px) needs, so the role
   box wrapped onto two lines — and only for the longer roles, so the
   headline changed height as it cycled. Stacking early gives the headline
   the full width instead of shrinking the type to fit a cramped column. */
@media (max-width: 60rem) {
  .hero {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
    padding-top: 16px;
    padding-bottom: 24px;
  }

  .hero__art {
    max-width: 400px;
    align-self: center;
    /* The artboard's own aspect, so Contain leaves no empty box above and
       below the art. */
    aspect-ratio: 330 / 260;
    /* Stacked, the chick is centred on its own: the drawing spans 27.9% to
       88.5% of the box, so its middle is 8.2% right of the box's. */
    margin-left: 0;
    transform: translateX(-8.2%);
  }

  .hero__text {
    gap: 28px;
  }

  /* The art fills this box edge to edge once it takes the artboard's own
     aspect, so any percentage inside the box lands on the chick. The hint
     goes below the box instead, with the hero gap opened to make room for
     it — it's absolutely positioned, so it reserves no space of its own. */
  .hero__hint {
    top: calc(100% - 49px);
  }
}

@media (max-width: 48rem) {
  :root {
    /* 38px overflows here: "Yelim Kim is a" plus the longest role needs more
       than the 606px of content at this width, so the role box wrapped to a
       second line — and only for the longer roles, so the headline jumped
       height as it cycled. Fluid, capped at the design-system 38. */
    --fs-hero: clamp(26px, 4.8vw, 38px);
    --lh-hero: 1.2em;
  }

  .work-grid {
    grid-template-columns: 1fr;
    row-gap: 36px;
  }

  .site-footer__row {
    flex-direction: column;
    align-items: flex-start;
    gap: 32px;
  }

  /* Row becomes a column here, so the footer is taller and the spacer must
     match or the reveal stops short. */
  :root {
    --footer-h: 336px;
  }

  /* The row stacks here, so the art needs the full width to keep sitting
     against the right edge — it inherits the column's flex-start otherwise
     and jumps to the left. */
  .site-footer__art {
    width: 100%;
    justify-content: flex-end;
  }
}

/* Below ~480px a 60px gutter each side leaves too little room for content. */
@media (max-width: 30rem) {
  :root {
    /* The one place the scale steps down: below 480px the design-system
       sizes genuinely don't fit. Ratios are kept, not invented. */
    --fs-hero: 28px;
    --lh-hero: 33.6px;
    --fs-title: 22px;
    --lh-title: 26.4px;
    --fs-lede: 19px;
    --lh-lede: 24.7px;
    --pad-x: 24px;
    --pad-x-header: 24px;
  }

  .hero__art {
    max-width: 100%;
  }
}

/* ==========================================================================
   About page
   ========================================================================== */

.about {
  display: flex;
  align-items: flex-start;
  gap: 13px;
  width: 100%;
  max-width: 1140px;
  margin-inline: auto;
  padding: 101px 100px 60px;
}

.about__col {
  display: flex;
  flex-direction: column;
  gap: 25px;
  width: 611px;
  max-width: 100%;
}

.about__title {
  font-size: var(--fs-title);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-title);
  font-weight: 400;
}

.about__desc {
  display: flex;
  flex-direction: column;
  gap: 17px;
  max-width: 600px;
}

.about__links {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding-top: 20px;
}

.about__linkrow {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

.about__link,
.about__note {
  color: var(--christine);
}

.about__link {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

.about__link svg {
  width: 8px;
  height: 8px;
  flex: none;
  overflow: visible;
}

.about__link:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* character.riv — the design's container is 250x264 with the 250x326.14 art
   box centred on it, so the art overflows top and bottom (nothing clips it).
   The canvas is that art box, centred on the band; Cover fills it. */
.about__art {
  position: relative;
  flex: none;
  width: 250px;
  height: 264px;
  /* Clips left/right at the frame edge while leaving vertical overflow
     alone, as the design intends. At 1.2x the canvas box reaches 39px left
     into a 13px column gap; the art itself is only 233px wide so nothing
     visible is lost, but without this the empty canvas would sit over the
     bio text and swallow clicks. clip-path also clips hit-testing, so the
     Rive hover still works everywhere the art actually is. */
  clip-path: inset(-100% 0 -100% 0);
}

.about__art .rive-canvas,
.about__art .rive-still {
  /* Same treatment: art is 356x484 of a 500x500 artboard, so the canvas is
     the artboard rendered at the scale that makes the art 264px tall. */
  --rive-scale: 1.2;
  position: absolute;
  width: calc(272.73px * var(--rive-scale));
  height: calc(272.73px * var(--rive-scale));
  left: calc((250px - 194.2px * var(--rive-scale)) / 2 - 39.27px * var(--rive-scale));
  top: calc(-7.64px * var(--rive-scale));
}

@media (max-width: 62rem) {
  .about {
    flex-direction: column-reverse;
    align-items: flex-start;
    gap: 32px;
    padding: 48px var(--pad-x) 48px;
  }

  .about__col {
    width: 100%;
  }
}

/* ==========================================================================
   Gallery
   ========================================================================== */

.gallery-head {
  /* --pad-x, not the 72px the Framer page used, so the heading starts on the
     same line as the header rule and the work grid. */
  padding: 34px var(--pad-x) 0;
}

.gallery-head h1 {
  font-size: 23px;
  line-height: 27.6px;
  letter-spacing: -0.46px;
  font-weight: 500;
}

.masonry {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  padding: 30px var(--pad-x) 72px;
}

.masonry__col {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}

.masonry img {
  width: 100%;
  height: auto;
}

@media (max-width: 62rem) {
  .gallery-head {
    padding: 28px var(--pad-x) 0;
  }

  .masonry {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    padding: 20px var(--pad-x) 48px;
  }
}

@media (max-width: 40rem) {
  .masonry {
    grid-template-columns: 1fr;
  }
}

/* ==========================================================================
   Rive
   ========================================================================== */

.rive-canvas,
.rive-still {
  display: block;
  width: 100%;
  height: 100%;
}

/* A still frame of each .riv, rendered from the whole artboard, shown until
   the animation is running — and for good if it never loads. It takes the
   canvas's exact box, and contain matches Fit.Contain, so the art sits where
   the live version will and the swap doesn't move anything. */
.rive-still {
  object-fit: contain;
  /* The footer and about canvases are wider than their frames by design;
     the global img cap would squeeze the still back inside. */
  max-width: none;
}

/* The wordmark starts as live text and becomes a canvas once Rive loads, so
   the site name is present for crawlers and for anyone whose script fails. */
.mark-rive {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.mark-rive[data-rive-ready] {
  display: block;
  width: 165px;
  height: 42px;
  gap: 0;
  /* The drawing starts 9px inside its artboard. Pulling the canvas back by
     that much puts the dot's left edge on the page gutter, where the text
     stand-in's dot already sits and in line with everything below it. */
  margin-left: -9px;
}

/* Until the .riv has loaded, its canvas must not take part in the text's
   row. In flow it was a flex item beside "Yelim Kim": it squeezed the name
   onto two lines, and because Rive resizes the canvas's bitmap to its box
   while the box was sized from that bitmap, it grew a few pixels every
   frame until load. Out of flow, hidden, and already at its final 165x42 —
   Rive measures it at the right size, and nothing moves until the swap. */
.mark-rive {
  position: relative;
}

.mark-rive:not([data-rive-ready]) .rive-canvas {
  position: absolute;
  left: 0;
  top: 50%;
  width: 165px;
  height: 42px;
  margin-top: -21px;
  visibility: hidden;
  pointer-events: none;
}

/* ==========================================================================
   Case study pages
   Section labels use Hot Cinnamon #E65C23, which is a different orange from
   the #F26113 used on the homepage — both are in the Figma styles.
   ========================================================================== */

:root {
  --hot-cinnamon: #e65c23;   /* yelimkim.me/Hot Cinnamon */
  --shaft-80: #212121;       /* yelimkim.me/Mine Shaft 80% */
}

.case-hero {
  width: 100%;
  aspect-ratio: 1920 / 732.8;
  overflow: hidden;
}

.case-hero img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.case-body {
  display: flex;
  align-items: flex-start;
  padding-block: 55px 60px;
}

.case-side {
  position: sticky;
  /* Header height plus a 50px gap, so the side nav never sits flush against
     the top menu when it sticks. Derived, so changing --header-h moves both
     this and the anchor offset together. */
  top: calc(var(--header-h) + 50px);
  flex: none;
  width: 201px;
  display: flex;
  flex-direction: column;
  gap: 60px;
  padding-block: 9px 22px;
}

.case-side__nav {
  display: flex;
  flex-direction: column;
  gap: 23px;
  padding-left: 50px;
}

/* The section in view is marked with the header nav's underline (the rule
   is shared, up by .site-nav) drawing in under its label, rather than by
   recolouring the text. */

.case-side__back {
  padding-left: 50px;
}

.case-side__back {
  display: inline-block;
  /* The column is a flex container and would stretch this to its full
     width, taking the underline with it. */
  align-self: flex-start;
}


.case-main {
  width: 100%;
  max-width: 1536px;
  padding-inline: 52px;
  display: flex;
  flex-direction: column;
  /* Between sections. Down from the 80px step: that read as a break in the
     page rather than a pause. Still above the 40px between subsections, so
     the ladder holds. */
  gap: 56px;
  min-width: 0;
}

.case-head {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}

.case-title {
  font-size: var(--fs-title);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-title);
  font-weight: 400;
}

.case-meta {
  display: flex;
  justify-content: space-between;
  gap: 40px;
  width: 615px;
  max-width: 100%;
  margin: 0;
}

.case-meta dt {
  color: var(--hot-cinnamon);
  margin-bottom: 9px;
}

.case-meta dd {
  margin: 0;
}

/* Spacing here is per-relationship rather than one gap: a gap applies the
   same value between the heading and the first block as it does between
   blocks, which put 40px under every title. */
.case-section {
  display: flex;
  flex-direction: column;
  gap: 0;
  scroll-margin-top: calc(var(--header-h) + 50px);
}

/* Between blocks. Kept modest: everything inside a section belongs to that
   section, so only the 80px between sections should read as a real break. */
.case-section > * + * {
  margin-top: var(--sp-5);
}

/* A title sits close to what it introduces. Both rules come after the one
   above and match at the same specificity, so they win on order. */
.case-section__head + *,
.case-label + * {
  margin-top: var(--sp-3);
}

/* Subsection labels get one gap wherever they appear — directly under the
   section title or after a block. At (0,2,0) this outranks both rules above,
   which is what stops the spacing depending on position. Larger than the gap
   between blocks, so the ladder is strict: section > subsection > block >
   paragraph. */
.case-section > .case-label {
  margin-top: var(--sp-6);
}

/* Except straight under the section title: the two labels read as one
   heading there, so the subsection gap would only pull them apart. */
.case-section > .case-section__head + .case-label {
  margin-top: var(--sp-1);
}

.case-section__head {
  display: flex;
  align-items: center;
  gap: 4px;
  color: var(--hot-cinnamon);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: var(--lh-body);
}

.case-section__dot {
  width: 6px;
  height: 6px;
  border-radius: 6px;
  background: var(--hot-cinnamon);
  flex: none;
}

.case-copy {
  color: var(--body-text);
  display: flex;
  flex-direction: column;
  /* Tighter than the 16 used elsewhere — consecutive paragraphs are one
     thought, and the line height already separates them. */
  gap: var(--sp-3);
}

.case-copy--narrow { max-width: 716px; }
.case-copy--reflect { max-width: 650px; padding-block: 14px 30px; }

.case-grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-media);
}

.case-stack {
  display: flex;
  flex-direction: column;
  gap: var(--sp-media);
}

.case-stack img,
.case-grid-2 img {
  width: 100%;
  height: auto;
}

/* Click-to-load YouTube facade: shows the poster and the design's play
   button, and only pulls in YouTube's iframe once you actually press it. */
.case-video {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  overflow: hidden;
}

/* Scoped to the poster only. ".case-video img" also matched the play
   button — and beat its own rule on specificity — so the icon was being
   stretched across the whole frame. */
.case-video__poster {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.case-video .case-video__play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 68px;
  height: 48px;
  transition: transform var(--dur-fast) var(--ease-out);
}

.case-video:hover .case-video__play {
  transform: translate(-50%, -50%) scale(1.06);
}

.case-video iframe {
  width: 100%;
  height: 100%;
  border: 0;
}

@media (max-width: 68rem) {
  .case-body {
    flex-direction: column;
    padding-block: 32px 48px;
  }

  /* Hidden rather than laid out horizontally above the content. A wrapped
     row of section links plus Home read as a second navigation bar under the
     real one. Home is still on the header wordmark, and the sections are
     reachable by scrolling. */
  .case-side {
    display: none;
  }

  .case-main { padding-inline: var(--pad-x); gap: 36px; }

  /* The four facts stay on one row at every width, as a single summary
     strip. Columns size to their content and give way down to their longest
     word, so values wrap inside a column rather than the row breaking in
     two. */
  .case-meta {
    display: grid;
    grid-template-columns: repeat(4, auto);
    justify-content: space-between;
    gap: 12px;
    width: 100%;
  }
}

/* A pair that shares one height instead of one width. The columns are in
   the ratio of the two frames (1.475 : 1.772), so each image at its own
   aspect comes out the same height. The second is shown whole; the first,
   a taller photo, is cropped to its frame. */
.case-grid-2--level {
  grid-template-columns: minmax(0, 1.475fr) minmax(0, 1.772fr);
}

.case-grid-2--level img:first-child {
  aspect-ratio: 1.475;
  object-fit: cover;
}

@media (max-width: 48rem) {
  .case-grid-2 { grid-template-columns: 1fr; }

  /* Stacked, there is no shared height to match, so the photo is whole. */
  .case-grid-2--level img:first-child { aspect-ratio: auto; }
}

/* Case-study sub-structure */
.case-lede {
  max-width: 750px;
  font-size: 23px;
  line-height: 27.6px;
  letter-spacing: -0.46px;
  font-weight: 500;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.case-sub {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.case-sub img,
.case-sub video {
  width: 100%;
  height: auto;
}

.case-sub__head {
  font-size: 23px;
  line-height: 27.6px;
  letter-spacing: -0.46px;
  font-weight: 500;
}

.case-footnote {
  color: var(--gray);
  font-size: var(--step--1);
}

/* A status line under the title, e.g. "[case study in progress]". */
.case-note {
  color: var(--mine-shaft);
  font-style: italic;
}

/* The type ramp only has 400 and 500, so emphasis in running copy is the
   medium weight in the text black rather than a synthesised bold. */
.case-copy strong {
  font-weight: 500;
  color: var(--black);
}

/* One piece of media with an optional caption. The hairline keeps white
   diagrams from dissolving into the page. */
.case-figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.case-figure img,
.case-figure video {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-card);
}

.case-figure img {
  outline: 1px solid #ededed;
  outline-offset: -1px;
}

.case-figure figcaption {
  color: var(--gray);
  font-size: 14px;
  line-height: 18.2px;
  letter-spacing: -0.14px;
  font-style: italic;
  text-align: center;
}

/* Before/after comparison. The "after" clip fills the frame and the
   "before" clip sits on top of it, clipped at --pos; dragging anywhere in
   the frame (or the arrow keys, via the hidden range input) moves the
   split. */
.compare {
  --pos: 50%;
  position: relative;
  aspect-ratio: 1284 / 747.86;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: #121212;
  cursor: ew-resize;
  /* Horizontal drags move the split; vertical ones still scroll the page. */
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}

.compare .compare__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  border-radius: 0;
}

.compare .compare__media--before {
  clip-path: inset(0 calc(100% - var(--pos)) 0 0);
}

.compare__divider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos);
  width: 2px;
  margin-left: -1px;
  background: rgba(255, 255, 255, 0.9);
  pointer-events: none;
}

.compare__divider::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.9);
}

.compare__tag {
  position: absolute;
  top: 12px;
  padding: 7px 10px 8px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.45);
  color: var(--white);
  font-size: 11px;
  line-height: 13.2px;
  pointer-events: none;
}

.compare__tag--before { left: 12px; }
.compare__tag--after { right: 12px; }

/* Keyboard access only: it holds focus and takes the arrow keys, while
   pointer dragging is handled on the frame itself so the split lands
   exactly under the cursor. */
.compare__range {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  pointer-events: none;
  cursor: ew-resize;
}

.compare:has(.compare__range:focus-visible) {
  outline: 2px solid var(--hot-cinnamon);
  outline-offset: 2px;
}

.feature-row,
.stat-row {
  display: grid;
  gap: 24px;
  margin: 0;
}

.feature-row { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.stat-row { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 15px; }

.feature__shot {
  background: var(--wild-sand);
  border-radius: var(--radius-card);
  overflow: hidden;
  margin-bottom: 14px;
}

.feature__shot img,
.stat img {
  width: 100%;
  height: auto;
}

.feature__title,
.stat__title {
  font-size: 23px;
  line-height: 27.6px;
  letter-spacing: -0.46px;
  font-weight: 500;
  color: var(--black);
  margin-bottom: var(--sp-2);
}

.stat img {
  border-radius: 6px;
  margin-top: 14px;
}

.feature-row p,
.stat-row p {
  color: var(--body-text);
}

.case-lists {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-5);
}

@media (max-width: 48rem) {
  .case-lists {
    grid-template-columns: 1fr;
    gap: 24px;
  }
}

.case-list ul {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
  color: var(--body-text);
}

.case-list li {
  position: relative;
  padding-left: 16px;
}

.case-list li::before {
  content: "\2022";
  position: absolute;
  left: 0;
  /* Nudged onto the text's own baseline — the glyph sits high otherwise. */
  top: 0;
  line-height: var(--lh-body);
  /* Same grey as the text it belongs to, rather than the case accent. */
  color: var(--body-text);
}

/* Converted GIFs render as looping muted video. */
.masonry video,
.case-stack video,
.case-grid-2 video,
.feature__shot video {
  width: 100%;
  height: auto;
  display: block;
}

/* ==========================================================================
   Footer social stack
   Three cards overlapping in the design's 112x153 box. Positions are
   percentages derived from the Figma frame, so the stack scales as a unit
   instead of breaking apart at other sizes. Each card's own top-left is
   computed from the centre of its (rotation-expanded) bounding box.
   ========================================================================== */

.social-stack {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
}

.social-stack li {
  position: absolute;
  width: 67.857%;
  height: 36.601%;
}

.social-stack__mail { left: 7.145%; top: 4.576%; transform: rotate(-12.44deg); z-index: 1; }
.social-stack__x { left: 19.066%; top: 35.694%; transform: rotate(13deg); z-index: 2; }
.social-stack__linkedin { left: 2.679%; top: 64.052%; z-index: 3; }

.social-stack a,
.social-stack span {
  display: block;
  width: 100%;
  height: 100%;
  transition: transform var(--dur-fast) var(--ease-out);
}

/* Rotation lives on the <li>, so the hover lift here composes with it
   rather than fighting it. */
.social-stack a:hover {
  transform: translateY(-3px);
}

.social-stack img,
.social-stack svg {
  width: 100%;
  height: 100%;
}

/* The cup body is the one white shape in each card. Targeted directly
   rather than through a custom property — one less layer to go wrong, and
   the hover colours come straight from the hover-state designs. */
.social-stack .cup {
  transition: fill var(--dur-fast) var(--ease-out);
}

.social-stack__mail a:hover .cup,
.social-stack__mail a:focus-visible .cup {
  fill: #ffe0fd;
}

.social-stack__x a:hover .cup,
.social-stack__x a:focus-visible .cup {
  fill: #ffedc3;
}

.social-stack__linkedin a:hover .cup,
.social-stack__linkedin a:focus-visible .cup {
  fill: #d8f1ff;
}

/* Case-study: interview quotes, numbered insights, pending-video slot */
/* Interview quotes (Figma 1297:1149). Three bubbles, each its own shape,
   colour and size, overlapping in a 595 x 258.1 field. Positions and insets
   are that node's own coordinates as percentages, so the cluster scales as
   one piece. */
.quote-cluster {
  position: relative;
  width: 595px;
  max-width: 100%;
  aspect-ratio: 595 / 258.1;
  margin: 24px auto 0;
}

.quote {
  position: absolute;
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

.quote--1 {
  left: 9.916%;
  top: 0;
  width: 53.95%;
  height: 50.407%;
  background-image: url("/assets/ui/bubble-1.svg");
}

.quote--2 {
  left: 0;
  top: 53.894%;
  width: 52.542%;
  height: 46.106%;
  background-image: url("/assets/ui/bubble-male.svg");
}

.quote--3 {
  left: 50.084%;
  top: 37.234%;
  width: 49.916%;
  height: 39.985%;
  background-image: url("/assets/ui/bubble-right.svg");
}

.quote__inner {
  position: absolute;
  display: flex;
  flex-direction: column;
  gap: 6.05px;
}

.quote--1 .quote__inner { left: 6.23%; top: 10.84%; width: 92.64%; }
.quote--2 .quote__inner { left: 5.97%; top: 11.9%;  width: 88.64%; }
.quote--3 .quote__inner { left: 4.71%; top: 12.6%;  width: 93.27%; }

/* All three names share one grey now — the bubbles were recoloured from
   pink/blue/cream to a neutral #FAFAFA fill with an #EDEDED edge. */
.quote cite {
  font-style: normal;
  font-size: 16px;
  line-height: 1.2;
  letter-spacing: var(--ls-body);
  color: var(--gray);
}

.quote blockquote {
  margin: 0;
  font-size: 14px;
  line-height: 1.32;
  letter-spacing: 0;
  color: var(--body-text);
}

/* Accent sub-labels inside a section: 16/20.8 in the case's accent colour,
   with the design's 34px of air above. These are a different level from the
   23px black sub-headings and were being rendered as those. */
.case-label {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  font-weight: 400;
  color: var(--hot-cinnamon);
}

.insight-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  margin: 0;
}

.insight__title {
  font-size: 23px;
  line-height: 27.6px;
  letter-spacing: -0.46px;
  font-weight: 500;
  color: var(--black);
  margin-bottom: var(--sp-4);
}

.insight p {
  color: var(--body-text);
  max-width: 750px;
}

/* The takeaway is indented 70px from the body it follows. */
/* Qualified with the element so it outranks .insight p, which is (0,1,1) and
   would otherwise pull the takeaway back to body grey. */
.insight p.insight__takeaway {
  margin-top: var(--sp-4);
  margin-left: 70px;
  color: var(--hot-cinnamon);
}

.case-videoslot {
  width: 100%;
  aspect-ratio: 1432 / 800.4;
}

/* ==========================================================================
   Case-study solution rows  (Figma 1276:3513)
   Rows are 668px tall, contents centred as a pair, sides alternating. The
   text column is 306px and the phone 415px with no gap between them — the
   breathing room comes from the screen being inset inside the phone box.
   ========================================================================== */

.solution-row {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 668px;
}

/* Both children need an explicit order. With only the media ordered, the
   text defaulted to 0 — below either value — so it always came first and
   the flip did nothing. */
.solution-row__text { order: 1; }
.solution-row__media { order: 2; }

.solution-row--flip .solution-row__text { order: 2; }
.solution-row--flip .solution-row__media { order: 1; }

.solution-row__text {
  flex: none;
  width: 306px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.solution-row__title {
  font-size: 23px;
  line-height: 27.6px;
  letter-spacing: -0.46px;
  font-weight: 500;
  color: var(--black);
}

.solution-row__text p {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  color: var(--body-text);              /* the design's "gray", lighter than Mine Shaft */
}

/* Phone mockup: the screen sits under the bezel PNG, both at the design's
   exact offsets within a 415 x 620.17 box. */
.phone {
  position: relative;
  /* So the screen's corner radius can be expressed against the phone's own
     width and scale with it. */
  container-type: inline-size;
  /* Fills whatever width its flex parent is given. Sizing it as a percentage
     here collapsed it: the parent is sized by this element, so the
     percentage had nothing to resolve against. */
  width: 100%;
  aspect-ratio: 415 / 620.17;
  overflow: hidden;
}

.phone__screen {
  position: absolute;
  inset: 5% 18.96% 5% 18.95%;
  /* 30px at the design's 415 width, scaling from there. */
  border-radius: 7.23cqw;
  overflow: hidden;
}

.phone__screen img,
.phone__screen video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Percentages of the phone box, so the bezel tracks the screen at any size.
   These are the design's pixel offsets divided by the 415 x 620.17 box. */
.phone__frame {
  position: absolute;
  left: 14.886%;
  top: 2.096%;
  width: 70%;
  height: 96%;
  pointer-events: none;
}

.solution-row__cluster {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  align-items: center;
  width: 415px;
  flex: none;
}

.solution-row__cluster img,
.solution-row__cluster video {
  width: 100%;
  height: auto;
  border-radius: 14px;
}

/* The row is short of space from about 975px down, so the phone gives up
   more than the 55px it concedes on a full desktop. The 1/12 indent goes
   too — it is 74px the text cannot spare here. */
@media (max-width: 68rem) {
  .case-main--narrow .solution-row:not(.solution-row--flip) .solution-row__text {
    padding-left: 0;
    max-width: 100%;
  }
}

/* The rows' stacked layout lives in the 48rem block above, scoped to
   .case-main--narrow. This block used to repeat it unscoped at 62rem, which
   meant every case study stacked at 992px — before the narrow rule could
   apply — and stacked text-first rather than phone-first. */
@media (max-width: 48rem) {
  .solution-row__cluster { width: 100%; max-width: 420px; }
}


/* ==========================================================================
   Bumble — Notifications  (Figma 1276:1966)
   Two cropped phone fragments rather than whole devices: the top of one
   handset and the bottom of another, each faded off where it's cut.
   ========================================================================== */

.notif-stack {
  flex: none;
  width: 447px;
  height: 577px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  align-items: center;
  justify-content: center;
}

.notif-phone {
  position: relative;
  width: 415px;
  flex: none;
  overflow: hidden;
}

.notif-phone--top { height: 223px; }
.notif-phone--bottom { height: 297px; }

.notif-phone__screen {
  position: absolute;
  overflow: hidden;
}

.notif-phone__screen img,
.notif-phone__screen video {
  position: absolute;
  left: 0;
  max-width: none;
}

/* Top fragment: screen rounded only at the top, since the bottom is cut. */
.notif-phone--top .notif-phone__screen {
  left: 36px;
  top: 31px;
  width: 261px;
  height: 181px;
  border-radius: 30px 30px 0 0;
}

.notif-phone--top .notif-phone__screen img,
.notif-phone--top .notif-phone__screen video {
  top: 1.99px;
  width: 260px;
  height: 244.34px;
}

/* Bottom fragment: rounded only at the bottom, screen pulled up so the
   lower part of the shot shows. */
.notif-phone--bottom .notif-phone__screen {
  left: 107px;
  top: 9px;
  width: 258px;
  height: 174px;
  border-radius: 0 0 30px 30px;
}

.notif-phone--bottom .notif-phone__screen img,
.notif-phone--bottom .notif-phone__screen video {
  top: -72.98px;
  width: 258px;
  height: 245.33px;
}

.notif-phone__bezel {
  position: absolute;
  overflow: hidden;
}

.notif-phone__bezel img {
  position: absolute;
  left: 0;
  width: 296px;
  max-width: none;
}

.notif-phone--top .notif-phone__bezel {
  left: 17px;
  top: 23px;
  width: 296px;
  height: 189px;
}

.notif-phone--top .notif-phone__bezel img {
  top: -7.54px;
  height: 204.08px;
}

.notif-phone--bottom .notif-phone__bezel {
  left: 88px;
  top: 9px;
  width: 296px;
  height: 189px;
}

.notif-phone--bottom .notif-phone__bezel img {
  top: -3.19px;
  height: 195.41px;
}

/* The fades that sell the crop. The design fades to #FAFAFA; the page here
   is white, so they fade to white or the seam would show as a grey band. */
.notif-phone__fade {
  position: absolute;
  pointer-events: none;
}

.notif-phone--top .notif-phone__fade {
  left: 17px;
  bottom: 11px;
  width: 293px;
  height: 49px;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff);
}

.notif-phone--bottom .notif-phone__fade {
  left: 88px;
  top: 7px;
  width: 296px;
  height: 26px;
  background: linear-gradient(to bottom, #fff, rgba(255, 255, 255, 0));
}

/* Fixed 447x577 can't shrink, so scale the whole cluster as a unit and
   claw back the height the transform doesn't remove from layout. */
@media (max-width: 62rem) {
  .notif-stack {
    transform: scale(0.72);
    transform-origin: top left;
    margin-bottom: -161px;
  }
}

@media (max-width: 30rem) {
  .notif-stack {
    transform: scale(0.55);
    margin-bottom: -260px;
  }
}

/* ==========================================================================
   Narrow case-study column  — currently on bumble only, as a trial
   ========================================================================== */

@media (min-width: 68.01rem) {
  /* Mirrors the 201px side nav on the right so the auto margins below centre
     the column on the page rather than in the space left over beside the nav. */
  /* The content column sits between two rails, with the section nav living
     in the left one. The left rail is 60px wider than the right: at 200 the
     longest nav labels wrapped and still ended 26px from the content, so
     the column is nudged right to give the nav room (50px clear of the
     longest label, on Typekins). Both rails grow together once the column
     reaches its 1188 cap. 980 of content on a 1440 screen. */
  .case-body--narrow {
    display: grid;
    grid-template-columns: minmax(260px, 1fr) minmax(0, 1188px) minmax(200px, 1fr);
    align-items: start;
  }

  .case-body--narrow .case-side {
    width: auto;
  }
}

/* The rails supply the space either side, so the column has no padding of
   its own until the layout stacks. */
.case-main--narrow {
  max-width: 1188px;
  padding-inline: 0;
  margin-inline: 0;
}

/* Below the stacking breakpoint the page has no other side padding, so the
   column needs its own again. */
@media (max-width: 68rem) {
  .case-main--narrow {
    padding-inline: var(--pad-x);
    max-width: calc(960px + var(--pad-x) * 2);
  }
}

/* At 900 the solution rows still fit side by side (306 + 415 = 721), so the
   alternating rhythm survives — it was lost at 768. Only the genuinely wide
   blocks below need to collapse. */
.case-main--narrow .solution-row {
  /* Edge to edge rather than centred as a pair, so the text starts on the
     column's left edge and the phone ends on its right — the pair was
     floating free of the grid before. */
  justify-content: space-between;
  min-height: 520px;
  /* More air between features than the wide layout needs, since the rows are
     physically closer together in a narrower column. */
  padding-block: var(--sp-5);
  /* The design runs text and phone flush, relying on the screen being inset
     within the phone box for separation. At 340px that inset shrinks with
     everything else, so the pair needs a real gap here. */
  gap: 48px;
}

/* Text keeps a readable measure while figures take the full column — the
   reference runs body copy at 36-42rem inside its 69.25rem figure column,
   rather than letting both span the same width. */
/* Body copy runs to the end of the 9th column. Written as a fraction of the
   row like the indent, so it stays on the guides if the container moves. */
.case-main--narrow .case-lede,
.case-main--narrow .case-copy,
.case-main--narrow .case-copy--narrow,
.case-main--narrow .case-copy--reflect,
.case-main--narrow .insight p {
  /* Capped at the design's 750 measure: the column is wide for the sake of
     the figures, and nine twelfths of it is too long a line to read. */
  max-width: min(calc(100% / 12 * 9), 750px);
  width: 100%;
}

/* A note set against the right half of the column: it starts at the middle
   and runs to the right edge. Back to full width once the column is too
   narrow for half of it to hold a readable line. */
.case-main--narrow .case-copy--half-right {
  width: 50%;
  max-width: 50%;
  margin-left: 50%;
}

@media (max-width: 48rem) {
  .case-main--narrow .case-copy--half-right {
    width: 100%;
    max-width: 100%;
    margin-left: 0;
  }
}

/* The summary block keeps a measure rather than running the full 896. The
   reference splits its top section into a 505px description and a 343px
   meta column — it never spans the content width either. */
.case-main--narrow .case-head {
  max-width: 615px;
}

/* Text column takes the space the phone doesn't, instead of a fixed 306px
   that leaves a ragged right edge. */
.case-main--narrow .solution-row__text {
  width: auto;
  flex: 1 1 auto;
  max-width: 440px;
}

/* A fixed share of the row, not a flex remainder. With flex: 0 1 415px the
   phone took whatever the text left over, and since the text is sized by its
   own content, every row ended up with a differently sized phone. 46% of the
   896 content box is 412, so it still reaches the design's 415 cap. */
.case-main--narrow .solution-row__media {
  flex: none;
  width: 46%;
  max-width: 415px;
  min-width: 0;
}

/* The notifications cluster is its own fixed-size composition, not a phone. */
.case-main--narrow .solution-row__media:has(.notif-stack) {
  width: auto;
  max-width: none;
}

/* Stacked layout, below 48rem — roughly half a MacBook screen. This has to
   sit after the side-by-side rules above: media queries add no specificity,
   so an earlier copy loses to them and its sizing never applies. That is
   exactly what left the phones at 46% here instead of the intended cap. */
@media (max-width: 48rem) {
  .case-main--narrow .solution-row,
  .case-main--narrow .solution-row--flip {
    flex-direction: column;
    /* flex-start, so the text sits flush left like every other paragraph on
       the page. The phone still centres: an auto inline margin on a flex item
       wins over align-items. */
    align-items: flex-start;
    /* Tight to the text it belongs with, so the space between features reads
       as the separation instead. */
    gap: 14px;
    min-height: 0;
    padding-block: 44px;
  }

  /* One explicit width for every phone — no percentage of the row, no flex
     remainder, so they cannot drift apart from each other. */
  .case-main--narrow .solution-row__media,
  .case-main--narrow .solution-row--flip .solution-row__media,
  .case-main--narrow .solution-row__media:has(.notif-stack) {
    order: 1;
    flex: none;
    width: 300px;
    max-width: 100%;
    min-width: 0;
    margin-inline: auto;
  }

  .case-main--narrow .solution-row__text,
  .case-main--narrow .solution-row--flip .solution-row__text,
  .case-main--narrow .solution-row:not(.solution-row--flip) .solution-row__text {
    order: 2;
    width: 100%;
    /* The same 9-of-12 measure the rest of the body copy uses, rather than
       running the full width. */
    max-width: calc(100% / 12 * 9);
    padding-left: 0;
  }

  /* The cluster is a fixed 447 composition, so it scales rather than resizes. */
  .case-main--narrow .notif-stack {
    transform: scale(0.67);
    transform-origin: top center;
    margin: 0 auto -190px;
  }
}

/* Rows whose text sits on the left get it indented from the column edge, with
   the phone staying flush right. 40px rather than 48: the notifications row
   carries a 393px cluster instead of a 340px phone, and 48 would overflow the
   768 column. */
/* Indent to the start of the second column so the text aligns with the grid.
   Bounded with min-width rather than left open: this rule sits after the
   stacked block, and media queries add no specificity, so unbounded it
   re-applied below 48rem — indenting non-flip rows only, which is the
   alternation showing up in a layout that has no left and right any more. */
@media (min-width: 48.01rem) {
  .case-main--narrow .solution-row:not(.solution-row--flip) .solution-row__text {
    padding-left: calc(100% / 12);
    max-width: calc(100% / 12 + 406px);
  }
}

/* Quantitative charts: three rows of image-left / text-right rather than
   three columns. The .stat markup is <img>, <h3>, <p>, so the image spans
   both rows of a two-column grid and the text stacks beside it. */
.case-main--narrow .stat-row {
  grid-template-columns: 1fr;
  gap: var(--sp-5);
}

.case-main--narrow .stat {
  display: grid;
  grid-template-columns: 320px minmax(0, 1fr);
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: 24px;
  row-gap: 4px;
}

.case-main--narrow .stat img {
  grid-column: 1;
  grid-row: 1 / 3;
  margin-top: 0;
  align-self: center;
}

.case-main--narrow .stat__title {
  grid-column: 2;
  align-self: end;
  margin-bottom: 0;
}

.case-main--narrow .stat p {
  grid-column: 2;
  align-self: start;
}

/* The cluster is 602px wide and simply scales down inside a narrower
   column — no re-flow needed. */

/* Trial column: smaller feature phones, full-size notification cluster. */
/* The 1108 column has room for the phone at its design size again; the 340px
   version existed only because the trial column was 768. */

/* Scaled back slightly. A transform leaves the element's layout height at
   the original 577px, so the negative margin reclaims what it no longer
   occupies — otherwise the section carries 69px of dead space. */
.case-main--narrow .notif-stack {
  transform: scale(0.88);
  transform-origin: top center;
  margin-bottom: -69px;
}

/* ==========================================================================
   Gallery hover
   Title bottom-left over a gradient that fades up from the image's own
   bottom edge. Only items that carry a title show anything.
   ========================================================================== */

.gal-item {
  position: relative;
  display: block;
  margin: 0;
  overflow: hidden;
  border-radius: var(--radius-card);
}

.gal-item img,
.gal-item video {
  display: block;
  width: 100%;
  height: auto;
}

/* The blur, the gradient and the caption all fade in together. */
.gal-item::before,
.gal-item::after,
.gal-item figcaption {
  opacity: 0;
  transition: opacity var(--dur) var(--ease-out);
  pointer-events: none;
}

.gal-item:not([data-title=""])::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* White rather than black, so the caption sits in the page's own light
     rather than a dark scrim over the work. */
  height: 38%;
  background: linear-gradient(
    to top,
    rgba(255, 255, 255, 1),
    rgba(255, 255, 255, 0.68) 55%,
    rgba(255, 255, 255, 0)
  );
}

/* Progressive blur under the gradient: the work is blurred where the caption
   sits and comes back into focus towards the top of the band. One blurred
   layer, masked so that it thins out upwards — full strength for the bottom
   third, gone by the top. Hover only: on touch the caption is always
   showing, and a live backdrop blur on every item (several are video) is
   too much to keep running. */
@media (hover: hover) {
  .gal-item:not([data-title=""])::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 38%;
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    -webkit-mask-image: linear-gradient(to top, #000 30%, transparent);
    mask-image: linear-gradient(to top, #000 30%, transparent);
  }
}

.gal-item figcaption {
  position: absolute;
  /* Above the gradient. ::after paints after the element's content, so
     without this the white title sat underneath it and vanished — most
     obviously on the lighter images. */
  z-index: 1;
  left: 14px;
  right: 14px;
  bottom: 12px;
  color: var(--body-text);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
}

/* focus-visible, not focus-within: closing the lightbox returns focus to the
   item that opened it, and focus-within kept the caption showing afterwards.
   focus-visible only fires for keyboard focus, which is the case that
   actually needs it. */
.gal-item:not([data-title=""]):hover::before,
.gal-item:not([data-title=""]):hover::after,
.gal-item:not([data-title=""]):hover figcaption,
.gal-item:not([data-title=""]):focus-visible::before,
.gal-item:not([data-title=""]):focus-visible::after,
.gal-item:not([data-title=""]):focus-visible figcaption {
  opacity: 1;
}

@media (hover: none) {
  /* No hover to reveal it on touch, so the title stays visible. */
  .gal-item:not([data-title=""])::after,
  .gal-item:not([data-title=""]) figcaption {
    opacity: 1;
  }
}

/* ==========================================================================
   Gallery lightbox
   ========================================================================== */

.gal-item {
  cursor: zoom-in;
}

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(24px, 5vw, 64px);
  /* Tinted with the same grey the tag pills use (--wild-sand #f5f5f5).
     Opaque enough to quiet the gallery behind it, still translucent. */
  background: rgba(245, 245, 245, 0.78);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}

.lightbox[hidden] {
  display: none;
}

.lightbox__figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  max-width: 100%;
  max-height: 100%;
}

.lightbox__media {
  display: flex;
  min-height: 0;
}

.lightbox__media img,
.lightbox__media video {
  display: block;
  max-width: 100%;
  /* Leaves room for the caption under the tallest image. */
  max-height: calc(100vh - clamp(24px, 5vw, 64px) * 2 - 48px);
  width: auto;
  height: auto;
  border-radius: var(--radius-card);
}

/* Bottom left, aligned to the image's own left edge. */
.lightbox__caption {
  color: var(--body-text);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
}

.lightbox__close {
  position: absolute;
  top: clamp(16px, 3vw, 28px);
  right: clamp(16px, 3vw, 28px);
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  padding: 0;
  /* Just the x — no circle behind it. The 36px box stays as the hit area. */
  border: 0;
  background: none;
  color: var(--body-text);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}

.lightbox__close:hover {
  color: var(--black);
}

.lightbox__close svg {
  width: 16px;
  height: 16px;
}
