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

/* Phones run the page under the status bar and the browser's toolbar (viewport-fit=cover). iOS
   Safari reads the root backgrounds for its bars, so (per Josh Puckett's technique) html carries the
   top edge's colour and body the bottom edge's, both solid and kept current by safari-edges.js. The
   body paints its own white over its colour, so the page itself looks exactly as before. */
html {
  background-color: var(--edge-top, #fff);
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  background-color: var(--edge-bottom, #fff);
  /* Leave the last two pixels to the matched sky colour, avoiding a fractional white seam. */
  background-image: linear-gradient(#fff calc(100% - 2px), transparent 0);
  color: #000;
  font-family: "GNU Grotesk", sans-serif;
  /* Kerning stays on. */
  font-kerning: normal;
  font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
  font-synthesis: none;
  letter-spacing: 0;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
  /* Colour only: the background follows the bottom edge at once, so Safari's toolbar never lags. */
  transition: color 0.7s ease;
}

button,
input,
select,
textarea {
  font: inherit;
}

/* The site's left and right margin, one value for the page, the menu and the project pages,
   stepping up on the 4/8px scale as the screen widens. */
:root {
  --gutter: 32px;
}

@media (min-width: 721px) {
  :root {
    --gutter: 96px;
  }
}

@media (min-width: 1024px) {
  :root {
    --gutter: 208px;
  }
}

@media (min-width: 1440px) {
  :root {
    --gutter: 288px;
  }
}

@media (min-width: 1600px) {
  :root {
    --gutter: 384px;
  }
}

/* Four text styles. 20 / 28 / 17 are the web sizes; phones use the same ratio, about four fifths. */
:root {
  /* Card corners: design units per canvas pixel (--u), shared by tiles, grid, and case media. */
  --corner-ratio: 16;
  --corner-min: 12px;
  --tile-inner-stroke: rgba(0, 0, 0, 0.03);
  --tile-inner-stroke-disco: rgba(255, 255, 255, 0.05);
  --text-ui: 16px;
  --text-intro: 20px;
  --text-body: 16px;
  --text-display: 44px;
  --track-ui: 0;
  --track-intro: 0;
  --track-body: 0;
  --track-display: 0;
  --leading-body: 1.6;
  --player-chrome-radius: 12px;
  --player-track-radius: 2px;
  --player-control-size: 36px;
  --player-bar-padding: 6px;
  --player-bar-height: calc(var(--player-control-size) + 2 * var(--player-bar-padding));
  --player-bar-inset: 28px;
  /* Inner control corners scale with the frosted bar radius. */
  --player-button-radius: calc(var(--player-chrome-radius) * var(--player-control-size) / var(--player-bar-height));
}

@media (min-width: 721px) {
  :root {
    --text-ui: 20px;
    --text-intro: 24px;
    --text-body: 16px;
    --text-display: clamp(56px, 6.7vw, 112px);
  }
}

.page {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto 1fr auto;
  justify-items: stretch;
  width: min(1720px, 100%);
  margin: 0 auto;
  padding: clamp(24px, 2.7vw, 44px) var(--gutter) 0;
}

.page > .site-header {
  grid-row: 1;
}

.work-route,
.about-page {
  grid-row: 2;
  grid-column: 1;
  min-width: 0;
  transition: none !important;
}

.about-page[hidden],
.work-route[hidden] {
  display: none !important;
}

.page > .site-footer {
  grid-row: 3;
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px clamp(16px, 1.95vw, 32px);
  min-height: 56px;
  margin-top: calc(-1 * clamp(24px, 2.7vw, 44px));
  padding-block: 28px 12px;
}

/* The bar's background bleeds to the viewport edges, and no further: anything wider makes phones
   widen the whole page to fit it. The header sits centred in the page, so its middle is the viewport's. */
.site-header::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0 calc(50% - 50vw);
  transition: background-color 0.7s ease;
}

/* Progressive blur under the header: three backdrop blurs, each masked to its own band so
   the blur deepens toward the top edge, then a wash that fades the page out to white. */
/* iOS Safari colours the area behind its status bar from a solid fixed element touching the top of
   the page (Josh Puckett, "Seamless Backgrounds on Mobile Safari"). This 12px strip is that element:
   solid, in the colour at the top of the page (safari-edges.js), sitting over the header's white
   wash where it can't be seen. The bottom toolbar floats over the page, so it needs no strip; past
   the end of the page Safari shows the body's colour, which follows the bottom edge. */
.edge-top {
  position: fixed;
  z-index: 200;
  top: 0;
  right: 0;
  left: 0;
  height: 12px;
  background: var(--edge-top, #fff);
  pointer-events: none;
}

.top-edge {
  --edge-tint: 255, 255, 255;
  position: fixed;
  z-index: 90;
  top: 0;
  right: 0;
  left: 0;
  height: clamp(140px, 13vw, 200px);
  pointer-events: none;
}

.top-edge > span {
  position: absolute;
  inset: 0;
}

.top-edge-blur {
  /* A trace of colour keeps Safari rendering the backdrop filter. */
  background: rgba(255, 255, 255, 0.004);
}

.top-edge-blur:nth-child(1) {
  -webkit-backdrop-filter: blur(1.5px);
  backdrop-filter: blur(1.5px);
  -webkit-mask-image: linear-gradient(to top, transparent 0%, #000 25%, #000 50%, transparent 75%);
  mask-image: linear-gradient(to top, transparent 0%, #000 25%, #000 50%, transparent 75%);
}

.top-edge-blur:nth-child(2) {
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
  -webkit-mask-image: linear-gradient(to top, transparent 25%, #000 50%, #000 75%, transparent 100%);
  mask-image: linear-gradient(to top, transparent 25%, #000 50%, #000 75%, transparent 100%);
}

.top-edge-blur:nth-child(3) {
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  -webkit-mask-image: linear-gradient(to top, transparent 50%, #000 75%, #000 100%);
  mask-image: linear-gradient(to top, transparent 50%, #000 75%, #000 100%);
}

.top-edge-wash {
  /* Solid white through the header's band, then a long fade. */
  background: linear-gradient(
    rgb(var(--edge-tint)) 0%,
    rgb(var(--edge-tint)) 22%,
    rgba(var(--edge-tint), 0.95) 32%,
    rgba(var(--edge-tint), 0.86) 42%,
    rgba(var(--edge-tint), 0.72) 52%,
    rgba(var(--edge-tint), 0.54) 62%,
    rgba(var(--edge-tint), 0.36) 72%,
    rgba(var(--edge-tint), 0.19) 82%,
    rgba(var(--edge-tint), 0.06) 92%,
    rgba(var(--edge-tint), 0) 100%
  );
}

.disco .top-edge {
  --edge-tint: 0, 0, 0;
}

/* Project pages are a full-screen layer over the work page; lift the same top-edge above the modal
   so scroll bleeds through the progressive blur, not the modal's flat gradient wash. */
.is-project-open {
  --project-top-blur: clamp(96px, 9.5vw, 132px);
}

.is-project-open .top-edge {
  z-index: 115;
  height: var(--project-top-blur);
}

html.is-project-open:not(.is-project-closing) .top-edge {
  transition: none;
}

/* Keep the same stacking as while the project is open so the band can grow on top of the
   fading sheet instead of finishing its transition hidden underneath it. */
html.is-project-open.is-project-closing .top-edge {
  z-index: 115;
  height: var(--project-top-blur);
  transition: none;
}

html.is-project-open.is-top-edge-expanding .top-edge {
  height: clamp(140px, 13vw, 200px);
  transition: height 420ms cubic-bezier(0.4, 0, 0.2, 1);
}

.is-project-open .modal-shell::after {
  display: none;
}

/* Margin on the cover, not scroll padding: extra padding-top scrolls on its own and drags the
   picture downward before the case study moves. */
.is-project-open .modal-scroll {
  padding-top: 0;
}

.is-project-open .modal-hero {
  margin-top: calc(var(--project-top-blur) + 12px);
}

@media (max-width: 720px) {
  /* Match the work-page top-edge band so the cover never sits under the fixed blur wash. */
  .is-project-open {
    --project-top-blur: clamp(140px, 13vw, 200px);
  }

  .is-project-open .modal-hero {
    margin-top: max(
      calc(var(--project-top-blur) + 20px),
      calc(var(--header-h, 72px) + 20px)
    );
  }

  .modal-hero.is-cover {
    top: max(calc(var(--header-h, 0px) + 12px), calc(var(--project-top-blur) + 12px));
  }
}

/* Figma: 40px regular on an 1800px frame; scaled to match the filter tabs below. */
.wordmark,
.site-nav a {
  color: inherit;
  font-size: var(--text-ui);
  font-weight: 500;
  letter-spacing: var(--track-ui);
  line-height: 1.5;
  text-decoration: none;
}

.site-nav a {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
}

/* Blue dot for the page you're on, grey for the rest. Colour only, so it changes in the same frame. */
.nav-dot {
  flex: none;
  width: 0.375em;
  height: 0.375em;
  border-radius: 50%;
  background: #c7c7c7;
}

.site-nav a[aria-current="page"] .nav-dot {
  background: #004dff;
}

.doodle {
  width: 18px;
  height: 18px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1);
}

.disco .site-header::before {
  background: transparent;
}

/* The name is the way home: a link back to the top of the work page, led by a small star. */
.wordmark {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* A little eight-petal flower. */
.wordmark-star {
  flex: none;
  width: 0.8em;
  height: 0.8em;
  transition: rotate 0.5s cubic-bezier(0.23, 1, 0.32, 1);
}

@media (hover: hover) {
  .wordmark:hover .wordmark-star {
    rotate: 45deg;
  }
}

.site-nav {
  position: relative;
  z-index: 70;
}

.nav-menu {
  display: flex;
  flex-wrap: wrap;
  gap: 0 16px;
  font-size: var(--text-ui);
  letter-spacing: var(--track-ui);
}

.nav-toggle {
  display: none;
}

/* Phones: a two-line burger opens a full-screen menu that fades in over the page. */
@media (max-width: 720px) {
  .nav-toggle {
    position: relative;
    z-index: 1;
    display: grid;
    place-content: center;
    gap: 7px;
    width: 48px;
    height: 48px;
    margin-right: -12px;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }

  .nav-toggle span {
    display: block;
    width: 26px;
    height: 2px;
    border-radius: 2px;
    background: currentColor;
    transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1);
  }

  .menu-open .nav-toggle span:first-child {
    transform: translateY(4.5px) rotate(45deg);
  }

  .menu-open .nav-toggle span:last-child {
    transform: translateY(-4.5px) rotate(-45deg);
  }

  .nav-menu {
    position: fixed;
    inset: 0;
    flex-direction: column;
    align-items: flex-start;
    flex-wrap: nowrap;
    gap: clamp(22px, 6vw, 32px);
    padding: 120px var(--gutter) 40px;
    background: #fff;
    color: #111;
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.2s ease, visibility 0.2s;
  }

  .menu-open .nav-menu {
    visibility: visible;
    opacity: 1;
    transition: opacity 0.2s ease;
  }

  .site-nav .nav-menu a {
    gap: 12px;
    padding: 4px 0;
    font-size: clamp(28px, 7.4vw, 36px);
    font-weight: 500;
    letter-spacing: var(--track-intro);
    line-height: 1.15;
  }

  /* The wordmark and close button sit above the menu. */
  .menu-open .wordmark {
    z-index: 71;
  }

  .menu-open .site-header::before {
    background: transparent;
  }

  /* Lock scrolling on the root only: hiding body's overflow too would make it a scroll box,
     which unpins the sticky header (and the close button with it). */
  .menu-open {
    overflow: hidden;
  }

  .menu-open .disco-ball {
    visibility: hidden;
  }
}

@media (prefers-reduced-motion: reduce) {
  .nav-toggle span,
  .nav-menu,
  .menu-open .nav-menu {
    transition: none;
  }
}

.site-nav a:focus-visible,
.nav-toggle:focus-visible,
.wordmark:focus-visible {
  outline: 2px solid #000;
  outline-offset: 4px;
}

/* Intro line and sketched computer from the Figma header. Hidden for now; the markup stays. */
.hero[hidden] {
  display: none !important;
}

.hero {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: clamp(16px, 2.5vw, 48px);
  padding-block: clamp(64px, 9vw, 160px);
}

.intro {
  flex: 0 0 auto;
  /* A measure where the two paragraphs wrap into a similar block, not a ragged column. */
  width: 16.5em;
  max-width: 100%;
  color: #000;
  font-size: 20px;
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 1.6;
}

@media (min-width: 721px) {
  .intro {
    font-size: 22px;
  }
}

.intro p {
  margin: 0;
  text-wrap: balance;
}

.intro p + p {
  margin-top: 1lh;
}

/* Highlighted words in the intro redraw the scene around the computer. */
.scene-word {
  display: inline;
  margin: 0 -0.14em;
  padding: 0.06em 0.14em;
  border: 0;
  border-radius: 0;
  background: #efeeea;
  color: inherit;
  font: inherit;
  /* Buttons size to their own line, so a tighter one than the paragraph's leaves a gap between lines. */
  line-height: 1.2;
  letter-spacing: inherit;
  text-align: inherit;
  cursor: pointer;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.25s ease, color 0.25s ease;
}

@media (hover: hover) {
  .scene-word:hover {
    background: #e2e1dc;
  }
}

.scene-word:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

/* Selected, each word takes its own muted tint, with a deeper shade of it for the text. */
.scene-word[data-scene="julia"][aria-pressed="true"] {
  background: #f1dfdb;
  color: #b0503e;
}

.scene-word[data-scene="vancouver"][aria-pressed="true"] {
  background: #dce6ee;
  color: #3a6f9f;
}

.scene-word[data-scene="warmth"][aria-pressed="true"] {
  background: #f1e4c9;
  color: #a06c18;
}

.disco .scene-word[data-scene="julia"][aria-pressed="true"] {
  background: rgba(214, 150, 140, 0.32);
  color: #f3d6d0;
}

.disco .scene-word[data-scene="vancouver"][aria-pressed="true"] {
  background: rgba(130, 165, 195, 0.32);
  color: #d4e3ef;
}

.disco .scene-word[data-scene="warmth"][aria-pressed="true"] {
  background: rgba(205, 165, 95, 0.32);
  color: #f0e0bf;
}

/* Hero windows from Figma: a stage beside the intro that the windows can be dragged and flung
   around in. It stretches to the page's right edge; the windows keep their designed size, in
   design units (--u = one pixel of the 643-wide Figma window group). */
@property --cover-k {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}

@property --u {
  syntax: "<length>";
  inherits: true;
  initial-value: 1px;
}

.windows {
  container-type: inline-size;
  /* Scale to the room left beside the intro, so a wider page margin doesn't push the stage past it. */
  --u: calc(min(100cqw, 320px) / 643);
  position: relative;
  /* Keep draggable windows and postcard trees below the page header. */
  isolation: isolate;
  flex: 1 1 0;
  min-width: 0;
  max-width: 320px;
  aspect-ratio: 720 / 660;
}

/* On a large screen the stage stays 320 wide, and the intro gives up room if the margin needs it. */
@media (min-width: 1280px) {
  .hero .intro {
    flex: 1 1 auto;
    min-width: 0;
  }

  .windows {
    flex: 0 0 320px;
    width: 320px;
  }
}

/* Yarn strung between the windows (yarn.js), over them. Its canvas runs on below the stage so a
   slack strand can hang past the bottom window. It steps away while a scene is in. */
.yarn {
  position: absolute;
  z-index: 1000;
  top: 0;
  left: 0;
  width: 100%;
  height: 135%;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.windows.is-scene .yarn {
  opacity: 0;
}

/* Registered so the Julia photo can glide between window and photo sizes. */
@property --x {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

@property --y {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

@property --w {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

@property --h {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

.window {
  position: absolute;
  top: calc(var(--y) * var(--u));
  left: calc(var(--x) * var(--u));
  width: calc(var(--w) * var(--u));
  height: calc(var(--h) * var(--u));
  overflow: hidden;
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.window.is-dragging {
  cursor: grabbing;
}

.window:focus-visible {
  outline: 2px solid #003cff;
  outline-offset: 2px;
}

/* Film grain over each window: grey noise in overlay keeps each colour's tone while adding texture.
   It lifts off the photo window when it becomes the print or postcard, which have their own finish. */
.window::after {
  content: "";
  position: absolute;
  z-index: 5;
  inset: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='1 0 0 0 0  1 0 0 0 0  1 0 0 0 0  0 0 0 0 0.5'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px;
  mix-blend-mode: overlay;
  opacity: 0.55;
  pointer-events: none;
  transition: opacity 0.22s ease;
}

.windows.is-scene .is-scene-window::after {
  opacity: 0;
}

.window-photo {
  background: #d6faff;
}

.window-rabbit {
  background: rgba(207, 213, 122, 0.4);
}

.window-blank {
  background: rgba(196, 161, 218, 0.4);
}

.window-bar {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: calc(30 * var(--u));
  background: #f8f7e3;
}

.window-buttons {
  position: absolute;
  top: calc(9 * var(--u));
  left: calc(9 * var(--u));
  width: calc(47 * var(--u));
  height: calc(11 * var(--u));
}

.window-art {
  position: absolute;
  max-width: none;
  pointer-events: none;
}

.photo-art {
  top: calc(30 * var(--u));
  left: calc(-1 * var(--u));
  width: calc(354 * var(--u));
  height: calc(354 * var(--u));
  object-fit: cover;
}

/* The shader canvas replaces the plain photo once it has drawn; until then (or without WebGPU)
   the photo shows. */
.photo-pixels {
  visibility: hidden;
}

.window-photo.is-pixelated .photo-pixels {
  visibility: visible;
}

.window-photo.is-pixelated img.photo-art {
  visibility: hidden;
}

/* "Julia": the photo window becomes a flat print of Figma's dash effect (393 x 425), centred in
   the stage. The window glides to the print's spot (set by windows.js) and loses its chrome. */
.julia-art,
.photo-glare {
  position: absolute;
  pointer-events: none;
  opacity: 0;
}

.julia-art {
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  object-position: center bottom;
}

/* Figma's dash effect over the print, drawn at the print's full size (393 x 425) so the dashes
   don't stretch while the window grows into it. It replaces the plain photo once it has drawn.
   White paper behind the dashes: the gaps between them are see-through, so without it the flower
   showed through while this faded in, then blinked off underneath. */
.julia-pixels {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(393 * var(--u));
  height: calc(425 * var(--u));
  background: #fff;
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
}

.windows.is-julia-pixelated .julia-pixels {
  visibility: visible;
}

.windows.is-julia-pixelated .julia-art {
  visibility: hidden;
}

/* Figma's "Screen" layer, in place over the print's right edge. */
.photo-glare {
  top: calc(74.12 * var(--u));
  left: calc(221.73 * var(--u));
  width: calc(126.192 * var(--u));
  height: calc(114.706 * var(--u));
  max-width: none;
}

.window .window-bar,
.window .window-art,
.window .blank-flowers {
  transition: opacity 0s;
}

/* Going into a scene, the window's own chrome and art fade away over the picture. Whichever window
   sat nearest the middle takes the scene (windows.js marks it .is-scene-window). */
.windows.is-scene .is-scene-window :is(.window-bar, .window-art, .blank-flowers) {
  transition: opacity 0.22s ease;
}

/* While a scene is in, the other windows step back and nothing can be dragged. */
.window {
  transition: opacity 0.12s linear, scale 0.22s ease;
}

.windows.is-scene .window:not(.is-scene-window) {
  opacity: 0;
  scale: 0.96;
  pointer-events: none;
}

.windows.is-scene .window {
  cursor: default;
}

.windows.is-scene .is-scene-window {
  background-color: transparent;
}

.windows.is-vancouver .is-scene-window {
  box-shadow:
    0 0.5px 1px rgba(0, 0, 0, 0.2),
    0 4px 8px rgba(0, 0, 0, 0.08),
    0 14px 26px -8px rgba(0, 0, 0, 0.22),
    0 30px 44px -24px rgba(0, 0, 0, 0.3);
}

.windows.is-scene .is-scene-window :is(.window-bar, .window-art, .blank-flowers) {
  opacity: 0;
}

.windows.is-julia .julia-art,
.windows.is-julia .julia-pixels,
.windows.is-julia .photo-glare {
  opacity: 1;
}

/* The dashes sit on white photo paper. */
.windows.is-julia.is-julia-pixelated .is-scene-window {
  background-color: #fff;
}

/* "Vancouver, Canada": a printed postcard, tilted 5° as in Figma. The card is matte printed paper:
   a fine grain, a slightly muted print, a soft darkening toward the edges, a hairline paper edge,
   and barely rounded corners. Paper-cut trees come in with it,
   their trunks hanging off the bottom edge. */
.windows.is-vancouver .is-scene-window {
  rotate: 5deg;
  border-radius: calc(3 * var(--u));
}

.windows.is-vancouver:not(.is-morphing) .is-scene-window {
  cursor: crosshair;
}

.postcard-face {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
}

.windows.is-vancouver .postcard-face {
  opacity: 1;
}

.postcard-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  filter: saturate(0.9) contrast(1.03);
}

.postcard-face::before,
.postcard-face::after {
  content: "";
  position: absolute;
  z-index: 2;
  inset: 0;
  pointer-events: none;
}

.postcard-face::before {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.35  0 0 0 0 0.3  0 0 0 0 0.25  0 0 0 0.5 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px;
  mix-blend-mode: multiply;
  opacity: 0.35;
}

.postcard-face::after {
  border-radius: inherit;
  box-shadow:
    inset 0 0 0 calc(1 * var(--u)) rgba(255, 255, 255, 0.4),
    inset 0 0 calc(46 * var(--u)) rgba(60, 40, 20, 0.16);
}

/* Figma's marker doodles: 15.96% down and 6.1% in from the card's top-left. */
.postcard-drawing {
  position: absolute;
  top: calc(63.43 * var(--u));
  left: calc(35.84 * var(--u));
  width: calc(532.826 * var(--u));
  height: calc(323.834 * var(--u));
  max-width: none;
}

/* Visitors' own drawing, in card units so it stays put however the page is sized. */
.postcard-ink {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(587.5 * var(--u));
  height: calc(397.4 * var(--u));
}

.postcard-trees {
  position: absolute;
  z-index: 1000;
  /* Over the postcard, which is centred in the stage. */
  top: calc(50% - 198.7 * var(--u));
  left: calc(50% - 293.75 * var(--u));
  width: calc(587.5 * var(--u));
  height: calc(397.4 * var(--u));
  rotate: 5deg;
  pointer-events: none;
}

.postcard-tree {
  position: absolute;
  max-width: none;
  opacity: 0;
  transform: translateY(calc(-10 * var(--u))) scale(1.05);
  filter: drop-shadow(0 calc(1 * var(--u)) calc(1 * var(--u)) rgba(0, 0, 0, 0.25)) drop-shadow(0 calc(3 * var(--u)) calc(4 * var(--u)) rgba(0, 0, 0, 0.12));
  transition: opacity 0.1s linear, transform 0.25s ease;
}

/* A crop of the left tree from Figma's sheet of four. */
.tree-left {
  top: calc(253.5 * var(--u));
  left: calc(10.575 * var(--u));
  width: calc(138.47 * var(--u));
  height: calc(167.33 * var(--u));
  overflow: hidden;
}

.tree-left img {
  position: absolute;
  top: 0;
  left: -0.07%;
  width: 203.8%;
  height: 212.93%;
  max-width: none;
}

.tree-right {
  top: calc(212.98 * var(--u));
  left: calc(439.68 * var(--u));
  width: calc(136.856 * var(--u));
  height: calc(213.044 * var(--u));
}

.windows.is-vancouver .postcard-tree {
  opacity: 1;
  transform: none;
  transition: opacity 0.1s linear, transform 0.38s cubic-bezier(0.22, 1, 0.36, 1);
}

/* windows.js adds this for the length of the change, so drags never lag behind the pointer. */
.windows.is-morphing .is-scene-window {
  transition:
    --x 0.38s cubic-bezier(0.22, 1, 0.36, 1),
    --y 0.38s cubic-bezier(0.22, 1, 0.36, 1),
    --w 0.38s cubic-bezier(0.22, 1, 0.36, 1),
    --h 0.38s cubic-bezier(0.22, 1, 0.36, 1),
    rotate 0.38s cubic-bezier(0.22, 1, 0.36, 1),
    border-radius 0.38s ease,
    background-color 0.1s linear,
    box-shadow 0.38s ease;
}

/* Crossfade between scenes with nothing showing through: the incoming picture appears at once,
   fully solid, underneath; the outgoing one stays on top (windows.js marks it with from-*) and fades
   away over it. */
.windows.is-morphing .julia-art,
.windows.is-morphing .julia-pixels,
.windows.is-morphing .photo-glare,
.windows.is-morphing .postcard-face {
  transition: opacity 0.22s ease;
}

.windows.is-morphing.is-julia .julia-art,
.windows.is-morphing.is-julia .julia-pixels,
.windows.is-morphing.is-julia .photo-glare,
.windows.is-morphing.is-vancouver .postcard-face {
  transition: opacity 0s;
}

.windows.from-windows .is-scene-window .window-bar,
.windows.from-windows .is-scene-window .window-art,
.windows.from-windows .is-scene-window .blank-flowers,
.windows.from-julia .julia-art,
.windows.from-julia .julia-pixels,
.windows.from-julia .photo-glare,
.windows.from-vancouver .postcard-face {
  z-index: 4;
}

@media (prefers-reduced-motion: reduce) {
  .windows.is-morphing .is-scene-window,
  .windows.is-morphing .julia-art,
  .windows.is-morphing .julia-pixels,
  .windows.is-morphing .photo-glare,
  .windows.is-morphing .postcard-face,
  .postcard-tree,
  .windows.is-vancouver .postcard-tree,
  .window {
    transition: none;
  }
}

/* Toys inside the windows (window-toys.js). The flower's lens: the plain photo above the pixels,
   seen only through a soft circle that follows the pointer. */
.window-photo .photo-lens {
  z-index: 1;
  --lens: calc(64 * var(--u));
  -webkit-mask-image: radial-gradient(circle var(--lens) at var(--lx, -999px) var(--ly, -999px), #000 70%, transparent 100%);
  mask-image: radial-gradient(circle var(--lens) at var(--lx, -999px) var(--ly, -999px), #000 70%, transparent 100%);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.window-photo.is-lensing .photo-lens {
  opacity: 1;
}

/* It's a copy of the plain photo, which hides once the pixels draw; the lens stays visible. */
.window-photo.is-pixelated img.photo-lens {
  visibility: visible;
}

/* Only over the pixelated flower: as the print or postcard, the photo window has its own art. */
.windows.is-scene .photo-lens {
  display: none;
}

/* The lilac window's flowers draw on a canvas under its title bar. */
.blank-flowers {
  position: absolute;
  top: calc(30 * var(--u));
  left: 0;
  width: 100%;
  height: calc(100% - 30 * var(--u));
  pointer-events: none;
}

.rabbit-art {
  top: calc(30 * var(--u));
  left: calc(9 * var(--u));
  width: calc(266.836 * var(--u));
  height: calc(378.512 * var(--u));
}

.computer {
  --paper: #fff;
  --glass: #fbf6ea;
  --glass-warm: #fdecb4;
  --glass-off: #2e2619;
  position: relative;
  flex: 0 0 auto;
  width: clamp(300px, 37vw, 580px);
  color: #1d1916;
}

.computer svg {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  -webkit-tap-highlight-color: transparent;
}

/* Everything that turns when the computer turns to face the front, swinging round the stand. */
.computer .turner {
  position: relative;
  z-index: 1;
  transform-origin: 44.5% 50%;
}

/* The layers above the base: the screen (projected onto the glass by computer.js), the inked
   drawing, and the mouse on its cord. Only the screen and the mouse take the pointer. */
.computer .screen-layer {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  transform-origin: 0 0;
  color: #3b2913;
}

.computer .screen-layer .screen-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.computer .ink {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.computer .tether-svg {
  position: absolute;
  inset: 0;
  height: 100%;
  pointer-events: none;
  transition: opacity 0.4s ease;
}

.computer.is-booting .tether-svg {
  opacity: 0;
}

.computer .mouse-grip {
  pointer-events: auto;
}

.computer path,
.computer circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Solid paper behind each part, so whatever is drawn further back stays hidden. */
.computer .paper {
  fill: var(--paper);
  stroke: none;
  transition: fill 0.7s ease;
}

/* A second, lighter pass over every line, as if gone over again in pencil. */
.computer .retrace {
  fill: none !important;
  opacity: 0.5;
  stroke-width: 2.6;
}

.computer .faint {
  opacity: 0.55;
  stroke-width: 3;
}

.computer #screen-shape,
.computer #mini-heart {
  fill: inherit;
  stroke: inherit;
  stroke-width: inherit;
}

.computer .glass {
  fill: var(--glass);
  stroke: none;
  transition: fill 0.5s ease;
}

.computer.is-warm .glass {
  fill: var(--glass-warm);
}

.computer.is-off .glass {
  fill: var(--glass-off);
}

.computer.is-booting .glass {
  fill: var(--paper);
}

.computer .glass-edges {
  pointer-events: none;
}

.computer .screen-hit {
  fill: transparent;
  stroke: none;
  cursor: crosshair;
  touch-action: none;
}

.computer.is-off .screen {
  pointer-events: none;
}

.computer .ground {
  stroke: #4d9b63;
  pointer-events: none;
}

.computer .doodle {
  cursor: pointer;
  outline: none;
  transform-box: fill-box;
  transform-origin: center;
  transition: scale 0.2s cubic-bezier(0.23, 1, 0.32, 1);
}

@media (hover: hover) {
  .computer .doodle:hover {
    scale: 1.08;
  }
}

.computer .doodle:focus-visible {
  scale: 1.08;
}

.computer .sun-body,
.computer .sun-rays,
.computer .sun-eyes,
.computer .cloud-body,
.computer .heart-body {
  transform-box: fill-box;
  transform-origin: center;
}

.computer .sun-face {
  fill: #f6dc9b;
}

.computer .sun-rays path {
  stroke: #d9a443;
}

.computer .ink-fill {
  fill: currentColor;
  stroke: none;
}

.computer .flower,
.computer .flower-body {
  transform-box: fill-box;
  transform-origin: 50% 100%;
}

.computer .leaf {
  fill: #b9d49a;
}

.computer .petal {
  fill: #cfc6f0;
}

.computer .bloom {
  fill: #f6dc9b;
}

.computer .cloud-body .paper {
  fill: var(--glass);
  stroke: currentColor;
}

.computer .heart-fill {
  fill: #f5b5b5;
}

.computer .rain path {
  stroke: #7fa8d6;
}

.computer .float-heart {
  fill: #f5b5b5;
  stroke: currentColor;
  stroke-width: 1.6;
}

.computer .scribbles path {
  stroke-width: 5.5;
  pointer-events: none;
}

.computer .power {
  cursor: pointer;
  outline: none;
}

.computer .power-hit {
  fill: transparent;
  stroke: none;
}

.computer .power:focus-visible .power-hit {
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-dasharray: 3 3;
}

.computer .power-led {
  fill: #7cc68d;
  stroke: currentColor;
  stroke-width: 1.6;
  transition: fill 0.2s ease;
}

.computer.is-off .power-led,
.computer.is-booting .power-led {
  fill: var(--paper);
}

/* Pencil-crayon scenes: canvases that computer.js stacks behind and in front of the sketch. */
.computer .scene-stage {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  transition: opacity 0.45s ease;
}

.computer .scene-stage.is-front {
  z-index: 2;
}

.computer .scene-stage.is-leaving {
  opacity: 0;
}

.computer .scene-stage canvas {
  position: absolute;
}

/* What is drawn on the screen stays inside the glass, and tilts with it. */
.computer .screen-window {
  position: absolute;
  /* Above the screen's own svg, which every svg in .computer lifts to z-index 1. */
  z-index: 2;
  inset: 0;
  overflow: hidden;
  border-radius: 4.7% / 5%;
  pointer-events: none;
  transition: opacity 0.45s ease;
}

.computer .screen-window.is-leaving {
  opacity: 0;
}

.computer.is-off .screen-window {
  visibility: hidden;
}

.computer .screen-doodles {
  transition: opacity 0.3s ease;
}

.computer.is-greeting .screen-doodles {
  opacity: 0;
  pointer-events: none;
}

.computer.is-greeting .screen-hit {
  pointer-events: none;
}

@keyframes crayon-wave {
  from {
    rotate: -9deg;
  }
  to {
    rotate: 17deg;
  }
}

@keyframes crayon-sway {
  from {
    rotate: -6deg;
  }
  to {
    rotate: 6deg;
  }
}

@keyframes crayon-bob {
  from {
    translate: 0 -3%;
    rotate: -4deg;
  }
  to {
    translate: 0 3%;
    rotate: 4deg;
  }
}

@keyframes crayon-fly {
  from {
    translate: -60% 10%;
  }
  to {
    translate: 60% -10%;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .computer .layer-wave {
    animation: crayon-wave 0.42s ease-in-out infinite alternate;
  }

  .computer .layer-sway {
    animation: crayon-sway 2.8s ease-in-out infinite alternate;
  }

  .computer .layer-bob {
    animation: crayon-bob 1.9s ease-in-out infinite alternate;
  }

  .computer .layer-fly {
    animation: crayon-fly 9s ease-in-out infinite alternate;
  }
}

/* The mouse: its cord is a simulated rope, so it can be picked up and pulled around. */
.computer .tether .cord {
  stroke-width: 3.4;
  pointer-events: none;
}

.computer .mouse-grip {
  fill: transparent;
  stroke: none;
  cursor: grab;
  outline: none;
  touch-action: none;
}

.computer .mouse-grip:focus-visible {
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-dasharray: 4 4;
}

.computer.is-dragging,
.computer.is-dragging .mouse-grip {
  cursor: grabbing;
}

.disco .intro {
  color: inherit;
}

.disco .scene-word {
  background: rgba(255, 255, 255, 0.16);
  color: inherit;
}

.disco .computer {
  --paper: #000;
  --glass: #14110c;
  --glass-warm: #3a2c08;
  --glass-off: #000;
  color: rgba(255, 255, 255, 0.85);
}

/* The ink is drawn dark; in the dark it turns light. */
.disco .computer .ink,
.disco .computer .mouse-ink {
  filter: invert(1);
}

.disco .computer .screen-layer {
  color: rgba(255, 255, 255, 0.85);
}

@media (max-width: 640px) {
  .hero {
    flex-direction: column;
    align-items: flex-start;
    row-gap: clamp(40px, 6vw, 64px);
    padding-left: 0;
  }

  .intro {
    width: 100%;
    max-width: none;
  }

  .computer {
    align-self: center;
    width: min(380px, 84vw);
  }

  /* On narrow screens, the stage spans the column under the intro. */
  .windows {
    --u: calc(min(304px, 72vw) / 643);
    flex: none;
    align-self: stretch;
    width: calc(643 * var(--u));
    min-width: calc(643 * var(--u));
    max-width: none;
    /* Size the stage to the artwork instead of the full tablet width. */
    height: calc(620 * var(--u));
    aspect-ratio: auto;
  }
}

.work-list {
  display: flex;
  flex-direction: column;
  gap: clamp(64px, 7.3vw, 120px);
  /* Clear the fixed top-edge wash so the first project cover stays crisp. */
  margin-top: clamp(72px, 7vw, 112px);
}

@media (max-width: 720px) {
  .work-list {
    /* Below the fixed top-edge wash; tighter than desktop but past the opaque header band. */
    margin-top: 48px;
  }
}

.project[hidden] {
  display: none;
}

.project {
  display: flex;
  flex-direction: column;
  gap: clamp(20px, 2.2vw, 36px);
}

@media (min-width: 900px) {
  .work-list .project {
    gap: clamp(14px, 1.2vw, 22px);
  }
}

/* Artwork is laid out on the design's 1480 × 800 canvas; --u is one design pixel. */
/* --card: the project cards' height as a share of their 1480 x 800 canvas. Each cover scales its
   art by it, so everything stays in proportion, just smaller and centred. The grid keeps 1. */
/* Featured projects, grid tiles, case-study media — see --corner-ratio on :root. */
.media {
  container-type: inline-size;
  /* Covers grow with the screen, and stop before a monitor can stretch them down the page. */
  --cover-max: 440px;
  --card: 0.8;
  --u: min(100cqw / 1480, var(--cover-max) / (800 * var(--card)));
  --surface-radius: max(var(--corner-min), calc(var(--corner-ratio) * var(--u)));
}

.anthology {
  --card: 1;
}

/* Phones: the project covers stand taller (each cover's art scales with them). */
@media (max-width: 720px) {
  .media,
  .modal-flyer-size {
    --card: 1.05;
  }

  .anthology {
    --card: 1;
  }
}

/* One cover shape, the same on every screen: 1024 × 600. --cover-k is that height,
   so the artwork scales up to fill it. The flying copy uses it too, so its height
   follows its width and the open and close stay one smooth move. */
.media > .tile,
.modal-flyer-size > .tile {
  aspect-ratio: 1024 / 600;
  height: auto;
  --u: calc(100cqw / 1480);
  --cover-k: calc(100cqw * 600 / 1024);
  --surface-radius: max(var(--corner-min), calc(var(--corner-ratio) * var(--u)));
}

@media (min-width: 1440px) {
  .media,
  .modal-flyer-size {
    --cover-max: 520px;
  }
}

@media (min-width: 1800px) {
  .media,
  .modal-flyer-size {
    --cover-max: 600px;
  }
}

.media-split {
  display: flex;
  gap: 8px;
}

.media-split .tile {
  flex: 1 1 0;
}

.tile {
  position: relative;
  height: calc(800 * var(--u) * var(--card, 1));
  overflow: hidden;
  /* The clip path repeats the radius, because Safari can drop a rounded overflow clip around video or a moving layer. */
  border-radius: var(--surface-radius, max(var(--corner-min), calc(var(--corner-ratio) * var(--u))));
  clip-path: inset(0 round var(--surface-radius, max(var(--corner-min), calc(var(--corner-ratio) * var(--u)))));
  isolation: isolate;
}

.tile::after {
  content: "";
  position: absolute;
  z-index: 3;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
}

.tile > img {
  position: absolute;
  max-width: none;
  pointer-events: none;
}

.tile-bg {
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.tile-lilac,
.tile-weave {
  background: rgba(121, 49, 255, 0.1);
}

/* The grid's tints are a notch quieter than the featured projects'. */
:is(.anthology, .grid-stage) .tile-lilac {
  background: rgba(124, 104, 176, 0.08);
}

:is(.anthology, .grid-stage) .tile-trash {
  background: rgba(176, 164, 96, 0.08);
}

:is(.anthology, .grid-stage) .tile-bg,
:is(.anthology, .grid-stage) .trash-texture {
  filter: saturate(0.75);
}

/* Photo backdrops fade toward white to match the lighter tints. */
:is(.anthology, .grid-stage) .tile-trash .tile-bg {
  opacity: 0.75;
}

/* A fine film grain over each grid backdrop, under the phones. Grey noise in overlay
   keeps the average tone, so the grain adds texture without dimming the tint. */
:is(.anthology, .grid-stage) .tile::before {
  content: "";
  position: absolute;
  z-index: 1;
  inset: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='1 0 0 0 0  1 0 0 0 0  1 0 0 0 0  0 0 0 0 0.22'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px;
  mix-blend-mode: overlay;
  pointer-events: none;
}

/* Grid tiles sit on one flat grey (tutundzhian.com's #f7f7f7) with no backdrop art or grain.
   The doubled class outranks each tile's own tint. */
:is(.anthology, .grid-stage) .tile.tile {
  background: #f7f7f7;
}

:is(.anthology, .grid-stage) :is(.tile-bg, .trash-texture),
:is(.anthology, .grid-stage) .tile::before {
  display: none;
}

:is(.anthology, .grid-stage) .tile::after,
:is(.work-list, .modal-hero) .media > .tile::after {
  box-shadow: inset 0 0 0 1px var(--tile-inner-stroke);
}

.disco :is(.anthology, .grid-stage) .tile.tile {
  background: #1a1a1a;
}

.disco :is(.anthology, .grid-stage) .tile::after,
.disco :is(.work-list, .modal-hero) .media > .tile::after {
  box-shadow: inset 0 0 0 1px var(--tile-inner-stroke-disco);
}

/* Split tiles show their artwork as two cards. Each card holds a tile-wide copy of the art,
   offset so the halves line up across the gap; the modal closes the gap to join them. */
@property --split-gap {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}

@property --card-radius {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}

.tile-split {
  --split-gap: 8px;
  --card-radius: var(--surface-radius, max(var(--corner-min), calc(var(--corner-ratio) * var(--u))));
  display: flex;
  gap: var(--split-gap);
}

.tile-split::after {
  content: none;
}

.split-card {
  position: relative;
  flex: 1 1 0;
  overflow: hidden;
  border-radius: var(--card-radius);
  isolation: isolate;
}

.split-card::after {
  content: "";
  position: absolute;
  z-index: 3;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
}

.card-art {
  position: absolute;
  top: 0;
  left: var(--art-x, 0px);
  width: 100cqw;
  height: 100%;
}

.split-card + .split-card {
  --art-x: calc(-50cqw - var(--split-gap) / 2);
}

.modal-hero .tile-split {
  --split-gap: 0px;
  --card-radius: 0px;
}

.modal-hero .split-card::after {
  content: none;
}

/* Weave art is laid out on Figma's 1203 × 639 frame, scaled so its height fills the tile; --k is one frame pixel. */
/* The art spans the whole tile, split down the middle, so the panel on the left is always centred in
   its half whatever the tile's shape (it used to be a fixed 1203-wide frame, cropped at the sides). */
.weave-art {
  --k: calc(var(--cover-k) / 639);
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.wv-left,
.wv-right {
  position: absolute;
  top: 0;
  height: 100%;
  overflow: hidden;
}

.wv-left {
  left: 0;
  width: 50%;
  background: #bacce5;
  display: grid;
  place-items: center;
  container-type: inline-size;
}

.wv-right {
  left: 50%;
  right: 0;
  background: #90aec0;
}

/* The panel keeps its design size, but shrinks with everything in it when its half gets too narrow
   to hold it with a margin (86% of the half at most). */
.wv-modal {
  --k: min(calc(var(--cover-k) / 639), calc(86cqw / 285));
  width: calc(285 * var(--k));
  background: #fff;
  border-radius: calc(15.4 * var(--k));
  overflow: hidden;
  box-shadow: 0 0 calc(0.6 * var(--k)) rgba(0, 0, 0, 0.08), 0 calc(11.9 * var(--k)) calc(28.5 * var(--k)) rgba(0, 0, 0, 0.18), 0 calc(2.4 * var(--k)) calc(5.9 * var(--k)) rgba(0, 0, 0, 0.15);
}

.wv-modal > img {
  display: block;
  width: 100%;
  height: auto;
}

.wv-head {
  display: flex;
  align-items: center;
  height: calc(47.5 * var(--k));
  padding: 0 calc(9.5 * var(--k)) 0 calc(19 * var(--k));
  border-bottom: calc(1.19 * var(--k)) solid #e6e6e6;
  font-weight: 500;
  color: rgba(0, 0, 0, 0.9);
}

.wv-head span {
  flex: 1;
}

.wv-head img {
  width: calc(28.5 * var(--k));
  height: calc(28.5 * var(--k));
  margin-left: calc(4.75 * var(--k));
}

.wv-body {
  padding: calc(19 * var(--k));
  color: rgba(0, 0, 0, 0.5);
}

.wv-body p {
  margin: 0 0 calc(9.5 * var(--k));
}

.wv-icons {
  position: relative;
  width: calc(247 * var(--k));
  height: calc(247 * var(--k));
  margin-bottom: calc(19 * var(--k));
  border: calc(1.19 * var(--k)) solid rgba(0, 0, 0, 0.1);
  border-radius: calc(5.94 * var(--k));
  overflow: hidden;
}

.wv-icons img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.wv-text {
  height: calc(66.5 * var(--k));
  padding: calc(4.75 * var(--k)) calc(9.5 * var(--k));
  border-radius: calc(5.94 * var(--k));
  background: #f5f5f5;
  color: #000;
}

.wv-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: calc(9.5 * var(--k));
  padding: calc(9.5 * var(--k));
  border-top: calc(1.19 * var(--k)) solid #e6e6e6;
}

.wv-select,
.wv-button {
  display: inline-flex;
  align-items: center;
  height: calc(28.5 * var(--k));
  border-radius: calc(5.94 * var(--k));
}

.wv-select {
  padding-left: calc(9.5 * var(--k));
  background: #f5f5f5;
  color: rgba(0, 0, 0, 0.9);
}

.wv-select img {
  width: calc(19 * var(--k));
  height: calc(19 * var(--k));
  margin-right: calc(4.75 * var(--k));
}

.wv-button {
  padding: 0 calc(9.5 * var(--k));
  background: #0d99ff;
  color: #fff;
}

.wv-photo {
  position: absolute;
  left: calc(-193 * var(--k));
  /* Pinned to the bottom and stretched up, so the sky covers the panel's top edge. */
  bottom: 0;
  width: calc(965 * var(--k));
  height: calc(100% + 8 * var(--k));
  object-fit: cover;
  object-position: center bottom;
}

.wv-card {
  position: absolute;
  left: calc(-179 * var(--k));
  top: calc(112.5 * var(--k));
  width: calc(601 * var(--k));
  height: calc(415 * var(--k));
  background: #d1ecff;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: calc(33 * var(--k));
  overflow: hidden;
}

.wv-card img {
  position: absolute;
  left: calc(151 * var(--k));
  top: -1px;
  width: calc(451 * var(--k));
  height: calc(100% + 2px);
  object-fit: cover;
}

.tile-profile {
  background: rgba(203, 179, 0, 0.1);
}

.profile-bg {
  position: absolute;
  max-width: none;
  top: 50%;
  left: 50%;
  width: calc(800 * var(--u));
  height: calc(1480 * var(--u));
  object-fit: cover;
  opacity: 0.8;
  transform: translate(-50%, -50%) rotate(-90deg);
}

.tile-lockets .tile-bg {
  opacity: 0.5;
}

.tile-trash {
  background: rgba(203, 179, 0, 0.1);
}

.tile > .trash-texture {
  top: -97.59%;
  left: -0.03%;
  width: 100%;
  height: 258.1%;
  opacity: 0.25;
}

/* The frame's screen matches a real iPhone's 1179 × 2556, inside an even bezel. Video runs just under the bezel so no edge shows. */
.phone {
  position: absolute;
  z-index: 1;
  top: calc(50% + 132.77 * var(--u));
  left: calc(50% + var(--x) * var(--u));
  width: calc(414 * var(--u));
  aspect-ratio: 415 / 851;
  transform: translate(-50%, -50%);
  container-type: size;
}

.phone-screen {
  position: absolute;
  left: 4.096%;
  top: 1.645%;
  width: 91.807%;
  height: 96.71%;
  border-radius: 11.57cqw;
  background: #1d2129;
  object-fit: cover;
}

.phone-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: fill;
  pointer-events: none;
}

.stickers {
  position: absolute;
  z-index: 2;
  inset: 0;
  pointer-events: none;
}

.sticker {
  position: absolute;
  left: calc(var(--cx) * var(--u));
  top: calc(var(--cy) * var(--u));
  width: calc(var(--s) * 1.4 * var(--u));
  height: calc(var(--s) * 1.4 * var(--u));
  transform: translate(-50%, -50%) rotate(var(--r, 0deg));
}

.sticker img {
  position: absolute;
  max-width: none;
}

.sticker-cake,
.sticker-daisy {
  filter: drop-shadow(0 calc(6.768 * var(--u)) calc(3.384 * var(--u)) rgba(0, 0, 0, 0.25))
    drop-shadow(0 calc(8 * var(--u)) calc(16.92 * var(--u)) rgba(0, 0, 0, 0.1));
}

.sticker-heart {
  --cx: 838;
  --cy: 268;
  --s: 96;
  --r: 11.03deg;
  display: grid;
  place-items: center;
  font-size: calc(112 * var(--u));
  font-weight: 600;
  line-height: 1;
  color: transparent;
  background: linear-gradient(180.5deg, rgba(255, 255, 255, 0.609) 12.9%, rgba(255, 255, 255, 0) 45.4%),
    #ff46a0;
  -webkit-background-clip: text;
  background-clip: text;
  filter: drop-shadow(0 calc(6.768 * var(--u)) calc(3.384 * var(--u)) rgba(0, 0, 0, 0.25))
    drop-shadow(0 calc(8 * var(--u)) calc(16.92 * var(--u)) rgba(0, 0, 0, 0.1));
}

.sticker-clover {
  --cx: 646;
  --cy: 548;
  --s: 72;
  --r: -45deg;
}

.sticker-clover img {
  left: -60.33%;
  top: -49.22%;
  width: 220.66%;
  height: 220.66%;
}

.sticker-cake {
  --cx: 84;
  --cy: 556;
  --s: 96;
  --r: 24deg;
}

.sticker-cake img {
  left: 0;
  top: 0;
  width: 100%;
  height: 95%;
}

.sticker-daisy {
  --cx: 842;
  --cy: 560;
  --s: 96;
  --r: 9deg;
}

.sticker-daisy .daisy-petals {
  left: 1.119%;
  top: 0.118%;
  width: 97.843%;
  height: 99.248%;
}

.sticker-daisy .daisy-center {
  left: 28.777%;
  top: 27.767%;
  width: 42.541%;
  height: 42.541%;
}

.sticker-turkey {
  --cx: 1402;
  --cy: 400;
  --s: 96;
  --r: -9deg;
}

.sticker-turkey img {
  left: -50%;
  top: -38.18%;
  width: 200%;
  height: 200%;
}

.sticker-pumpkin {
  --cx: 78;
  --cy: 330;
  --s: 96;
}

.sticker-pumpkin img {
  left: -35.25%;
  top: -26.92%;
  width: 170.5%;
  height: 169.49%;
}

.sticker-gift {
  --cx: 640;
  --cy: 262;
  --s: 96;
  --r: -16deg;
}

.sticker-gift img {
  left: -32.75%;
  top: -26.92%;
  width: 165.5%;
  height: 170.5%;
}

.anthology {
  --row-h: calc(100cqw * 0.5);
  display: grid;
  /* Ten columns: tiles take 4 or 6 (the 2 : 3 rhythm), and a pair can split a row 5 + 5. */
  grid-template-columns: repeat(10, minmax(0, 1fr));
  /* On a 4px grid: rows 32 apart (40 on wide screens, 24 on phones), tiles 16 apart side by side. */
  gap: 32px 16px;
}

/* Company, then a short line, under each tile. */
.grid-caption {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 16px;
}

@media (min-width: 1280px) {
  .anthology {
    row-gap: 40px;
  }

  .grid-caption {
    margin-top: 16px;
  }
}

@media (max-width: 720px) {
  .anthology {
    row-gap: 24px;
  }
}

.grid-caption .title,
.grid-caption .description {
  white-space: nowrap;
}

.grid-caption .description {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Captions: the company's small app icon (none for personal work), the project name in black, and
   its year right after it in grey. */
.caption-logo {
  flex: none;
  /* 8px to the project name (margin + flex gap), 4px between name and year. */
  margin-right: 4px;
  /* 16px, and never smaller than the size they already grew to on a wide screen. */
  width: max(16px, clamp(14px, 1.15vw, 17px));
  height: max(16px, clamp(14px, 1.15vw, 17px));
  aspect-ratio: 1;
  border-radius: 22.5%;
  object-fit: contain;
}

.grid-caption .description {
  color: #000;
  font-weight: 500;
}

.grid-caption .year {
  flex: none;
  margin: 0;
  color: rgba(0, 0, 0, 0.5);
  font-size: var(--text-body);
  letter-spacing: var(--track-body);
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
}

.disco .grid-caption .description {
  color: #fff;
}

.disco .grid-caption .year {
  color: rgba(255, 255, 255, 0.6);
}

.anthology .tile {
  --u: calc(var(--row-h) / 800);
  --surface-radius: max(var(--corner-min), calc(var(--corner-ratio) * var(--u)));
}

.grid-item {
  grid-column: span 4;
  min-width: 0;
}

/* Wide and closing tiles only on the multi-column grid; on phones a span would add columns. */
@media (min-width: 561px) {
  /* filters.js sets these for whichever tiles are showing: the 3 + 2 / 2 + 3 rhythm, a lone last
     tile across the row, pairs in equal halves, and full-width tiles. */
  .anthology .grid-item.is-wide {
    grid-column: span 6;
  }

  .anthology .grid-item.is-half {
    grid-column: span 5;
  }

  .anthology .grid-item.is-last,
  .anthology .grid-item.is-full {
    grid-column: 1 / -1;
  }
}

.grid-item[hidden] {
  display: none;
}



/* Small phones get one tile per row, tall enough that the prototypes read at full size. */
@media (max-width: 560px) {
  .anthology {
    --row-h: calc(100cqw * 1.15);
    grid-template-columns: minmax(0, 1fr);
  }

  .grid-item {
    grid-column: auto;
  }
}

.anthology .phone {
  pointer-events: none;
}

/* Every grid phone is the same size, whether a tile holds one or two: --u is shared by the whole row. */
.tile-lockets .phone {
  top: 50%;
}

.tile-lockets .phone,
.tile-trash .phone,
.tile-backup .phone,
.tile-carbon .phone,
.tile-claim .phone,
.tile-customize .phone,
.tile-receive .phone,
.tile-existing .phone,
.tile-multi .phone,
.tile-onboarding .phone {
  width: calc(300 * var(--u));
}

/* A one-column grid on small phones is only ~700 units wide, too narrow for two 300-unit phones
   and their gap, so paired phones shrink a little there and close in to fit. */
@media (max-width: 560px) {
  :is(.anthology, .grid-stage) :is(.tile-lockets, .tile-trash) .phone {
    width: calc(270 * var(--u));
    left: calc(50% + var(--x) * 0.86 * var(--u));
  }
}

.tile-trash .phone,
.tile-backup .phone,
.tile-carbon .phone,
.tile-claim .phone,
.tile-customize .phone,
.tile-receive .phone,
.tile-existing .phone,
.tile-multi .phone,
.tile-onboarding .phone {
  top: 50%;
}

.tile-onboarding {
  background: radial-gradient(120% 110% at 15% 15%, #f7eff1, transparent 60%), #f1f1f5;
}

.tile-multi {
  background: radial-gradient(120% 110% at 80% 10%, #f1f1f5, transparent 60%), #f0f3f0;
}

.tile-existing {
  background: radial-gradient(120% 110% at 15% 15%, #f0f1f5, transparent 60%), #f6f2e9;
}

.tile-receive {
  background: radial-gradient(120% 110% at 85% 90%, #f2eef5, transparent 60%), #eef2f5;
}

.tile-customize {
  background: radial-gradient(120% 110% at 15% 90%, #f5eee3, transparent 60%), #f6f2eb;
}

.tile-claim {
  background: radial-gradient(120% 110% at 85% 10%, #f1f0f5, transparent 60%), #f6f0eb;
}

.tile-carbon {
  background: radial-gradient(120% 100% at 80% 5%, #f0f1f5, transparent 60%), #eff3eb;
}

/* Family prototypes carry the Family mark in the corner. */
.tile > .tile-logo {
  z-index: 2;
  top: calc(32 * var(--u));
  left: calc(32 * var(--u));
  width: calc(28 * var(--u));
  height: calc(28 * var(--u));
}

/* The grid stays clean: no corner marks. */
:is(.anthology, .grid-stage) .tile > .tile-logo {
  display: none;
}

/* The recording has a baked-in backdrop, so it's cut to the iPad's own outline. */
.tile-brush {
  --ipad-w: 780;
  background: radial-gradient(120% 110% at 15% 10%, #f0f2ef, transparent 60%),
    radial-gradient(110% 110% at 90% 95%, #f5ece6, transparent 60%), #f3f1e8;
}

.brush-video {
  position: absolute;
  z-index: 2;
  top: 50%;
  left: 50%;
  width: calc(var(--ipad-w) / 0.85 * var(--u));
  aspect-ratio: 2552 / 1890;
  clip-path: inset(8.7% 7.6% 8.7% 7.4% round 3.1% / 4.2%);
  transform: translate(-50%, -50%);
}

/* Drawing tools: the iPad up close, its left side (colours and tools) in view. It's ~1.25x the emoji
   brush's iPad, its left edge ~160 in and its bottom ~125 up, so the top just runs off the tile
   behind a soft fade of the tile's grey (no hard cut-off line). */
.tile-brush[data-id="drawing-tools"] .brush-video {
  top: calc(-100 * var(--u));
  left: calc(70 * var(--u));
  width: calc(1150 * var(--u));
  transform: none;
}

.tile-fade-top {
  position: absolute;
  z-index: 3;
  top: 0;
  right: 0;
  left: 0;
  height: calc(170 * var(--u));
  pointer-events: none;
  background: linear-gradient(#f0f0ee 15%, rgba(240, 240, 238, 0));
}

@media (max-width: 720px) {
  .tile-brush {
    --ipad-w: 550;
  }
}

/* Company logo, then the title with a one-line description underneath. */
.details {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* Official app icons, clipped to the iOS corner radius. */
.logo {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 22.5%;
}

.logo img {
  display: block;
  width: 100%;
  height: 100%;
}

@media (max-width: 720px) {
  .details .logo {
    display: none;
  }
}

.details-text {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
}

.details .description {
  line-height: 1.5;
}

.heading {
  min-width: 0;
}

.title,
.description {
  margin: 0;
  font-size: var(--text-body);
  letter-spacing: var(--track-body);
  line-height: var(--leading-body);
}

.title {
  font-weight: 500;
}

.meta {
  margin: 0;
  font-size: var(--text-body);
  font-weight: 400;
  letter-spacing: var(--track-body);
  line-height: var(--leading-body);
  color: rgba(0, 0, 0, 0.5);
}

.description {
  font-weight: 400;
  color: rgba(0, 0, 0, 0.5);
  text-wrap: pretty;
}

.section-title {
  display: flex;
  align-items: center;
  gap: 0.4em;
  margin: 0;
  font-size: var(--text-display);
  font-weight: 500;
  letter-spacing: var(--track-display);
  line-height: 1.2;
}

.section-title-plain {
  display: block;
}

/* The rest of the title shades from sky blue into deep teal. */
.section-tint {
  background: linear-gradient(90deg, #79bfe0, #2f6e8f 45%, #1d3f57);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* A hand-drawn squiggle between the featured projects and the grid, drawn by separator.js. */
.separator {
  width: 100%;
  height: 14px;
}

.separator-line {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.separator-line path {
  fill: none;
  stroke: rgba(0, 0, 0, 0.16);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.disco .separator-line path {
  stroke: rgba(255, 255, 255, 0.2);
}

.section-mark {
  flex: 0 0 auto;
  width: 0.8em;
  aspect-ratio: 32 / 35;
  background: currentColor;
  -webkit-mask: url("assets/selected-work.svg") center / contain no-repeat;
  mask: url("assets/selected-work.svg") center / contain no-repeat;
}

/* Disco mode: hanging mirror ball, wall lights, and a dark theme. */
.disco-ball {
  --size: clamp(52px, 6.75vw, 104px);
  --drop: calc(clamp(24px, 2.7vw, 44px) + clamp(48px, 5.62vw, 92px) / 2 - var(--size) / 2);
  position: fixed;
  z-index: 60;
  top: var(--drop);
  left: calc(50% - var(--size) / 2);
  display: none;
  width: var(--size);
  height: var(--size);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  transform: translateY(calc(-100% - var(--drop) - 12px));
  transition: transform 0.8s cubic-bezier(0.55, 0, 0.8, 0.4);
  -webkit-tap-highlight-color: transparent;
}

.disco-ball.is-ready {
  display: block;
}

.disco-ball::before {
  content: "";
  position: absolute;
  bottom: calc(100% - 2px);
  left: calc(50% - 1px);
  width: 2px;
  height: 100vh;
  background: linear-gradient(to right, #6d6d6d, #e4e4e4 45%, #5a5a5a);
}

.disco-ball::after {
  content: "";
  position: absolute;
  top: -4px;
  left: calc(50% - 7px);
  width: 14px;
  height: 8px;
  border-radius: 3px 3px 1px 1px;
  background: linear-gradient(to right, #4c4c4c, #dcdcdc 42%, #3c3c3c);
}

.disco-ball.no-webgl {
  background: radial-gradient(circle at 35% 30%, #fff, #8d8d8d 45%, #1c1c1c);
}

.disco-ball canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.disco-ball:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 6px;
}

.disco .disco-ball {
  transform: none;
  transition: transform 1.3s cubic-bezier(0.22, 1.14, 0.36, 1);
}

.disco-lights {
  position: fixed;
  z-index: 65;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  mix-blend-mode: screen;
  pointer-events: none;
  transition: opacity 0.4s ease;
}

.disco .disco-lights {
  opacity: 1;
  transition: opacity 0.9s ease 0.55s;
}

@media (prefers-reduced-motion: reduce) {
  .disco-ball,
  .disco .disco-ball,
  .disco-lights,
  .disco .disco-lights {
    transition: none;
  }
}

html.disco,
.disco body {
  background: #000;
}

.disco body {
  color: #fff;
}

.disco .description,
.disco .meta {
  color: rgba(255, 255, 255, 0.6);
}

.disco .site-nav a:focus-visible,
.disco .nav-toggle:focus-visible,
.disco .wordmark:focus-visible {
  outline-color: #fff;
}

/* Project modal: the tile grows into the modal header. */
.project-link {
  position: relative;
}

.project-open {
  position: absolute;
  z-index: 4;
  inset: 0;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: var(--surface-radius, max(var(--corner-min), calc(var(--corner-ratio) * var(--u))));
  background: transparent;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  outline: none;
}

.project-open:focus-visible {
  outline: none;
}

.project-modal {
  position: fixed;
  inset: 0;
  /* 100vw, not 100%: a fixed layer sized to the layout viewport is already inside the
     root scrollbar gutter, and the project's own scrollbar then steals another strip
     and slides the column left of the work and info pages. */
  width: 100vw;
  max-width: 100vw;
  height: auto;
  max-height: none;
  margin: 0;
  /* A project opens as a page of its own, filling the window (like mant.design). Its picture and
     text keep the home page's margins, so the picture lands exactly where its card was. */
  --page-gutter: var(--gutter);
  /* Over the site header (100): a project page takes the whole window, its X where the header was. */
  z-index: 110;
  padding: 0;
  overflow: hidden;
  border: 0;
  background: transparent;
  color: #000;
}

  .project-modal[open],
  .project-modal.is-dismissing {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    /* iOS samples the top-layer dialog; carry the live bottom edge colour here too. */
    background-color: var(--edge-bottom, #fff);
    background-image: linear-gradient(#fff calc(100% - 2px), transparent 0);
  }

.disco .project-modal[open],
.disco .project-modal.is-dismissing {
  background-color: var(--edge-bottom, #141414);
  background-image: linear-gradient(#141414 calc(100% - 2px), transparent 0);
}

/* While the cover is on its way back, the dialog itself can't stay solid or the
   home page only appears once the cover has already landed. */
.project-modal.is-dismissing,
.disco .project-modal.is-dismissing {
  background-color: transparent;
  background-image: none;
}

.project-modal::backdrop {
  background: transparent;
}

/* Sit above the work header while the white sheet fades, so the name and nav emerge through it. */
.project-modal.is-dismissing {
  z-index: 89;
  pointer-events: none;
}

.modal-backdrop {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: rgba(190, 190, 188, 0.6);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

/* Phones and touch screens: no blur behind the project page or its close button. Re-blurring the
   whole page on every frame of the open and close fade dropped frames there, so the card jumped
   instead of growing. A nearly solid tint looks close and costs nothing to fade. */
@media (max-width: 720px), (hover: none) {
  .modal-backdrop {
    background: rgba(226, 226, 224, 0.94);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  /* With no scroll lock on phones (it made iOS resize the viewport mid-animation), swipes that
     start outside the project's own scrolling page don't move anything, and its scrolling never
     carries on into the page behind. */
  .project-modal.is-lite {
    touch-action: none;
  }

  .project-modal.is-lite .modal-scroll {
    touch-action: pan-y;
  }
}



/* The project page is opaque, so nothing behind it needs tinting or blurring. */
.project-modal .modal-backdrop {
  display: none;
}

.modal-shell {
  position: relative;
  width: 100%;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  --modal-radius: 0px;
  /* Same stack as body: Safari reads background-color for its toolbar; the white wash covers the
     sheet so only the bottom edge carries the live sky colour (safari-edges.js). */
  background-color: var(--edge-bottom, #fff);
  background-image: linear-gradient(#fff calc(100% - 2px), transparent 0);
  transform-origin: 0 0;
}

/* A white wash under the back button, like the home page's header, so text scrolls away under it. */
.modal-shell::after {
  content: "";
  position: absolute;
  z-index: 1;
  top: 0;
  right: 0;
  left: 0;
  height: calc(var(--header-h, 0px) + 16px);
  pointer-events: none;
  background: linear-gradient(#fff 70%, rgba(255, 255, 255, 0));
}

.disco .modal-shell {
  background-color: var(--edge-bottom, #141414);
  background-image: linear-gradient(#141414 calc(100% - 2px), transparent 0);
}

.disco .modal-shell::after {
  background: linear-gradient(#141414 70%, rgba(20, 20, 20, 0));
}

.project-modal.is-animating .modal-shell,
.project-modal.is-animating .modal-body {
  will-change: transform, opacity, filter;
}

.modal-scroll {
  flex: 1;
  min-height: 0;
  width: 100%;
  /* The reserved strip sits where the root scrollbar already is, so the column
     keeps the page's margins while this scroller can still show its own bar. */
  scrollbar-gutter: stable;
  /* clip, not hidden: hidden would also crop the sky where it runs past the text column to the
     screen edges, the same way the work page only clips overflow past the viewport. */
  overflow-x: clip;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  outline: none;
  overscroll-behavior: contain;
  padding: calc(var(--header-h, 0px) + 8px) var(--page-gutter) 0;
  /* Same canvas treatment as body: live sky colour under a white wash (safari-edges.js). */
  background-color: var(--edge-bottom, #fff);
  background-image: linear-gradient(#fff calc(100% - 2px), transparent 0);
}

.disco .modal-scroll {
  background-color: var(--edge-bottom, #141414);
  background-image: linear-gradient(#141414 calc(100% - 2px), transparent 0);
}

.modal-hero,
.modal-body {
  max-width: calc(1720px - 2 * var(--page-gutter));
  margin-inline: auto;
}

.modal-hero .media {
  container-type: inline-size;
  --u: min(100cqw / 1480, var(--cover-max, 400px) / (800 * var(--card, 0.8)));
  --surface-radius: max(var(--corner-min), calc(var(--corner-ratio) * var(--u)));
}

.modal-hero {
  position: relative;
  z-index: 1;
}

.modal-hero.is-cover {
  position: absolute;
  z-index: 3;
  top: calc(var(--header-h, 0px) + 8px);
  right: var(--page-gutter);
  left: var(--page-gutter);
  margin-inline: auto;
}

/* The grid viewer's close button: the same bare X, top right, its glyph lined up with the page's
   right margin (the button's padding hangs past it, as in the viewer). */
.modal-close {
  position: absolute;
  z-index: 2;
  /* Just above the cover: modal-scroll starts at --header-h + 8px. */
  top: calc(var(--header-h, 48px) - 44px);
  right: max(var(--page-gutter), (100% - 1720px) / 2 + var(--page-gutter));
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-right: -15px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: #111;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity 0.15s ease, transform 0.25s cubic-bezier(0.34, 1.15, 0.64, 1);
}

/* On web the X sits just above the cover's top-right corner, on the picture's own margin. */
@media (min-width: 721px) {
  .modal-close {
    top: calc(var(--project-top-blur, 96px) + 12px - 44px - 8px);
    right: max(var(--gutter), (100% - 1720px) / 2 + var(--gutter));
  }

  /* Docked inside .top-edge (project-modal.js): same layer as the white wash, painted after
     the blur spans so Safari's backdrop-filter can't cover it when the project scrolls. */
  html.is-project-open .top-edge .modal-close.is-docked {
    position: absolute;
    z-index: 2;
    display: flex;
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    top: calc(var(--header-mid, 40px) - 22px);
    right: max(var(--gutter), (100vw - 1720px) / 2 + var(--gutter));
    margin-right: -15px;
  }

  html.is-project-open.is-project-closing .top-edge .modal-close.is-docked {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: none;
  }
}

/* The name leaves ahead of the page, so the header fades out a little sooner than the
   rest of the home page. On a phone it sits above the project, so this fade is what hides it. */
.site-header .wordmark,
.site-header .nav-menu {
  transition: opacity 0.2s cubic-bezier(0.23, 1, 0.32, 1);
}

.is-project-open:not(.is-project-closing) .site-header .wordmark,
.is-project-open:not(.is-project-closing) .site-header .nav-menu {
  transition: none;
  opacity: 0;
  filter: blur(2px);
  transform: scale(0.995);
  pointer-events: none;
}

/* Above the top blur, so the name fades in with the page instead of waiting under
   that band until the project is gone. The bar itself stays clear; only the text returns. */
html.is-project-open.is-project-closing .site-header {
  z-index: 130;
  pointer-events: none;
}

html.is-project-open.is-project-closing .site-header::before {
  opacity: 0;
}

html.is-project-open.is-project-closing .site-header .wordmark,
html.is-project-open.is-project-closing .site-header .nav-menu {
  /* The return animation fades these in with the rest of the page. A transition here
     starts in the same frame as the opacity change and the text just pops on. */
  transition: none;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .site-header .wordmark,
  .site-header .nav-menu {
    transition-duration: 0.15s;
  }

  .is-project-open:not(.is-project-closing) .site-header .wordmark,
  .is-project-open:not(.is-project-closing) .site-header .nav-menu {
    filter: none;
    transform: none;
  }
}

@media (max-width: 720px) {
  html.is-project-open.is-project-closing .site-header {
    z-index: 130;
  }
}

/* The same two menu strokes become the project X on phones. */
@media (max-width: 720px) {
  .is-project-open .site-header { z-index: 120; }
  .is-project-open:not(.is-project-closing) .site-header::before { background: transparent; }
  /* is-project-closing drops the X the moment dismiss starts, so the strokes fold back
     while the page is still leaving instead of waiting for that exit to finish. */
  .is-project-open:not(.is-project-closing) .nav-toggle span:first-child {
    transform: translateY(4.5px) rotate(45deg);
  }
  .is-project-open:not(.is-project-closing) .nav-toggle span:last-child {
    transform: translateY(-4.5px) rotate(-45deg);
  }
  .is-project-open .project-modal .modal-close,
  .is-project-open .modal-close.is-docked { display: none; }
}

.modal-close:active {
  transform: scale(0.94);
  transition: opacity 0.15s ease, transform 0.08s ease-out;
}

@media (hover: hover) {
  .modal-close:hover {
    opacity: 0.55;
  }
}

.disco .modal-close {
  color: #fff;
}

.modal-close:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

.modal-body {
  container-type: inline-size;
  --u: calc(100cqw / 1480);
  --surface-radius: max(var(--corner-min), calc(var(--corner-ratio) * var(--u)));
  --modal-gap: 8px;
  --modal-block: clamp(40px, 5vw, 56px);
  --modal-muted: rgba(0, 0, 0, 0.55);
  position: relative;
  isolation: isolate;
  padding: clamp(24px, 4vw, 40px) 0 0;
  font-size: var(--text-body);
  letter-spacing: var(--track-body);
  line-height: var(--leading-body);
}

.modal-body::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  background: #fff;
}

/* Phones: the frozen copy of the picture that travels between the card and the modal
   (project-modal.js). Fixed to the screen and laid out on the same 1480 x 800 canvas as the card. */
.modal-flyer {
  position: fixed;
  z-index: 10;
  pointer-events: none;
  /* It slides by transform, which the GPU runs; it never scales (see .modal-flyer-size). */
  will-change: transform;
}

/* On the way in, the copy lives in the cover so a scroll takes it with the page.
   A fixed copy held still for the whole flight and felt stuck. */
.modal-flyer.is-in-hero {
  position: absolute;
  z-index: 2;
  left: 0;
  top: 0;
}

/* The flyer's real width, eased between the card's and the modal's, so the picture lays itself out
   at each size instead of being squeezed. */
.modal-flyer-size {
  container-type: inline-size;
  --cover-max: 440px;
  --u: min(100cqw / 1480, var(--cover-max) / (800 * var(--card)));
  --card: 0.8;
}

@media (min-width: 1440px) {
  .modal-flyer-size {
    --cover-max: 520px;
  }
}

@media (min-width: 1800px) {
  .modal-flyer-size {
    --cover-max: 600px;
  }
}

/* Phones: as tall as the covers it travels between (see .media). */
@media (max-width: 720px) {
  .modal-flyer-size {
    --card: 1.05;
  }
}

/* The first frame of the body fade, held while the browser starts the animation. */
.project-modal.is-entering:not(.is-lite) .modal-body::before {
  opacity: 0;
}

.modal-header {
  position: relative;
  transform-origin: 0 0;
}

/* Three sizes only: the title, body text and small captions. */
.modal-body .title {
  font-size: var(--text-display);
  letter-spacing: var(--track-display);
}

.modal-body .meta {
  color: var(--modal-muted);
  font-size: var(--text-body);
}

.modal-body p,
.modal-body h3 {
  max-width: 36em;
}

/* Intro: a short description beside the project facts. */
.modal-intro {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
  gap: clamp(24px, 5vw, 64px);
  margin-top: clamp(20px, 2.4vw, 28px);
}

.modal-summary p {
  margin: 0;
  font-size: var(--text-body);
  letter-spacing: var(--track-body);
  line-height: var(--leading-body);
  text-wrap: pretty;
}

.modal-summary p + p {
  margin-top: 1.25em;
}

.modal-facts {
  display: flex;
  flex-direction: column;
  gap: 20px;
  margin: 0;
}

.modal-facts dt {
  color: var(--modal-muted);
  font-size: 14px;
  line-height: 1.4;
}

.modal-facts dd {
  margin: 2px 0 0;
  font-size: var(--text-body);
  letter-spacing: var(--track-body);
  line-height: 1.4;
}

.modal-body section {
  margin-top: var(--modal-block);
}

.modal-body h3 {
  margin: 0 0 4px;
  font-size: 16px;
  font-weight: 500;
}

.modal-body section p {
  margin: 0;
  color: var(--modal-muted);
}

.modal-body section p + p {
  margin-top: 1.25em;
}

.modal-body figcaption {
  margin: 8px 0 0;
  color: var(--modal-muted);
  font-size: 14px;
  line-height: 1.4;
}

.modal-body figcaption strong {
  color: #000;
  font-weight: 500;
}

.modal-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--modal-gap);
  margin: 32px 0 0;
}

.modal-stats dt {
  font-size: clamp(22px, 2.2vw, 28px);
  font-weight: 500;
  line-height: 1.1;
}

.modal-stats dd {
  margin: 6px 0 0;
  padding-right: 24px;
  color: var(--modal-muted);
  font-size: 14px;
  line-height: 1.4;
}

.modal-body img {
  display: block;
  width: 100%;
  height: auto;
}

.modal-body figure {
  margin: 0;
}

/* Every image block shares one gutter and starts a fixed distance from the text above. */
.modal-body .modal-bleed,
.modal-recipe,
.modal-stage {
  margin-top: var(--modal-block);
}

.modal-bento,
.modal-reframe,
.modal-pair,
.modal-strip {
  margin-top: 20px;
}

.modal-body .modal-split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--modal-gap);
  align-items: center;
}

/* Text beside an image keeps to the same two columns as the image pairs, with room before the image. */
.modal-split > div:not(.month-art) {
  padding-right: clamp(12px, 3vw, 36px);
}

.modal-recipe,
.modal-bento,
.modal-pair,
.modal-strip {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--modal-gap);
}

.modal-body .modal-recipe img {
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.modal-pair {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.modal-reframe > div {
  display: flex;
  gap: var(--modal-gap);
}

.modal-reframe img:first-child {
  flex: 800 1 0;
  min-width: 0;
  aspect-ratio: 800 / 998;
}

.modal-reframe img:last-child {
  flex: 2509 1 0;
  min-width: 0;
  aspect-ratio: 1400 / 781;
}

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

.stage-panel {
  display: flex;
  justify-content: center;
  padding: clamp(24px, 5vw, 56px) 0;
  overflow: hidden;
  border-radius: var(--surface-radius);
  background-position: center 62%;
  background-size: cover;
}

.stage-cream {
  background-image: url("assets/calendar-cream.jpg");
}

.stage-sky {
  background-image: url("assets/calendar-sky.jpg");
}

.modal-stage .phone {
  position: relative;
  top: auto;
  left: auto;
  width: min(62%, 260px);
  transform: none;
}

.modal-body .modal-stage .phone-screen {
  position: absolute;
  width: 91.807%;
  height: 96.71%;
  border-radius: 11.57cqw;
}

.modal-body .modal-stage .phone-frame {
  position: absolute;
  height: 100%;
  border-radius: 0;
}

.month-art {
  position: relative;
}

.month-sticker {
  position: absolute;
  width: 34% !important;
  pointer-events: none;
}

.month-sticker-gift {
  top: -12%;
  right: -6%;
  rotate: 14deg;
}

.month-sticker-pumpkin {
  bottom: -14%;
  left: -8%;
  rotate: -12deg;
}

.modal-credit {
  margin: 20px 0 0;
  color: var(--modal-muted);
  font-size: 14px;
}

.modal-credit a {
  color: inherit;
}

.modal-body .modal-feature {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 160px;
  max-width: none;
  margin-top: var(--modal-block);
  overflow: hidden;
  border-radius: var(--surface-radius);
  background: #dccfe9;
  color: #000;
}

.modal-feature img {
  position: absolute;
  top: 0;
  right: 0;
  width: 62%;
  height: 100%;
  border-radius: 0;
  object-fit: cover;
  object-position: 50% 68%;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 40%);
  mask-image: linear-gradient(to right, transparent, #000 40%);
}

.modal-feature div {
  position: relative;
  max-width: 24em;
  padding: clamp(24px, 4vw, 40px);
}

.modal-body .modal-feature p {
  color: rgba(0, 0, 0, 0.6);
}

@media (max-width: 720px) {
  .modal-intro {
    grid-template-columns: minmax(0, 1fr);
  }

  .modal-facts {
    flex-direction: row;
    gap: 32px;
  }

  .modal-body .modal-split,
  .modal-bento,
  .modal-pair {
    grid-template-columns: minmax(0, 1fr);
  }

  .modal-recipe {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .recipe-result {
    grid-column: 1 / -1;
  }

  .modal-body .modal-feature {
    display: block;
    min-height: 0;
  }

  .modal-feature img {
    position: static;
    width: 100%;
    height: 200px;
    -webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent);
    mask-image: linear-gradient(to bottom, #000 60%, transparent);
  }
}

.disco .project-modal {
  color: #fff;
}

.disco .modal-backdrop {
  background: rgba(0, 0, 0, 0.6);
}

.disco .modal-body::before {
  background: #141414;
}

.disco .modal-body {
  --modal-muted: rgba(255, 255, 255, 0.6);
}

.disco .modal-body figcaption strong {
  color: #fff;
}

.disco .modal-body .modal-feature p {
  color: rgba(0, 0, 0, 0.6);
}

/* Grid viewer: a tile from the bottom row grows to the centre of the screen. */
.anthology .tile {
  cursor: zoom-in;
  /* No double-tap page zoom on the tiles that open the viewer (iOS Safari). */
  touch-action: manipulation;
}

/* One tile per row on small phones; enlarging still opens the full-screen viewer + chevrons. */
.anthology.is-single-column .tile {
  cursor: zoom-in;
}

.anthology .tile:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: -4px;
}

/* Escape closes the grid and the browser focuses the tile again. Hold the ring off
   until the next real Tab or click. */
html.grid-just-closed .anthology .tile:focus,
html.grid-just-closed .anthology .tile:focus-visible {
  outline: none !important;
}

:is(.grid-viewer, .case-phone-viewer) {
  --stage-w: calc(100vw - 64px);
  --stage-h: min(calc(100dvh - 152px), 880px);
  position: fixed;
  inset: 0;
  z-index: 120;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: 0;
  overflow: hidden;
  border: 0;
  background: transparent;
  color: #111;
}

/* The enlarged tile's video player: the frosted pill from the case-study films, inside the stage
   so the clip keeps it on the tile and it leaves when the tile does. */
.grid-player {
  position: absolute;
  z-index: 4;
  bottom: var(--player-bar-inset);
  left: calc(var(--clip-x, 0px) + var(--player-bar-inset));
  right: calc(var(--clip-x, 0px) + var(--player-bar-inset));
  display: flex;
  align-items: center;
  box-sizing: border-box;
  height: var(--player-bar-height);
  padding: var(--player-bar-padding);
  border-radius: var(--player-chrome-radius);
  background: rgba(255, 255, 255, 0.62);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
  backdrop-filter: blur(16px) saturate(1.4);
  transition: opacity 0.25s ease;
}

:is(.grid-viewer, .case-phone-viewer).is-closing .grid-player {
  transition: none;
}

:is(.grid-viewer, .case-phone-viewer).is-closing .grid-control {
  transform: none;
  outline: none;
}

/* The bar stays out of the way until the pointer is over the tile. */
@media (hover: hover) and (pointer: fine) {
  .grid-player.is-resting {
    opacity: 0;
    pointer-events: none;
  }

  :is(.grid-viewer, .case-phone-viewer).is-pointer-over .grid-player.is-resting,
  :is(.grid-viewer, .case-phone-viewer):has(.grid-stage:hover) .grid-player.is-resting,
  .grid-player.is-resting:hover,
  .grid-player.is-resting:focus-within {
    opacity: 1;
    pointer-events: auto;
  }
}

.grid-player[hidden] {
  display: none;
}

.grid-player-toggle {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--player-control-size);
  height: var(--player-control-size);
  padding: 0;
  border: 0;
  border-radius: var(--player-button-radius);
  background: transparent;
  color: #000;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.15s ease, transform 0.15s ease;
}

.grid-player-toggle svg {
  grid-area: 1 / 1;
  width: 20px;
  height: 20px;
}

.grid-player-toggle .icon-play,
.grid-player-toggle.is-paused .icon-pause {
  display: none;
}

.grid-player-toggle.is-paused .icon-play {
  display: block;
}

.grid-player-toggle:active {
  transform: scale(0.92);
}

@media (hover: hover) {
  .grid-player-toggle:hover {
    background: rgba(0, 0, 0, 0.06);
  }
}

/* The scrub bar: a thin rounded track, black up to the playhead, with a small round thumb. */
.grid-player-scrub {
  flex: 1;
  height: var(--player-control-size);
  margin: 0 12px 0 4px;
  background: transparent;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.grid-player-scrub::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: var(--player-track-radius);
  background: linear-gradient(90deg, #000 var(--p, 0%), rgba(0, 0, 0, 0.14) var(--p, 0%));
}

.grid-player-scrub::-moz-range-track {
  height: 4px;
  border-radius: var(--player-track-radius);
  background: linear-gradient(90deg, #000 var(--p, 0%), rgba(0, 0, 0, 0.14) var(--p, 0%));
}

.grid-player-scrub::-webkit-slider-thumb {
  width: 12px;
  height: 12px;
  margin-top: -4px;
  border: 0;
  border-radius: 50%;
  background: #000;
  -webkit-appearance: none;
  appearance: none;
}

.grid-player-scrub::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border: 0;
  border-radius: 50%;
  background: #000;
}

.grid-player-scrub:focus-visible {
  outline: 2px solid #000;
  outline-offset: 2px;
  border-radius: var(--player-track-radius);
}

:is(.grid-viewer, .case-phone-viewer)[open] {
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: none;
}

html.has-enlarged-viewer {
  touch-action: manipulation;
}

/* The frost is a real layer, like the project page's backdrop, so the dismiss can fade it.
   Animating the dialog's ::backdrop drops the white on iPhone and leaves a colour blur. */
.grid-scrim {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: rgba(255, 255, 255, 0.94);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

:is(.grid-viewer, .case-phone-viewer)::backdrop {
  background: transparent;
}

@media (max-width: 720px), (hover: none) {
  .grid-scrim {
    background: rgba(255, 255, 255, 0.96);
    -webkit-backdrop-filter: blur(18px);
    backdrop-filter: blur(18px);
  }
}

.disco :is(.grid-viewer, .case-phone-viewer) {
  color: #fff;
}

.disco .grid-scrim {
  background: rgba(12, 12, 12, 0.84);
}

.grid-stage {
  position: relative;
  z-index: 1;
  flex: none;
  width: var(--stage-w);
  height: var(--stage-h);
  overflow: hidden;
  /* One clip does the rounding, so the animation doesn't also have to mask a second rounded edge. */
  --u: calc(var(--stage-h) / 800);
  --surface-radius: max(var(--corner-min), calc(var(--corner-ratio) * var(--u)));
  clip-path: inset(0 round var(--surface-radius));
  /* The tiles' own grey, so wherever the growing view and the tile don't line up exactly, nothing
     white flashes at its edges. */
  background: #f7f7f7;
  outline: none;
  transform-origin: 0 0;
  will-change: transform;
}

.grid-stage .tile {
  width: 100%;
  height: 100%;
  --u: calc(var(--stage-h) / 800);
  outline: none;
}

/* Inner stroke on the visible window, not the clipped stage edge (grid-viewer.js moves it). */
.grid-stage-border {
  position: absolute;
  z-index: 3;
  box-sizing: border-box;
  pointer-events: none;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px var(--tile-inner-stroke);
}

/* One full inner stroke on the visible window; the tile underneath can be wider than the clip. */
:is(.grid-viewer, .case-phone-viewer) .grid-stage .tile::after {
  box-shadow: none;
}

.disco .grid-stage-border {
  box-shadow: inset 0 0 0 1px var(--tile-inner-stroke-disco);
}

/* Full image height stays available, so the art can ease to the grid's crop without gaps. */
.grid-stage .tile-bg {
  inset: auto;
  top: 50%;
  left: 50%;
  width: 100%;
  height: auto;
  min-height: 100%;
  translate: -50% -50%;
}

.disco .grid-stage {
  background: #141414;
}

.grid-control {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: #111;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* Release springs back with a little overshoot. */
  transition: opacity 0.15s ease, transform 0.25s cubic-bezier(0.34, 1.15, 0.64, 1);
}

/* Pressed by pointer or by the arrow keys: a quick squeeze. */
.grid-control:active,
.grid-control.is-pressed {
  transform: scale(0.94);
  transition: opacity 0.15s ease, transform 0.08s ease-out;
}

@media (hover: hover) {
  .grid-control:hover {
    opacity: 0.55;
  }
}

.disco .grid-control {
  color: #fff;
}

.grid-prev:focus,
.grid-next:focus,
.grid-prev:focus-visible,
.grid-next:focus-visible {
  outline: none;
}

.grid-prev,
.grid-next {
  position: absolute;
  z-index: 2;
  top: calc(50% + var(--stage-h) / 2 + 16px);
}

.grid-prev {
  left: calc(50% - 50px);
}

.grid-next {
  left: calc(50% + 6px);
}

.grid-viewer-header {
  position: absolute;
  bottom: calc(50% + var(--stage-h) / 2 + 14px);
  left: max(20px, calc(50% - var(--stage-w) / 2));
  width: min(var(--stage-w), calc(100% - 40px));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  pointer-events: none;
}

.grid-info {
  min-width: 0;
  text-align: left;
  overflow-wrap: anywhere;
}

.grid-info .title {
  font-size: var(--text-body);
}

.grid-info .meta {
  color: #111;
  font-size: var(--text-body);
  line-height: 1.3;
}

.disco .grid-info .meta {
  color: #fff;
}

/* Align the icon with the artwork's right edge while keeping its full hit area. */
.grid-close {
  margin-left: auto;
  margin-right: -15px;
  pointer-events: auto;
}

/* Close is Escape, backdrop tap, or browser dismiss — no on-screen X on enlarged grid or Locket panels. */
:is(.grid-viewer, .case-phone-viewer) .grid-viewer-header {
  display: none;
}

/* Locket viewer: chevrons track --stage-h; hide while closed/closing so they don't flash at the
   mobile fallback size when those vars are cleared mid-dismiss. */
.case-phone-viewer:not([open]) :is(.grid-prev, .grid-next),
.case-phone-viewer.is-closing :is(.grid-prev, .grid-next) {
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
}

@media (max-width: 720px) {
  :is(.grid-viewer, .case-phone-viewer) {
    --stage-w: calc(100vw - 24px);
    --stage-h: calc(100dvh - 140px);
    --player-bar-inset-x: 10px;
    --player-bar-inset-y: 10px;
  }

  :is(.grid-viewer, .case-phone-viewer) .grid-player {
    bottom: var(--player-bar-inset-y);
    left: calc(var(--clip-x, 0px) + var(--player-bar-inset-x));
    right: calc(var(--clip-x, 0px) + var(--player-bar-inset-x));
  }
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Filter tabs above the other-work grid; same type scale as .wordmark / .site-nav / .sky-bottom. */
.filters {
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-ui);
  font-weight: 500;
  letter-spacing: var(--track-ui);
  line-height: 1.5;
  font-kerning: normal;
  font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
  font-synthesis: none;
}

.filter {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-height: 1.75em;
  padding-block: 0.125em;
  /* 12px side padding in Figma; fixed so every label gets the same inset. */
  padding-inline: 12px;
  border: 0;
  border-radius: 2px;
  background: transparent;
  color: inherit;
  font: inherit;
  line-height: inherit;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.2s ease, background-color 0.2s ease;
}

.filter[aria-pressed="true"] {
  background: rgba(0, 60, 255, 0.05);
  color: #003cff;
}

.disco .filter[aria-pressed="true"] {
  background: rgba(143, 155, 255, 0.12);
  color: #8f9bff;
}

/* Focus ring for keyboard only; a click or tap leaves none behind. */
.filter:focus {
  outline: none;
}

.filter:focus-visible {
  outline: 2px solid #003cff;
  outline-offset: 2px;
}

.disco .filter:focus-visible {
  outline-color: #8f9bff;
}

/* Grid backdrops are flat, light colours that sit well together: no photos, gradients or grain. */
:is(.anthology, .grid-stage) .tile > .tile-bg,
:is(.anthology, .grid-stage) .trash-texture,
:is(.anthology, .grid-stage) .tile::before {
  display: none;
}

:is(.anthology, .grid-stage) .tile-lockets { background: #f0f0ee; }
:is(.anthology, .grid-stage) .tile-carbon { background: #f0f0ee; }
:is(.anthology, .grid-stage) .tile-backup { background: #f0f0ee; }
:is(.anthology, .grid-stage) .tile-brush { background: #f0f0ee; }
:is(.anthology, .grid-stage) .tile-trash { background: #f0f0ee; }
:is(.anthology, .grid-stage) .tile-claim { background: #f0f0ee; }
:is(.anthology, .grid-stage) .tile-customize { background: #f0f0ee; }
:is(.anthology, .grid-stage) .tile-receive { background: #f0f0ee; }
:is(.anthology, .grid-stage) .tile-existing { background: #f0f0ee; }
:is(.anthology, .grid-stage) .tile-multi { background: #f0f0ee; }
:is(.anthology, .grid-stage) .tile-onboarding { background: #f0f0ee; }

/* New York sky: a static, softly blended horizon beneath the contact links. */
.site-footer {
  /* Taller by the phone's home bar and, on iOS Safari, by the floating toolbar (lvh − svh), so
     the gradient runs under the chrome while the clock and links stay above it — work page and
     project pages use the same footer. No padding sits after it: a negative margin never covered
     that padding on mobile Safari, which left a white block under the sky. */
  --under-toolbar: max(0px, calc(100lvh - 100svh));
  --sky-height: calc(clamp(330px, 38vw, 580px) + env(safe-area-inset-bottom) + var(--under-toolbar));
  position: relative;
  height: var(--sky-height);
  margin-top: clamp(72px, 7vw, 112px);
  margin-bottom: 0;
  isolation: isolate;
}

/* Same sky as the work page. Only the column is narrowed; the gradient still runs to the screen edges.
   Phones hide it: the project sheet doesn't scroll the document, so this footer can't meet Safari's toolbar. */
.modal-scroll > .site-footer {
  flex-shrink: 0;
  width: 100%;
  max-width: calc(1720px - 2 * var(--page-gutter));
  margin-inline: auto;
}

@media (max-width: 720px) {
  .modal-scroll > .site-footer {
    display: none;
  }
}

.sky-panel {
  position: relative;
  height: var(--sky-height);
}

.sky-bottom {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(8% + env(safe-area-inset-bottom) + var(--under-toolbar, 0px));
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 24px;
  color: var(--sky-ink, #17202d);
  font-family: "GNU Grotesk", sans-serif;
  font-size: var(--text-ui);
  font-weight: 500;
  letter-spacing: var(--track-ui);
  line-height: 1.5;
  font-synthesis: none;
  font-kerning: normal;
  font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
}

.sky-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px clamp(18px, 1.78vw, 32px);
  text-align: right;
}

/* Each link gets a square see-through box on hover, like the hero's highlighted words, in its own ink colour, drawn just outside the
   text so the row never shifts. */
.sky-links a {
  position: relative;
  z-index: 0;
  color: inherit;
  text-decoration: none;
}
.sky-links a::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -0.18em -0.45em;
  background: color-mix(in srgb, currentColor 7%, transparent);
  opacity: 0;
  transition: opacity 0.15s ease;
}
.sky-links a:focus-visible { outline: 2px solid currentColor; outline-offset: 5px; }
@media (hover: hover) and (pointer: fine) {
  .sky-links a:hover::before { opacity: 1; }
}

.sky-gradient {
  position: absolute;
  z-index: -1;
  top: 0;
  /* Overlap the canvas edge to cover fractional CSS pixels on high-density phone screens. */
  bottom: -2px;
  /* footer.js replaces these with exact pixel offsets to the visible edges. */
  left: calc(50% - 50vw);
  right: calc(50% - 50vw);
  background: linear-gradient(#fff, #7c95bd 36%, #e8e5e0 70%, #fbddb9 92%);
}

.sky-clock {
  display: flex;
  flex: none;
  align-items: center;
  gap: 0.4em;
  margin: 0;
  font: inherit;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Safari gives <time> a system face; match the site typography explicitly. */
.sky-bottom time,
.sky-bottom .sky-location {
  font: inherit;
  font-variant-numeric: tabular-nums;
}

/* Fine static grain; the lower reading area keeps a much lighter texture. */
.sky-gradient::after,
.sky-swatch::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: url("assets/sky/grain.svg") repeat;
  background-size: 180px 180px;
  mix-blend-mode: soft-light;
  opacity: 0.24;
  -webkit-mask-image: linear-gradient(transparent, #000 18%, #000 52%, rgba(0,0,0,0.06) 70%);
  mask-image: linear-gradient(transparent, #000 18%, #000 52%, rgba(0,0,0,0.06) 70%);
}

.sky-icon {
  flex: none;
  width: 1em;
  height: 1em;
  background: currentColor;
  -webkit-mask: url("assets/sky/sun-fill.svg") center / contain no-repeat;
  mask: url("assets/sky/sun-fill.svg") center / contain no-repeat;
}
.sky-clock.is-night .sky-icon {
  -webkit-mask-image: url("assets/sky/moon-fill.svg");
  mask-image: url("assets/sky/moon-fill.svg");
}

@media (max-width: 720px) {
  .sky-bottom {
    gap: 12px;
    bottom: calc(8px + env(safe-area-inset-bottom) + var(--under-toolbar, 0px));
  }
  .sky-links { overflow-wrap: anywhere; }
}
@media (max-width: 560px) {
  .sky-links {
    flex-direction: column;
    align-items: flex-end;
  }
}

.fortune {
  --spring: cubic-bezier(0.34, 1.45, 0.64, 1);
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  padding-top: 76px;
}

.cookie {
  width: clamp(150px, 16vw, 210px);
  padding: 0;
  border: 0;
  background: none;
  color: #3b2913;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.cookie svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.cookie path {
  fill: none;
  stroke: currentColor;
  stroke-width: 3.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.cookie .dough {
  fill: #f3d59b;
}

.cookie .retrace {
  opacity: 0.5;
  stroke-width: 2.2;
}

.cookie .crease {
  opacity: 0.75;
  stroke-width: 2.4;
}

.cookie .shade {
  stroke: #d9a443;
  stroke-width: 2.6;
}

.cookie .half {
  transform-box: fill-box;
  transition: transform 0.55s var(--spring);
}

.cookie .half-left {
  transform-origin: 100% 50%;
}

.cookie .half-right {
  transform-origin: 0% 50%;
}

@keyframes cookie-rattle {
  0%,
  100% {
    rotate: 0deg;
  }
  25% {
    rotate: -5deg;
  }
  75% {
    rotate: 5deg;
  }
}

@media (hover: hover) {
  .cookie[aria-expanded="false"]:hover svg {
    animation: cookie-rattle 0.32s ease-in-out 2;
  }
}

.cookie[aria-expanded="true"] .half-left {
  transform: translate(-22%, 12%) rotate(-26deg);
}

.cookie[aria-expanded="true"] .half-right {
  transform: translate(22%, 12%) rotate(26deg);
}

.cookie .crumbs circle {
  fill: #e2b673;
  opacity: 0;
}

@keyframes cookie-crumb {
  0% {
    opacity: 1;
    translate: 0 0;
  }
  100% {
    opacity: 0;
    translate: var(--x) var(--y);
  }
}

.cookie[aria-expanded="true"] .crumbs circle {
  animation: cookie-crumb 0.6s cubic-bezier(0.23, 1, 0.32, 1);
}

/* The fortune itself: a narrow slip of thin paper, a little curled from being folded inside. */
.fortune-slip {
  position: absolute;
  top: 0;
  left: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  width: max-content;
  max-width: min(440px, 92%);
  margin: 0;
  padding: 11px 26px 10px;
  background:
    linear-gradient(90deg, rgba(120, 100, 60, 0.13), transparent 5%, transparent 31%, rgba(120, 100, 60, 0.07) 33%, transparent 36%, transparent 65%, rgba(120, 100, 60, 0.07) 67%, transparent 70%, transparent 95%, rgba(120, 100, 60, 0.13)),
    linear-gradient(180deg, #fffefa, #f8f4e9);
  box-shadow: 0 1px 1px rgba(60, 40, 10, 0.18), 0 6px 14px -6px rgba(60, 40, 10, 0.3);
  color: #b3261e;
  text-align: center;
  opacity: 0;
  transform: translate(-50%, 90px) rotate(-2deg) scale(0.12, 0.6);
  transition: transform 0.3s ease-in, opacity 0.2s ease-in;
  pointer-events: none;
}

.fortune.is-open .fortune-slip {
  opacity: 1;
  transform: translate(-50%, 0) rotate(-2deg) scale(1);
  transition: transform 0.7s var(--spring) 0.18s, opacity 0.2s ease 0.18s;
  pointer-events: auto;
}

.fortune-text {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.01em;
  line-height: 1.35;
  text-wrap: balance;
}

.fortune-numbers {
  font-size: 10px;
  letter-spacing: 0.08em;
  opacity: 0.8;
  text-transform: uppercase;
}

.fortune-hint {
  margin: 10px 0 0;
  color: #4f3400;
  font-size: 14px;
  opacity: 0.6;
}

.disco .cookie {
  color: rgba(255, 255, 255, 0.85);
}

.disco .cookie .dough {
  fill: #6b5326;
}

.disco .fortune-hint {
  color: inherit;
}

@media (prefers-reduced-motion: reduce) {
  .cookie .half,
  .fortune-slip,
  .fortune.is-open .fortune-slip {
    transition-duration: 0.01s;
    transition-delay: 0s;
  }

  .cookie svg,
  .cookie .crumbs circle {
    animation: none !important;
  }
}

/* Sites block concepts, from Figma's 711 × 710 cards. Both scale from the tile's height (the row
   they share), so their windows sit at the same size and the same spot from the top-left corner,
   running off the right and bottom edges, whatever each tile's width. */
.tile-sb-carousel,
.tile-sb-spotify {
  container-type: size;
  background: #f0f0ee;
}

.sb-art {
  position: absolute;
  inset: 0;
}

/* One scale for both concept tiles so the Desktop chrome and recordings share the same --k (Figma 710 card). */
.tile-sb-carousel .sb-art,
.tile-sb-spotify .sb-art {
  --k-h: calc(100cqh / 710);
  --k: var(--k-h);
}

@media (min-width: 561px) {
  .tile-sb-carousel .sb-art,
  .tile-sb-spotify .sb-art {
    --k: min(var(--k-h), calc((100cqw - 95 * var(--k-h)) / 675));
  }
}

.sb-window {
  isolation: isolate;
  position: absolute;
  top: calc(94 * var(--k));
  right: calc(-40 * var(--k));
  /* Well past the tile's bottom, so the window's lower edge never shows. */
  bottom: calc(-60 * var(--k));
  left: calc(81 * var(--k));
  /* The window is an image of the real frame (2000 x 1757), used as a nine-slice border: the
     corners, the "Desktop" bar and its + keep their shape, and only plain edges and the white
     middle stretch to the window's size. One image pixel is 37/107 of --k (the bar's height). */
  --px: calc(37 / 107 * var(--k));
  border-style: solid;
  border-width: calc(107 * var(--px)) calc(110 * var(--px)) calc(44 * var(--px)) calc(240 * var(--px));
  border-image: url("assets/sites/frame.webp") 107 110 44 240 fill / calc(107 * var(--px)) calc(110 * var(--px)) calc(44 * var(--px)) calc(240 * var(--px)) stretch;
}

.sb-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: calc(37 * var(--k));
  padding: 0 calc(7.4 * var(--k));
  font-size: calc(10.2 * var(--k));
  color: rgba(0, 0, 0, 0.9);
}

.sb-bar span {
  display: inline-flex;
  align-items: center;
  gap: calc(4.6 * var(--k));
}

.sb-bar img {
  width: calc(22.2 * var(--k));
  height: calc(22.2 * var(--k));
}

/* The recording sits in the frame's white middle, just inside its hairline. The window's border is
   the frame image, so the body is placed from the window's outer edge. */
.sb-body {
  position: absolute;
  inset: calc((36 - 107 * 37 / 107) * var(--k)) calc((13.5 - 110 * 37 / 107) * var(--k)) calc((13.9 - 44 * 37 / 107) * var(--k)) calc((13.9 - 240 * 37 / 107) * var(--k));
  overflow: hidden;
}

.sb-video {
  position: absolute;
  display: block;
  max-width: none;
}

/* Centre each recording in the frame's white body (not the tile), and cap width so it never outgrows the chrome. */
.tile-sb-carousel .sb-video {
  left: calc(50% - 258.4 * var(--k));
  bottom: calc((58.8 + 58 + 16) * var(--k));
  width: calc(516.8 * var(--k));
  max-width: 100%;
  height: auto;
  aspect-ratio: 516.8 / 363.8;
}

.tile-sb-spotify .sb-video {
  left: calc(50% - 337.5 * var(--k));
  bottom: calc((39 + 58 + 16) * var(--k));
  width: calc(675 * var(--k));
  max-width: 100%;
  height: auto;
  aspect-ratio: 3392 / 2160;
}

/* Spotify embed: the wide recording uses the same tile-centred math as desktop; max-width there breaks scale on phones. */
@media (max-width: 720px) {
  .tile-sb-spotify .sb-video {
    left: calc((100cqw - 81 * var(--k)) / 2 - (13.9 + 305) * var(--k));
    width: calc(675 * var(--k));
    max-width: none;
    height: auto;
  }
}

/* Figma Sites, a row from Figma 572 tall: --k is one Figma pixel, scaled so 572 fills the row. */
.tile-fonts,
.tile-blocks,
.tile-sb-carousel,
.tile-sb-spotify,
.tile-reference-ds {
  --k: calc(var(--u) * 800 / 572);
}

.tile-reference-ds .rds-cover {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 96%;
  height: 96%;
  max-width: none;
  object-fit: contain;
  /* Inset 100% + object-position barely moves the art; overscale and translate instead. */
  translate: calc(-50% - 10%) calc(-50% + 8%);
  pointer-events: none;
}

/* Taller wash than the Sites tiles so the art's lower edge never punches through on the grey. */
.tile-reference-ds .tile-fade {
  z-index: 3;
  height: calc(220 * var(--k));
  background: linear-gradient(
    rgba(247, 247, 247, 0) 0%,
    rgba(247, 247, 247, 0.45) 42%,
    rgba(247, 247, 247, 0.92) 72%,
    #f7f7f7 100%
  );
}

/* One Figma panel shadow for Sites blocks, custom fonts, and Slides presenter notes — extra lift
   under the top edge so the frame reads on the grey tile. */
:is(.blocks-window, .fonts-window, .notes-window) {
  box-shadow:
    0 calc(-6 * var(--k)) calc(16 * var(--k)) rgba(0, 0, 0, 0.035),
    0 calc(-2 * var(--k)) calc(6 * var(--k)) rgba(0, 0, 0, 0.022),
    0 calc(74.186 * var(--k)) calc(59.349 * var(--k)) rgba(0, 0, 0, 0.04),
    0 calc(30.993 * var(--k)) calc(24.795 * var(--k)) rgba(0, 0, 0, 0.03),
    0 calc(16.57 * var(--k)) calc(13.256 * var(--k)) rgba(0, 0, 0, 0.024),
    0 calc(9.289 * var(--k)) calc(7.431 * var(--k)) rgba(0, 0, 0, 0.022),
    0 calc(4.933 * var(--k)) calc(3.947 * var(--k)) rgba(0, 0, 0, 0.016),
    0 calc(2.053 * var(--k)) calc(1.642 * var(--k)) rgba(0, 0, 0, 0.012),
    0 0 calc(0.357 * var(--k)) rgba(0, 0, 0, 0.1),
    0 calc(2.14 * var(--k)) calc(5.707 * var(--k)) rgba(0, 0, 0, 0.06),
    0 calc(0.713 * var(--k)) calc(2.14 * var(--k)) rgba(0, 0, 0, 0.05);
}

/* The Insert panel: 71 in from the left and top, running off the bottom and always past the right
   edge, however wide the tile gets (enlarged in the viewer, too), so there's never a gap at its
   right. Its recording only covers the panel's left part; the rest of the window is the editor's
   white canvas, set off from the recording by a hairline. 8px corners, no outline. */
.blocks-window {
  position: absolute;
  top: calc(50% - (286 - 71) * var(--k));
  left: calc(71 * var(--k));
  width: max(calc(770 * var(--k)), calc(100% - 71 * var(--k) + 40px));
  height: calc(638 * var(--k));
  overflow: hidden;
  border-radius: 8px;
  background: #fff;
}

/* Custom fonts: a white window of the font settings, centred 167.5 left of the tile's middle so its
   left side runs off the tile. */
.fonts-window {
  isolation: isolate;
  position: absolute;
  top: calc(50% - (286 - 71) * var(--k));
  left: calc(50% - (167.5 + 436.5) * var(--k));
  width: calc(873 * var(--k));
  height: calc(570.7 * var(--k));
  overflow: hidden;
  border-radius: 8px;
  background: #fff;
}

.fonts-window::before {
  content: "";
  position: absolute;
  z-index: 2;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: inset 0 0 0 1px var(--tile-inner-stroke);
}

/* Presenter notes, from the 682 × 572 Figma card. The window is centred 51 to the right of
   the tile, so a wide tile shows the same crop as the frame and a narrow one clips the right. */
.tile-notes {
  --k: calc(var(--u) * 800 / 572);
}

.notes-window {
  position: absolute;
  top: calc(50% - (0.22 + 389.551 / 2) * var(--k));
  left: calc(50% + (51 - 650 / 2) * var(--k));
  width: calc(650 * var(--k));
  height: calc(389.551 * var(--k));
  overflow: hidden;
  border-radius: calc(8.157 * var(--k));
  background: #fff;
}

.notes-video {
  position: absolute;
  top: 0;
  left: calc(-1.12 * var(--k));
  width: calc(651.123 * var(--k));
  height: 100%;
  max-width: none;
  object-fit: cover;
}

.fonts-window img {
  position: absolute;
  top: 0;
  left: calc((39 - 834 * 0.0244) * var(--k));
  width: calc(834 * 1.0248 * var(--k));
  height: calc(559 * var(--k));
  max-width: none;
}

/* The Figma Sites tiles fade into the tile's grey along the bottom (Figma's 116-tall wash). */
.tile-fade {
  position: absolute;
  z-index: 2;
  right: 0;
  bottom: 0;
  left: 0;
  height: calc(116 * var(--k));
  pointer-events: none;
  background: linear-gradient(rgba(247, 247, 247, 0), #f7f7f7 70%);
}

.blocks-window::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(326.4 * var(--k));
  width: 1px;
  background: rgba(0, 0, 0, 0.08);
}

/* Lifted 3 units so the recording's grey top row sits outside the window. */
.blocks-video {
  position: absolute;
  top: calc(-3 * var(--k));
  left: 0;
  width: calc(326.4 * var(--k));
  height: calc(565.3 * var(--k));
  max-width: none;
  object-fit: cover;
}

/* Case study opening, from Figma (an 1800 frame): a big centred title, a facts column beside two
   intro paragraphs, then the film or the before/after. Those pictures stay the full width of the
   cover. The text uses 1160/1800 of that width once the page is wide enough. */
/* One rhythm down the page: the same gap under the title, above the film and before the footer. */
.modal-body:has(> .case-masthead) {
  --case-space: 120px;
  padding-top: 0;
  padding-bottom: var(--case-space);
}

.modal-body .case-masthead {
  display: flex;
  align-items: center;
  justify-content: center;
  padding-block: clamp(88px, 11vw, 136px);
  text-align: center;
}

/* Phones: the same rhythm, tighter. Title 48 from the cover, then 64 between each part. */
@media (max-width: 720px) {
  .modal-body:has(> .case-masthead) {
    --case-space: 64px;
  }

  .modal-body .case-masthead {
    padding-block: 64px 72px;
  }
}

.modal-body .case-masthead .title {
  width: 100%;
  margin: 0;
  font-family: "Zalando Sans SemiExpanded", sans-serif;
  font-size: var(--text-display);
  font-weight: 600;
  font-synthesis: none;
  line-height: 1;
  letter-spacing: -0.035em;
  text-align: center;
  /* Each project can tint its title (Calendar's is a warm brown). */
  color: var(--case-accent, inherit);
}

.case-intro,
.case-film,
.case-compare {
  width: 100%;
  margin-inline: auto;
}

@media (min-width: 1024px) {
  .case-intro {
    width: calc(100% * 1160 / 1800);
  }
}

.case-intro {
  display: flex;
  gap: clamp(32px, 4.4vw, 80px);
  font-size: var(--text-body);
  letter-spacing: var(--track-body);
  line-height: 1.6;
}

.case-intro-facts {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 1.33em;
  margin: 0;
}

.case-intro-facts dt {
  color: rgba(0, 0, 0, 0.5);
}

.case-intro-facts dd {
  margin: 3px 0 0;
}

.modal-body .case-intro-text p {
  margin: 0;
  color: inherit;
}

.modal-body .case-intro-text p + p {
  margin-top: 1.5em;
}

.modal-body .case {
  container-type: inline-size;
  --u: calc(100cqw / 1480);
  --surface-radius: max(var(--corner-min), calc(var(--corner-ratio) * var(--u)));
}

.modal-body .case :is(.case-film, .case-video, .case-phone-panel) {
  border-radius: var(--surface-radius);
}

.modal-body .case img:not(.iphone-render) {
  border-radius: var(--surface-radius);
}

.case-film {
  display: block;
  aspect-ratio: 1160 / 640;
  overflow: hidden;
  background: #f7f7f7;
}

/* Media Chrome's look, through its own custom properties: no gradient, dark glyphs. Shared by the
   Weave film and the Locket phones. */
.case-film,
.case-phone-panel,
.film-controls {
  --media-control-height: 36px;
  --media-control-padding: 0px;
  --media-control-hover-background: rgba(0, 0, 0, 0.06);
}

.case-film,
.case-phone-panel {
  --media-control-background: transparent;
  --media-control-hover-background: rgba(0, 0, 0, 0.06);
  --media-primary-color: #000;
  --media-secondary-color: transparent;
  --media-icon-color: #000;
  --media-font-family: "GNU Grotesk", sans-serif;
  --media-range-track-height: 4px;
  --media-range-track-border-radius: var(--player-track-radius);
  --media-range-track-background: rgba(0, 0, 0, 0.14);
  --media-range-bar-color: #111;
  --media-range-track-pointer-background: rgba(0, 0, 0, 0.1);
  --media-time-range-buffered-color: rgba(0, 0, 0, 0.1);
  --media-range-thumb-background: #111;
  --media-range-thumb-width: 12px;
  --media-range-thumb-height: 12px;
  --media-range-thumb-box-shadow: none;
  /* No time readout, same as the grid player: just the track and the thumb. */
  --media-preview-box-display: none;
  --media-tooltip-display: none;
  --media-focus-box-shadow: inset 0 0 0 2px #111;
}

.case-film video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The controls: one floating frosted bar. Tapping the picture hides it (video-sound.js). */
.film-controls {
  align-self: stretch;
  align-items: center;
  gap: 0;
  margin: 0 var(--player-bar-inset) var(--player-bar-inset);
  padding: var(--player-bar-padding);
  box-sizing: border-box;
  min-height: var(--player-bar-height);
  border-radius: var(--player-chrome-radius);
  background: rgba(255, 255, 255, 0.62);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
  backdrop-filter: blur(16px) saturate(1.4);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 6px 20px rgba(0, 0, 0, 0.08);
  transition: opacity 0.25s ease;
}

.film-controls media-play-button,
.film-controls media-mute-button {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--player-control-size);
  height: var(--player-control-size);
  padding: 0;
  border-radius: var(--player-button-radius);
  --media-button-icon-width: 20px;
  --media-button-icon-height: 20px;
  transition: transform 0.15s ease;
}

.film-controls media-play-button:active,
.film-controls media-mute-button:active {
  transform: scale(0.92);
}

/* The scrub bar starts right after the play button: only the thumb's half-width of room on its left,
   so the thumb never clips at the start. */
/* The expand button (video-sound.js), styled like the player's own buttons. */
.film-expand {
  flex: none;
  align-self: center;
  display: grid;
  place-items: center;
  width: var(--player-control-size);
  height: var(--player-control-size);
  padding: 0;
  border: 0;
  border-radius: var(--player-button-radius);
  background: transparent;
  color: #000;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.15s ease, transform 0.15s ease;
}

.film-expand svg {
  width: 18px;
  height: 18px;
}

.film-expand:active {
  transform: scale(0.92);
}

@media (hover: hover) {
  .film-expand:hover {
    background: rgba(0, 0, 0, 0.06);
  }
}

.film-expand:focus-visible {
  outline: 2px solid #000;
  outline-offset: 2px;
}

/* Full screen is for phones and tablets; on a desktop-sized screen with a mouse, no expand button. */
@media (min-width: 1024px) and (hover: hover) and (pointer: fine) {
  .film-expand {
    display: none;
  }
}

.film-controls media-time-range,
.film-controls .film-scrub {
  flex: 1;
  height: var(--player-control-size);
  --media-range-padding-left: 6px;
  --media-range-padding-right: 10px;
  border-radius: var(--player-track-radius);
}

.film-controls .film-scrub {
  margin: 0 8px 0 6px;
}

@media (max-width: 720px) {
  .film-controls {
    margin: 0 8px 8px;
  }
}

/* Before and after, side by side: two shorter panels with a front-on phone in each. --k is one unit of
   the phone render (276 units wide). */
.case-compare {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(12px, 1.6vw, 23px);
}

@media (max-width: 1024px) {
  .case-compare {
    gap: clamp(6px, 0.9vw, 12px);
  }
}

@media (max-width: 720px) {
  .case-compare {
    gap: 8px;
  }
}

.modal-body .case-compare figure {
  margin: 0;
}

.case-phone-panel {
  position: relative;
  container-type: inline-size;
  isolation: isolate;
  /* Phone body ~46% of panel width; 460 × 540 panel leaves air under the handset for controls. */
  --k: calc(100cqw * 0.46 / 276);
  aspect-ratio: 460 / 540;
  overflow: hidden;
  cursor: zoom-in;
  touch-action: manipulation;
}

.case-phone-panel::after {
  content: "";
  position: absolute;
  z-index: 4;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: inset 0 0 0 1px var(--tile-inner-stroke);
}

.case-phone-panel:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
}

/* Enlarged: keyboard steps the stage, not the panel or chevrons (same as grid tiles). */
.case-phone-viewer[open] .case-phone-panel:focus,
.case-phone-viewer[open] .case-phone-panel:focus-visible {
  outline: none;
}

html.case-phone-just-closed .case-phone-panel:focus,
html.case-phone-just-closed .case-phone-panel:focus-visible {
  outline: none !important;
}

.grid-stage .case-phone-panel {
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
}

/* Soft yellow washes (replaces locket-thumb/bg.webp). */
.case-phone-before {
  background:
    radial-gradient(100% 90% at 84% 14%, rgba(255, 230, 165, 0.75), transparent 58%),
    radial-gradient(85% 78% at 8% 86%, rgba(255, 244, 200, 0.7), transparent 52%),
    linear-gradient(168deg, #fff8e6 0%, #ffefb8 42%, #ffe49a 100%);
}

.case-phone-after {
  background:
    radial-gradient(95% 85% at 78% 80%, rgba(255, 215, 150, 0.72), transparent 56%),
    radial-gradient(75% 70% at 12% 16%, rgba(255, 238, 188, 0.68), transparent 50%),
    linear-gradient(158deg, #fff6e0 0%, #ffecc0 46%, #ffdfa8 100%);
}

/* Front-on Locket phones: centre in the panel instead of Figma’s --cx/--cy offsets. */
.case-phone-panel .iphone {
  top: 50%;
  left: 50%;
  translate: -50% -50%;
}

/* The screen runs 2 units under the frame's black border on every side, so no sliver of background
   shows between the video and the frame; the frame's rounded cut-out clips its corners. */
.case-phone-panel .iphone-plane {
  --screen-round: calc(39.44 * var(--k));
  width: calc(265.33 * var(--k));
  height: calc(566.81 * var(--k));
  border-radius: var(--screen-round);
  /* A clip path as well as overflow: a playing video gets its own compositing layer, which can
     ignore a rounded overflow clip inside a 3D-transformed parent and show square corners. */
  clip-path: inset(0 round var(--screen-round));
}

.case-phone-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--screen-round);
  clip-path: inset(0 round var(--screen-round));
}

/* Undo the page's general image rules (auto height, rounded corners, placeholder tint) inside the panels. */
.modal-body .case .case-phone-panel img {
  height: 100%;
  max-width: none;
  border-radius: 0;
  background: none;
}

.modal-body .case-compare figcaption {
  margin-top: clamp(12px, 1.6vw, 23px);
  color: rgba(0, 0, 0, 0.5);
  font-size: var(--text-body);
  letter-spacing: var(--track-body);
  line-height: 1.6;
}

@media (max-width: 560px) {
  .case-compare {
    grid-template-columns: 1fr;
    row-gap: clamp(20px, 5.5vw, 32px);
  }

  .modal-body .case-compare figcaption {
    margin-top: 8px;
  }
}

.modal-body .case .case-intro + section {
  margin-top: var(--case-space);
}

@media (max-width: 720px) {
  .case-intro {
    flex-direction: column;
  }

  .case-intro-facts {
    flex-direction: row;
    gap: 32px;
  }
}

.disco .case-intro-facts dt {
  color: rgba(255, 255, 255, 0.6);
}

/* Case study layout, used by the Weave Tools project: a label column, short text, and large quiet media. */
.modal-body .case {
  --case-gap: clamp(8px, 1vw, 12px);
  --case-block: clamp(64px, 9vw, 112px);
}

.modal-body .case-lede {
  max-width: 21em;
  margin: clamp(16px, 2vw, 24px) 0 0;
  font-size: var(--text-intro);
  letter-spacing: var(--track-intro);
  line-height: 1.16;
  text-wrap: balance;
}

.case-facts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px 24px;
  margin: clamp(28px, 4vw, 48px) 0 0;
  padding-top: 16px;
  border-top: 1px solid rgba(0, 0, 0, 0.1);
}

.case-facts dt {
  color: var(--modal-muted);
  font-size: 14px;
  line-height: 1.4;
}

.case-facts dd {
  margin: 2px 0 0;
}

.modal-body .case section {
  margin-top: var(--case-block);
}

.case-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 3fr);
  gap: 8px 24px;
  margin-bottom: clamp(24px, 3.4vw, 40px);
}

.modal-body .case .case-label {
  margin: 0;
  padding-top: 0.5em;
  color: var(--modal-muted);
  font-size: 14px;
  line-height: 1.4;
}

.modal-body .case h3 {
  max-width: 24em;
  margin: 0;
  font-size: var(--text-intro);
  font-weight: 500;
  line-height: 1.24;
  letter-spacing: var(--track-intro);
  text-wrap: balance;
}

.modal-body .case .case-head h3 + p {
  max-width: 34em;
  margin: 12px 0 0;
  color: var(--modal-muted);
  text-wrap: pretty;
}

.modal-body .case .case-quote {
  max-width: 19em;
  margin: 0;
  color: inherit;
  font-size: var(--text-intro);
  line-height: 1.14;
  letter-spacing: var(--track-intro);
  text-wrap: balance;
}

.case-questions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--case-gap) 24px;
  max-width: 36em;
  margin: clamp(28px, 4vw, 48px) 0 0;
  padding: 0;
  list-style: none;
}

.case-questions li {
  padding-top: 12px;
  border-top: 1px solid rgba(0, 0, 0, 0.1);
  font-weight: 500;
}

.case-questions span,
.case-phases span {
  display: block;
  margin-bottom: 2px;
  color: var(--modal-muted);
  font-size: 14px;
  font-weight: 400;
}

.case-media {
  display: grid;
  gap: clamp(20px, 2.4vw, 28px) var(--case-gap);
}

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

.modal-body .case img {
  background: #eef4fb;
}

.case-video {
  position: relative;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  margin-bottom: var(--case-gap);
  background: #0b0b0c;
}

.case-video.is-loop {
  pointer-events: none;
}

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

.modal-body .case .case-note {
  margin: clamp(40px, 5vw, 64px) 0 clamp(20px, 2.4vw, 28px);
  padding-left: calc(25% + 18px);
}

.modal-body .case .case-note p {
  max-width: 22em;
  margin: 0;
  color: var(--modal-muted);
  font-size: var(--text-body);
  letter-spacing: var(--track-body);
  line-height: 1.6;
  text-wrap: balance;
}

.case-note strong {
  color: #000;
  font-weight: 500;
}

/* Alternatives side by side, with the one we chose marked. */
.case-options {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(24px, 3vw, 32px) var(--case-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}

.case-options h4 {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 12px 0 2px;
  font-size: 16px;
  font-weight: 500;
}

.case-options h4 span {
  padding: 2px 8px;
  border-radius: 99px;
  background: #e5f4ff;
  color: #007be5;
  font-size: 12px;
  line-height: 1.4;
}

.modal-body .case-options p {
  margin: 0;
  padding-right: 12px;
  color: var(--modal-muted);
  font-size: 14px;
  line-height: 1.45;
}

.case-options .is-chosen img {
  outline: 2px solid #0d99ff;
  outline-offset: -2px;
}

.case-phases {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 20px var(--case-gap);
  margin: clamp(24px, 3vw, 32px) 0 0;
  padding: 0;
  list-style: none;
  color: var(--modal-muted);
  font-size: 14px;
  line-height: 1.45;
}

.case-phases li {
  padding: 12px 16px 0 0;
  border-top: 1px solid rgba(0, 0, 0, 0.1);
}

.case-phases li:first-child {
  border-top-color: #000;
}

.case-phases strong {
  display: block;
  color: #000;
  font-size: 16px;
  font-weight: 500;
}

/* Tablet: the label moves above its text, and the alternatives become rows. */
@media (max-width: 900px) {
  .case-head {
    grid-template-columns: minmax(0, 1fr);
  }

  .modal-body .case .case-label {
    padding-top: 0;
  }

  .modal-body .case .case-note {
    padding-left: 0;
  }

  .case-options {
    grid-template-columns: minmax(0, 1fr);
  }

  .case-options li {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    column-gap: 20px;
  }

  .case-options img {
    grid-row: 1 / 3;
  }

  .case-options h4 {
    margin-top: 0;
  }
}

/* Phone: everything in one column. */
@media (max-width: 600px) {
  .case-facts,
  .case-questions {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .case-media.two,
  .case-phases {
    grid-template-columns: minmax(0, 1fr);
  }

  .case-options li {
    display: block;
  }

  .case-options h4 {
    margin-top: 12px;
  }
}

/* Grid clips stay invisible until the first frame, then ease in (filters.js). */
.anthology video {
  opacity: 0;
}

.anthology video.is-fading {
  transition: opacity 450ms cubic-bezier(0.23, 1, 0.32, 1);
}

.anthology video.is-ready {
  opacity: 1;
}

/* Family Trash, filmed: a live-action clip that fills its tile, cropped from the centre. */
/* Concept renders are on white: multiplied onto the tile, their white turns into the tile's grey,
   so the phone or object sits straight on the tile. Each is sized to the tile's height at its own
   shape (--ratio) and centred, so a clip can trim the recording itself: --crop takes off the thin
   grey line some exports have around their edge. */
.concept-video {
  display: block;
  position: absolute;
  /* --concept-y / --concept-ty pin the render (defaults: centred). */
  top: var(--concept-y, 50%);
  left: 50%;
  width: auto;
  /* --fit shrinks a render that would otherwise read larger than the grid's phones. */
  height: calc(100% * var(--fit, 1));
  max-width: none;
  aspect-ratio: var(--ratio, 1);
  translate: -50% var(--concept-ty, -50%);
  clip-path: inset(
    var(--crop-top, var(--crop, 0.5%))
    var(--crop-right, var(--crop, 0.5%))
    var(--crop-bottom, var(--crop, 0.5%))
    var(--crop-left, var(--crop, 0.5%))
  );
  mix-blend-mode: multiply;
}

.tile[data-id="furniture-visualization"] .concept-video {
  /* Same visual phone scale as 640-wide concept renders; trim export hairlines (top + bottom). */
  --fit: 0.8;
  --crop-top: 4.5%;
  --crop-bottom: 3.5%;
}

@media (max-width: 720px) {
  .tile[data-id="furniture-visualization"] .concept-video {
    --fit: 1;
  }
}

.tile-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
}

/* Locket thumbnail (Figma, 1203 x 640). Laid out in its own units, --k = one Figma pixel, and
   centred in the card's 1480 x 800 canvas (a touch taller), with the background filling the card. */
.tile-locket {
  /* The 640-tall frame fills the cover, so the phone and photos keep this layout at any size. */
  --k: calc(var(--cover-k) / 640);
  background: transparent;
}

/* The same gradient the bitmap was: cream across the top left, gold through the middle,
   the orange glow low and just right of centre, a pale cyan corner at the top right. */
.lt-bg {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(100% 74% at 56% 104%, rgba(255, 112, 40, 0.8) 0%, rgba(255, 168, 64, 0.42) 34%, transparent 70%),
    radial-gradient(88% 72% at 26% 42%, rgba(255, 214, 100, 0.82) 0%, transparent 64%),
    radial-gradient(40% 32% at 100% 0%, rgba(168, 236, 220, 0.95) 0%, transparent 58%),
    #fff6e2;
  pointer-events: none;
}

.lt-frame {
  position: absolute;
  top: calc(50% - 320 * var(--k));
  left: calc(50% - 601.5 * var(--k));
  width: calc(1203 * var(--k));
  height: calc(640 * var(--k));
}

/* Floating photo cards: --box is Figma's rotated bounding box at --x/--y, --size the card inside. */
.lt-card {
  position: absolute;
  top: calc((var(--y) + var(--box) / 2 - var(--size) / 2) * var(--k));
  left: calc((var(--x) + var(--box) / 2 - var(--size) / 2) * var(--k));
  width: calc(var(--size) * var(--k));
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: calc(16 * var(--k));
  rotate: var(--turn);
  isolation: isolate;
  /* Each card is tipped in 3D, leaning toward the phone, as if floating at different angles. */
  transform: perspective(calc(700 * var(--k))) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
  /* Figma's layered drop shadow, deepened a little so the cards float higher. */
  box-shadow:
    0 calc(76 * var(--k)) calc(80 * var(--k)) rgba(0, 0, 0, 0.11),
    0 calc(22.912 * var(--k)) calc(24.118 * var(--k)) rgba(0, 0, 0, 0.08),
    0 calc(9.516 * var(--k)) calc(10.017 * var(--k)) rgba(0, 0, 0, 0.06),
    0 calc(3.442 * var(--k)) calc(3.623 * var(--k)) rgba(0, 0, 0, 0.04);
}

/* The highlight is painted over the photo: the card is its own layer stack (isolation), the photo
   at the bottom and this edge of light above it (and above the weather pill). */
.lt-card::after {
  content: "";
  position: absolute;
  z-index: 3;
  inset: 0;
  border-radius: inherit;
  /* Inner shadow: x 0, y 2, blur 2, spread 0, white at 85%. */
  box-shadow: inset 0 calc(2 * var(--k)) calc(2 * var(--k)) rgba(255, 255, 255, 0.85);
  pointer-events: none;
}

.lt-card img {
  position: absolute;
  z-index: 0;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
}

/* The beach photo is anchored to its bottom edge, as in Figma. */
.lt-weather img {
  object-position: bottom;
}

/* The weather pill sits centred low on its photo, as in Figma (123 down a 180 card). */
.lt-pill {
  position: absolute;
  z-index: 1;
  top: calc(123 / 180 * 100%);
  left: 50%;
  translate: -50% 0;
  display: flex;
  align-items: center;
  gap: calc(6 * var(--k));
  padding: calc(8 * var(--k)) calc(12 * var(--k));
  border-radius: calc(48 * var(--k));
  background: linear-gradient(#2f9bfe, #62d1fa);
  color: #fff;
  font-size: calc(18 * var(--k));
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
  rotate: 0.17deg;
}

.lt-pill svg {
  width: calc(24 * var(--k));
  height: calc(24 * var(--k));
  fill: #ffd60a;
  stroke: #ffd60a;
}

/* Silver iPhone 16 Pros, rendered in 3D at their poses (assets/locket-thumb/phone-*.png, from a
   three.js model at true proportions, screens cut out). The render box is 420 x 700 design units,
   centred on Figma's --cx/--cy, and seen from 1800 units away; the screen is one element turned with
   the same rotation and perspective, lifted to the glass, so it lines up with the cut-out at any
   size. Model: 276 units wide (3.86 per mm), screen 67.7 x 145.8 mm with 9.7 mm corners, its glass
   16.87 units in front of the phone's centre. */
.iphone {
  position: absolute;
  top: calc((var(--cy) - 350) * var(--k));
  left: calc((var(--cx) - 210) * var(--k));
  width: calc(420 * var(--k));
  height: calc(700 * var(--k));
  perspective: calc(1800 * var(--k));
  /* Figma's phone is a little bigger than the render's 276 units; the render and its screen scale together. */
  scale: var(--scale, 1);
}

.iphone-plane {
  position: absolute;
  top: 50%;
  left: 50%;
  width: calc(261.33 * var(--k));
  height: calc(562.81 * var(--k));
  overflow: hidden;
  border-radius: calc(37.44 * var(--k));
  background: #000;
  translate: -50% -50%;
  transform: rotateZ(var(--turn-z)) rotateX(var(--turn-x)) rotateY(var(--turn-y)) translateZ(calc(16.87 * var(--k)));
}

/* A faint reflection on the glass, over whatever the screen shows. */
.iphone-plane::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(118deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.04) 30%, transparent 48%);
  pointer-events: none;
}

.iphone-screen {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
}

.iphone-render {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  pointer-events: none;
}

/* A soft shadow cast on the background, down and to the right. */
.iphone-shadow {
  position: absolute;
  top: 50%;
  left: 50%;
  width: calc(250 * var(--k));
  height: calc(560 * var(--k));
  border-radius: calc(44 * var(--k));
  background: rgba(70, 45, 0, 0.3);
  filter: blur(calc(28 * var(--k)));
  translate: -50% -50%;
  transform: translate(calc(20 * var(--k)), calc(28 * var(--k))) rotate(var(--turn-z));
}

/* About uses the project description text scale and the shared sky footer. */
.page > .site-footer[hidden] { display: none; }

.is-about .disco-ball, .is-about .disco-lights { display: none; }
.is-about .page { min-height: 100svh; }
.is-about .site-footer { flex: none; }
.about-page {
  flex: 1;
  width: 100%;
  margin-inline: 0;
  padding-block: clamp(64px, 9vw, 148px) clamp(32px, 5vw, 72px);
  color: #000;
  font-size: var(--text-body);
  font-weight: 400;
  letter-spacing: var(--track-body);
  line-height: 1.6;
}
.about-column {
  display: flex;
  flex-direction: column;
  gap: 40px;
}
.about-intro-row {
  display: flex;
  align-items: center;
  gap: clamp(24px, 4vw, 72px);
}
.about-intro {
  flex: 0 0 auto;
  width: min(100%, 437px);
  max-width: 437px;
  margin: 0;
  letter-spacing: 0;
  line-height: 1.6;
  text-wrap: pretty;
}
.about-intro p { margin: 0; }
.about-intro p + p { margin-top: 1.75em; }
/* Fills the row beside the intro; the portrait frame is centred inside it on wide screens. */
.about-portrait-box {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
}

/* The Julia print on desktop, smaller than the previous step, still at 393 × 425. */
.about-portrait {
  --u: calc(clamp(300px, 36vw, 460px) / 643);
  position: relative;
  flex: 0 0 auto;
  margin: 0;
  width: calc(393 * var(--u));
  height: calc(425 * var(--u));
  background: #fff;
  /* A finger paints the dashes. Without this the page scrolls and the shader never sees the drag. */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}
.about-portrait img,
.about-portrait canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.about-portrait img {
  z-index: 1;
  max-width: none;
  object-fit: cover;
  object-position: center bottom;
}
.about-portrait.is-pixelated img {
  visibility: hidden;
}
.about-experience { margin: 0; }
.experience-list { margin: 0; padding: 0; list-style: none; }
.experience-item {
  display: grid;
  grid-template-columns: 99px minmax(0, 1fr);
  gap: 48px;
  align-items: baseline;
  padding-block: 12px;
}
.experience-company, .experience-role, .experience-dates { margin: 0; font: inherit; }
.experience-dates { white-space: nowrap; text-align: left; color: rgba(0, 0, 0, 0.5); }
.disco .about-page { color: #fff; }
.disco .experience-dates { color: rgba(255, 255, 255, 0.6); }
@media (max-width: 720px) {
  .about-page { padding-top: 48px; }
  .about-intro-row {
    flex-direction: column;
    align-items: stretch;
    gap: 40px;
  }
  .about-intro {
    flex: none;
    width: auto;
    max-width: none;
  }
  .about-portrait-box {
    flex: none;
    width: 100%;
  }
  .about-portrait {
    width: min(340px, 92%);
    height: auto;
    aspect-ratio: 393 / 425;
    margin-inline: auto;
    align-self: center;
  }
}

/* Medium widths: smaller than the small-phone cap and the wide desktop print. */
@media (min-width: 481px) and (max-width: 720px) {
  .about-portrait {
    width: min(232px, 78%);
  }
}

@media (min-width: 721px) and (max-width: 1279px) {
  .about-portrait {
    --u: calc(clamp(240px, 30vw, 420px) / 643);
  }
}


/* Footer email (copy-email.js): "Email" like the other links; coolicons CopyCheck only after copy. */
.copy-email {
  position: relative;
}

.copy-label {
  transition: opacity 0.15s ease;
}

.copy-tip {
  position: absolute;
  z-index: 1;
  inset: -0.18em -0.45em;
  display: flex;
  align-items: center;
  justify-content: center;
  color: inherit;
  opacity: 0;
  scale: 0.96;
  pointer-events: none;
  transition: opacity 0.15s ease, scale 0.2s cubic-bezier(0.23, 1, 0.32, 1);
}

.copy-icon {
  display: block;
  width: 1.125em;
  height: 1.125em;
}

.copy-email.is-copied .copy-label {
  opacity: 0;
}

.copy-email.is-copied .copy-tip {
  opacity: 1;
  scale: 1;
}

