/* One family, two weights. Geist Medium is the body voice; SemiBold is the only
   emphasis. Instrument Sans, Inter and DM Sans used to appear in one rule each,
   which is what made the pages read as unrelated. */
@font-face {
  font-family: "Geist";
  src: url(/assets/fonts/b15a4c65593f0281.otf) format("opentype");
  font-weight: 500;
  font-display: swap;
}

@font-face {
  font-family: "Geist";
  src: url(/assets/fonts/99c2c8841c09530a.otf) format("opentype");
  font-weight: 600;
  font-display: swap;
}

:root {
  /* Colour */
  --paper: #fdfdfc;
  --ink: #21201c;
  --muted: rgba(99, 99, 94, 0.5);
  --purple: #7755cc;
  --line: rgba(33, 32, 28, 0.14);
  --rule: rgba(33, 32, 28, 0.12);
  --surface: #f2f2f2;
  --surface-media: #f1f0ed;
  --on-dark: #fff;
  --on-dark-muted: rgba(255, 255, 255, 0.72);

  /* Type scale. Redefined once at each breakpoint below, so components never
     restate a size and every page steps down together. */
  --text-xs: 13px;
  --text-sm: 14px;
  --text-base: 16px;
  --text-md: 20px;
  --text-lg: 26px;
  --text-xl: 32px;
  --text-display: clamp(58px, 8vw, 124px);

  --leading-tight: 1.2;
  --leading-snug: 1.3;
  --leading-body: 1.6;
  --tracking-tight: -1px;

  /* Spacing — 4px ramp. The old set skipped 12 and 20, which is why headings
     and their lists could not be spaced closer than the blocks around them. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 32px;
  --space-8: 40px;
  --space-9: 48px;
  --space-10: 64px;
  --space-11: 80px;
  --space-12: 120px;

  /* The page's outer margin. The header and the card grid both derive their
     edges from this, so they cannot fall out of alignment. */
  --gutter: 64px;

  /* Motion. The browser's built-in easings are too weak for deliberate
     movement, so these are the canonical strong curves. Entrances and exits
     take --ease-out; on-screen movement takes --ease-in-out. Never ease-in:
     it starts slow, holding back the exact moment the eye is on. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);

  /* Press feedback is the same everywhere something is pressable. */
  --press-scale: 0.97;
  --press-duration: 160ms;

  /* Radii */
  --radius-sm: 8px;
  --radius-md: 10px;
  --radius-lg: 16px;

  color-scheme: light;
  font-family: "Geist", Arial, sans-serif;
  font-weight: 500;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

* {
  box-sizing: border-box;
}

html {
  background: var(--paper);
  scroll-behavior: smooth;
}

body {
  margin: 0;
  min-width: 320px;
  background: var(--paper);
  color: var(--ink);
}

body:not(.route-home) {
  overflow-x: hidden;
}

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

/* Type hierarchy. h1 titles a page, h2 opens a section, h3 titles an entry,
   h4 is a run-in label. Weight carries emphasis; size carries level. */
h1,
h2,
h3,
h4 {
  margin: 0;
  color: var(--ink);
  font-weight: 500;
}

h1,
h2 {
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
}

h3 {
  font-size: var(--text-md);
  line-height: 1.25;
}

h4 {
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.4;
}

p {
  margin: 0;
}

strong {
  font-weight: 600;
}

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

a:hover,
a:focus-visible {
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

button,
input,
textarea {
  font: inherit;
}

.page-shell {
  width: min(1044px, calc(100% - 48px));
  margin-inline: auto;
}

/* Existing cellular-automata homepage */

body.route-home {
  height: 100vh;
  overflow: hidden;
}

.route-home #app {
  height: 100%;
}

.route-home > canvas {
  position: fixed;
  inset: 0;
  z-index: -1;
}

.landing {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  background: var(--paper);
}

.intro-frame {
  padding: var(--gutter);
}

.intro-title {
  padding-bottom: var(--space-2);
}

/* Typing animation on the work heading. The caret keeps the line box from
   collapsing while the text is empty mid-animation, so nothing below it jumps. */
.typed-caret {
  display: inline-block;
  width: 2px;
  height: 0.92em;
  margin-left: 2px;
  animation: caret-blink 1s steps(1, end) infinite;
  background: currentColor;
  vertical-align: -0.08em;
}

.typed-caret-done {
  animation: none;
  opacity: 0;
}

@keyframes caret-blink {
  0%,
  50% {
    opacity: 1;
  }
  50.01%,
  100% {
    opacity: 0;
  }
}

.intro-copy {
  color: var(--muted);
  font-size: var(--text-base);
  line-height: var(--leading-snug);
}

.intro-primary,
.intro-links {
  padding-bottom: var(--space-2);
}

.intro-copy a {
  color: var(--purple);
}

/* The back link grows an arrow on hover. It sits outside the link's own box,
   so it can fade in without nudging the row of links along. The short delay
   keeps it from flashing as the pointer crosses on its way somewhere else. */

.back-link {
  position: relative;
}

.back-link::before {
  content: '\2190';
  position: absolute;
  right: 100%;
  padding-right: 6px;
  opacity: 0;
  transform: translateX(3px);
  transition:
    opacity 150ms var(--ease-out) 90ms,
    transform 150ms var(--ease-out) 90ms;
  pointer-events: none;
}

.back-link:focus-visible::before {
  opacity: 1;
  transform: translateX(0);
}

@media (hover: hover) and (pointer: fine) {
  .back-link:hover::before {
    opacity: 1;
    transform: translateX(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .back-link::before {
    transform: none;
    transition: opacity 150ms ease 90ms;
  }
}

#threejs-container {
  /* Clickable: a press seeds a new colony. */
  cursor: pointer;
  width: 100%;
  height: 100%;
  min-height: 0;
  padding-top: 16px;
  overflow: hidden;
  border-radius: 5px;
}

#threejs-container canvas {
  display: block;
  max-width: 100%;
  border-radius: 5px;
}

#threejs-container.canvas-fallback {
  background:
    linear-gradient(135deg, rgba(119, 85, 204, 0.2) 25%, transparent 25%) 0 0 / 18px 18px,
    linear-gradient(315deg, rgba(119, 85, 204, 0.14) 25%, transparent 25%) 0 0 / 18px 18px,
    #fafaf7;
}

#cell-automata {
  padding-top: 8px;
}

/* Work index */

/* Sized by its own content — this used to be a fixed 240px (180px on mobile),
   which left a large gap above the first card once the copy got shorter. */
.work-header {
  width: 100%;
  margin: 0;
}

.work-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-6);
  /* No max width: capping it centred the grid on wide screens while the header
     stayed at the gutter, so the two drifted apart. The cards scale instead. */
  width: calc(100% - var(--gutter) * 2);
  margin: 0 auto var(--space-12);
}

.work-card {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: #efefec;
  color: var(--on-dark);
  isolation: isolate;
}

.work-card-visual {
  position: absolute;
  inset: 0;
  display: block;
}

.work-card img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transition: transform 200ms var(--ease-out);
}

.work-card-cover,
.work-card-background {
  object-fit: cover;
}

.work-card-background {
  z-index: 0;
}

.work-card-foreground {
  z-index: 1;
  object-fit: contain;
  transform-origin: center bottom;
}

/* Desktop screen + phone side by side on a flat backdrop. Used by the Copilot
   work card and its case hero, so both read as the same composition. */
.device-stage {
  position: absolute;
  z-index: 0;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--stage-bg, #000814);
}

.work-card .device-stage-desktop,
.work-card .device-stage-phone,
.case-hero .device-stage-desktop,
.case-hero .device-stage-phone {
  position: relative;
  inset: auto;
  height: auto;
  /* Keeps a margin of backdrop around the pair however the box is proportioned. */
  max-height: 78%;
  align-self: center;
  object-fit: contain;
}

/* Widths are set per context rather than on .device-stage-* alone: the
   surrounding `.work-card img` / `.case-hero img` rules already set width:100%
   at the same specificity, and would otherwise win and blow both images up. */

/* The card is square, so the pair has to run wide to fill it. */
.work-card .device-stage-desktop {
  width: 80%;
}

/* The phone rides over the right edge of the desktop screen, the way Copilot
   pairs them — side by side alone leaves too much dead space in a square card. */
.work-card .device-stage-phone {
  width: 22%;
  margin-left: -6%;
}

/* The hero is wide and short, so keep both devices well inside it. */
.case-hero .device-stage-desktop {
  width: 58%;
}

.case-hero .device-stage-phone {
  width: 14%;
  margin-left: -3%;
}

.case-hero-stage {
  position: relative;
  background: #000814;
}

/* Progressive blur. A single backdrop-filter behind a mask only fades the
   opacity of a blur that is already at full strength, which is why the old
   caption went from sharp to heavily blurred over a few pixels. Instead, stack
   layers whose blur radius doubles each step, each masked to start where the
   previous one finished — every layer blurs the output of the one behind it, so
   the strength itself ramps and the onset is invisible. */
.work-card-scrim {
  position: absolute;
  z-index: 2;
  right: 0;
  bottom: 0;
  left: 0;
  /* A quarter of the card. The scrim exists to seat the caption, not to
     compete with the artwork. */
  height: 27%;
  pointer-events: none;
  /* Perceptual ease-in rather than a straight line: a linear alpha ramp reads
     as a visible edge where it begins. */
  background: linear-gradient(
    to bottom,
    rgba(10, 10, 10, 0) 0%,
    rgba(10, 10, 10, 0.04) 26%,
    rgba(10, 10, 10, 0.13) 44%,
    rgba(10, 10, 10, 0.28) 60%,
    rgba(10, 10, 10, 0.45) 74%,
    rgba(10, 10, 10, 0.6) 87%,
    rgba(10, 10, 10, 0.7) 100%
  );
}

.work-card-scrim > span {
  position: absolute;
  inset: 0;
}

.work-card-scrim > span:nth-child(1) {
  backdrop-filter: blur(1.5px);
  -webkit-backdrop-filter: blur(1.5px);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 32%, #000 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 32%, #000 100%);
}

.work-card-scrim > span:nth-child(2) {
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  mask-image: linear-gradient(to bottom, transparent 24%, #000 56%, #000 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 24%, #000 56%, #000 100%);
}

.work-card-scrim > span:nth-child(3) {
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  mask-image: linear-gradient(to bottom, transparent 48%, #000 78%, #000 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 48%, #000 78%, #000 100%);
}

.work-card-scrim > span:nth-child(4) {
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  mask-image: linear-gradient(to bottom, transparent 70%, #000 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 70%, #000 100%);
}

.work-card-caption {
  position: absolute;
  z-index: 3;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  min-height: 108px;
  flex-direction: column;
  justify-content: flex-end;
  padding: 36px var(--space-6) var(--space-6);
}

.work-card-title {
  position: relative;
  z-index: 1;
  display: block;
  color: var(--on-dark);
  font-size: var(--text-lg);
  line-height: 1.1;
  letter-spacing: -0.5px;
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.55);
}

.work-card-year {
  color: var(--on-dark-muted);
}

.work-card:focus-visible img {
  transform: scale(1.025);
}

.work-card:focus-visible .work-card-background {
  transform: scale(1.04);
}

.work-card:focus-visible .work-card-foreground {
  transform: translateY(-1.25%) scale(1.025);
}

/* Touch reports a hover on tap and then leaves it stuck there, so the pointer
   version of this only runs where there is a real cursor. */
@media (hover: hover) and (pointer: fine) {
  .work-card:hover img {
    transform: scale(1.025);
  }

  .work-card:hover .work-card-background {
    transform: scale(1.04);
  }

  .work-card:hover .work-card-foreground {
    transform: translateY(-1.25%) scale(1.025);
  }
}

.work-card:focus-visible {
  outline: 3px solid var(--purple);
  outline-offset: 4px;
}

/* Press feedback. The interface should answer the finger going down, not wait
   for it to come up, and it answers on touch as well as with a cursor. */
.work-card {
  transition: transform var(--press-duration) var(--ease-out);
}

.work-card:active {
  transform: scale(0.99);
}

.work-card-placeholder {
  cursor: default;
  background:
    radial-gradient(circle at 78% 18%, rgba(119, 85, 204, 0.4), transparent 34%),
    linear-gradient(145deg, #d9d2ec 0%, #8d78bf 54%, #433568 100%);
}

a.work-card-placeholder {
  cursor: pointer;
}

.work-card-placeholder:nth-child(4) {
  background:
    radial-gradient(circle at 20% 18%, rgba(255, 255, 255, 0.52), transparent 30%),
    linear-gradient(145deg, #d8d4cc 0%, #9b9287 54%, #514b45 100%);
}

.work-card-placeholder-mark {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: rgba(255, 255, 255, 0.14);
  font-size: var(--text-display);
  letter-spacing: -5px;
}

.case-content a {
  color: var(--purple);
  text-decoration: none;
}


/* Case studies */

.case-page {
  width: min(800px, calc(100% - 48px));
  padding: var(--space-11) 0 var(--space-12);
}

/* Same shape as the home and work headers: title, grey subtitle, link row,
   all ranged left. */
.case-header {
  margin-bottom: var(--space-8);
}

.case-hero,
.case-media {
  margin: 0;
}

.case-hero {
  aspect-ratio: 1.74 / 1;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--surface-media);
}

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

.case-content {
  margin-top: var(--space-8);
  color: var(--muted);
  font-size: var(--text-base);
  line-height: var(--leading-body);
}

.case-page-text-only .case-content {
  margin-top: 0;
}

/* Vertical rhythm. A heading belongs to the content beneath it, so the space
   above it is always larger than the space below — that proximity is what makes
   the levels legible without extra weight or rules. */
.case-content h2 {
  margin: var(--space-10) 0 var(--space-4);
}

.case-content h3 {
  margin: var(--space-8) 0 var(--space-3);
}

.case-content h4 {
  margin: var(--space-7) 0 var(--space-2);
}

/* Media is a large visual block, so a heading that starts a new section after
   one needs more separation than it would after a paragraph. This replaces the
   empty spacer elements the case content used to carry. */
.case-content .case-media + :is(h2, h3, h4),
.case-content .media-pair + :is(h2, h3, h4),
.case-content figure + :is(h2, h3, h4) {
  margin-top: var(--space-10);
}

/* Whatever directly follows a heading is already spaced by that heading's
   bottom margin; without this the two margins stack and the heading drifts away
   from the very block it introduces. */
.case-content :is(h2, h3, h4) + :is(p, ul, .case-media, .dispatch-log) {
  margin-top: 0;
}

.case-content h4 a {
  color: inherit;
}

.case-content p {
  margin: var(--space-5) 0 0;
}

.case-content > :first-child {
  margin-top: 0;
}

.case-content ul {
  margin: var(--space-3) 0 0;
  padding-left: 20px;
}

.case-content li {
  padding-left: 0;
}

.case-content li + li {
  margin-top: var(--space-2);
}

.case-content li > ul {
  margin-top: 0;
}

.case-content strong {
  color: var(--ink);
}

/* Action buttons (app download / website), matching the product sites' pill shape */

.case-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

.case-content .case-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 14px 28px;
  border: 1px solid var(--ink);
  border-radius: 999px;
  background: var(--ink);
  color: var(--on-dark);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1;
  text-decoration: none;
  transition:
    opacity 160ms ease,
    background 160ms ease,
    transform var(--press-duration) var(--ease-out);
}

.case-content .case-action:active {
  transform: scale(var(--press-scale));
}

@media (hover: hover) and (pointer: fine) {
  .case-content .case-action:hover {
    opacity: 0.85;
  }
}

.case-content .case-action-secondary {
  border-color: var(--line);
  background: transparent;
  color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  .case-content .case-action-secondary:hover {
    background: var(--surface);
    opacity: 1;
  }
}

@media (max-width: 480px) {
  .case-actions {
    flex-direction: column;
    align-items: stretch;
  }
}

.case-media {
  width: min(var(--media-max, 100%), 100%);
  margin-top: var(--space-7);
}

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

.dispatch-log {
  margin-top: var(--space-7);
  border-left: 1px solid var(--rule);
  padding-left: var(--space-6);
}

.dispatch-entry {
  position: relative;
  margin-top: var(--space-10);
}

.dispatch-entry:first-child {
  margin-top: 0;
}

.dispatch-entry::before {
  content: "";
  position: absolute;
  top: 7px;
  left: -28px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--muted);
}

.case-content .dispatch-meta {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  margin: 0;
  font-size: var(--text-xs);
}

/* A quiet pill keeps the category from reading as a stray word next to the date. */
.dispatch-tag {
  padding: 2px var(--space-2);
  border-radius: 100px;
  background: var(--surface);
  color: var(--ink);
  font-weight: 600;
  line-height: 1.5;
}

.case-content .dispatch-entry h3 {
  margin: var(--space-3) 0 var(--space-3);
}

/* Rounding now comes from the image itself, along with every other case media. */
.dispatch-media {
  margin-top: var(--space-6);
}

@media (max-width: 700px) {
  .dispatch-log {
    padding-left: 18px;
  }

  .dispatch-entry::before {
    left: -22px;
  }
}

.media-scroll-strip {
  width: min(1200px, calc(100vw - 48px));
  margin-left: 50%;
  overflow-x: auto;
  transform: translateX(-50%);
  scrollbar-width: thin;
}

.media-narrow {
  width: min(650px, 100%);
  margin-right: auto;
}

.case-video video {
  background: transparent;
  object-fit: contain;
}

.case-embed {
  aspect-ratio: 16 / 9;
}

.case-embed iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

.media-pair {
  display: grid;
  gap: var(--space-7);
  width: min(520px, 100%);
  margin-top: var(--space-7);
}

.media-pair .case-media {
  margin-top: 0;
}

/* A media label captions the figure beneath it, so it binds downward like a
   heading rather than floating between the two. */
.case-content .media-label {
  margin-top: var(--space-7);
  color: var(--ink);
}

.case-content .media-label + .case-media {
  margin-top: var(--space-2);
}

.case-footer {
  margin-top: var(--space-12);
}

.not-found {
  display: grid;
  min-height: 100vh;
  align-content: center;
  justify-items: start;
}

.not-found p {
  color: var(--muted);
}

.not-found h1 {
  margin: var(--space-1) 0 20px;
}

.not-found a {
  color: var(--purple);
}

@media (max-width: 760px) {
  /* The scale steps down in one place. Components below only adjust layout —
     no rule restates a font size, so the pages stay in proportion with
     each other at every width. */
  :root {
    --text-xs: 12px;
    --text-sm: 13px;
    --text-base: 15px;
    --text-md: 18px;
    --text-lg: 22px;
    --text-xl: 24px;
    --tracking-tight: -0.5px;
  }

  .page-shell {
    width: calc(100% - 40px);
  }

  :root {
    --gutter: 24px;
  }

  .intro-frame {
    padding: var(--gutter);
  }

  .work-grid {
    grid-template-columns: 1fr;
    gap: var(--space-4);
    margin-bottom: 72px;
  }

  .work-card {
    border-radius: 12px;
  }

  .work-card-caption {
    min-height: 86px;
    padding: 30px 18px 18px;
  }

  .case-page {
    width: calc(100% - 64px);
    padding: var(--space-7) 0 var(--space-10);
  }

  .case-header {
    margin-bottom: var(--space-7);
  }

  .case-hero {
    aspect-ratio: 1.698 / 1;
  }

  .case-content {
    margin-top: var(--space-7);
  }

  .media-scroll-strip {
    width: calc(100vw - 24px);
  }

  .media-scroll-strip img {
    width: 1000px;
    max-width: none;
  }

  .media-pair {
    gap: var(--space-6);
    width: 100%;
  }

  .case-footer {
    margin-top: var(--space-11);
  }
}

/* The work grid arriving on first load. Cards land in sequence rather than
   all at once, 40ms apart — long enough to read as a sequence, short enough
   that the last card is not left waiting.

   Only on a cold load: a client-side navigation already has the route
   transition to explain itself, and running both at once is two ideas
   competing. `backwards` rather than `both`, so the finished animation does
   not keep hold of transform and block the press state. */

.work-grid[data-enter] .work-card {
  animation: card-enter 300ms var(--ease-out) backwards;
}

.work-grid[data-enter] .work-card:nth-child(2) { animation-delay: 40ms; }
.work-grid[data-enter] .work-card:nth-child(3) { animation-delay: 80ms; }
.work-grid[data-enter] .work-card:nth-child(4) { animation-delay: 120ms; }
.work-grid[data-enter] .work-card:nth-child(5) { animation-delay: 160ms; }
.work-grid[data-enter] .work-card:nth-child(6) { animation-delay: 200ms; }
.work-grid[data-enter] .work-card:nth-child(7) { animation-delay: 240ms; }
.work-grid[data-enter] .work-card:nth-child(n + 8) { animation-delay: 280ms; }

@keyframes card-enter {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
}

/* Route transitions

   The router swaps routes inside document.startViewTransition, so the browser
   snapshots the old page, renders the new one, and animates between them.
   Nothing here runs in browsers without the API — they just cut.

   The easing is the one Apple's sheets use: a fast start that settles slowly,
   which is what reads as "considered" rather than "sliding". */

/* One transition, identical on every route. An earlier version paired each
   work card with the case hero, which meant the animation started from
   wherever that card sat in the grid — left, right, or nowhere at all on the
   pages with no hero. Every route now moves the same way. */

::view-transition-group(root) {
  /* Left to itself this animates the size difference between two pages, so a
     short page following a tall one drifts. Pinned so only opacity and the
     lift below ever move. */
  animation: none;
}

::view-transition-image-pair(root) {
  isolation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  /* Replace the default cross-fade so the two can be timed separately. */
  animation: none;
  mix-blend-mode: normal;
  backface-visibility: hidden;
}

::view-transition-old(root) {
  animation: route-exit 140ms var(--ease-out) both;
}

::view-transition-new(root) {
  animation: route-enter 260ms var(--ease-out) both;
}

@keyframes route-exit {
  to {
    opacity: 0;
  }
}

@keyframes route-enter {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}

/* Image lightbox */

.case-content img,
.case-hero img {
  cursor: pointer;
}

.lightbox {
  position: fixed;
  z-index: 100;
  inset: 0;
  /* Flex, not grid: an auto-sized grid row gives the image no definite height
     to resolve max-height against, so tall images ignored it and overflowed off
     the top of the screen. A flex container has a definite height here. */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-10);
  opacity: 0;
  background: rgba(20, 20, 18, 0.52);
  backdrop-filter: blur(24px) saturate(0.85);
  -webkit-backdrop-filter: blur(24px) saturate(0.85);
  pointer-events: none;
  transition: opacity 220ms var(--ease-out);
}

.lightbox[data-open] {
  pointer-events: auto;
  opacity: 1;
}

.lightbox img {
  max-width: min(1200px, 100%);
  max-height: 100%;
  width: auto;
  height: auto;
  min-height: 0;
  object-fit: contain;
  border-radius: var(--radius-md);
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.45);
  cursor: default;
  transform: scale(0.98);
  transition: transform 220ms var(--ease-out);
}

.lightbox[data-open] img {
  transform: scale(1);
}

.lightbox-close {
  position: absolute;
  top: var(--space-6);
  right: var(--space-6);
  display: grid;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.16);
  color: var(--on-dark);
  cursor: pointer;
  place-items: center;
}

.lightbox-close svg {
  width: 20px;
  height: 20px;
}

.lightbox-close:hover,
.lightbox-close:focus-visible {
  background: rgba(255, 255, 255, 0.28);
}

.lightbox-close {
  transition:
    background 160ms ease,
    transform var(--press-duration) var(--ease-out);
}

.lightbox-close:active {
  transform: scale(var(--press-scale));
}

body.lightbox-open {
  overflow: hidden;
}

@media (max-width: 760px) {
  .lightbox {
    padding: var(--space-6);
  }

  .lightbox-close {
    top: var(--space-4);
    right: var(--space-4);
  }
}

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

  .typed-caret {
    display: none;
  }

  /* Reduced motion means fewer and gentler, not none. Opacity and colour still
     carry meaning — a button that stops acknowledging a press is a loss, not a
     courtesy — so those keep their transitions and only movement is removed. */
  *,
  *::before,
  *::after {
    transition-property: opacity, background-color, border-color, color, box-shadow !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }

  /* Remove the movement itself, not merely the transition to it. */
  .work-card:active,
  .work-card:hover img,
  .work-card:focus-visible img,
  .work-card:hover .work-card-background,
  .work-card:focus-visible .work-card-background,
  .work-card:hover .work-card-foreground,
  .work-card:focus-visible .work-card-foreground,
  .case-content .case-action:active,
  .lightbox-close:active,
  .lightbox img,
  .back-link::before {
    transform: none;
  }
}


/*# sourceMappingURL=main.8649afd6d5b8396a.css.map*/