/* ============================================================
   Victor Chaves — landing
   Traced from Figma · ItsVictorChaves · node 291:488.
   The comp frame is 1728 x 4544. --u is one frame pixel, so every
   coordinate in the markup is the comp's own number and the whole
   composition scales with the viewport. Below 900px the same elements
   reflow into a single column (the comp has no mobile frame).
   ============================================================ */

@font-face {
  font-family: 'Geist';
  src: url('../fonts/geist-variable-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Geist Mono';
  src: url('../fonts/geist-mono-variable-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
/* Nuvra is one of the projects, not part of the interface: it is only ever
   used to set its own specimen, and only once that row has been opened. It
   blocks rather than swaps, because a specimen shown in the wrong face for
   a moment is worse than one that arrives a moment late. */
@font-face {
  font-family: 'Nuvra';
  src: url('../fonts/nuvra-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

:root {
  --u: calc(100vw / 1728);          /* one comp pixel */

  --bg: #111111;
  --fg: #ffffff;
  --line: #ffffff;
  --glass: rgba(0, 0, 0, 0.62);
  --glass-soft: rgba(0, 0, 0, 0.48);
  --glass-light: rgba(234, 234, 234, 0.31);
  --tag-bg: #000000;
  --live: #2bff00;

  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --dur-fold: 620ms;
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
  --ease-fold: cubic-bezier(0.65, 0, 0.35, 1);   /* ease-in-out, both ends */
  /* A window opens with a little overshoot and shuts without one: coming out
     should feel like something springing open, going back like something
     being put away. */
  --ease-pop: cubic-bezier(0.22, 1.3, 0.36, 1);
  --ease-shut: cubic-bezier(0.4, 0, 0.9, 0.55);
  --dur-fast: 160ms;
  --dur-ui: 220ms;
}

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

html { overflow-x: clip; scroll-behavior: smooth; }
/* An overlay owns the screen while it is up. overflow:hidden on the body is
   not enough on iOS, so the page is taken out of the scroll flow altogether
   and put back where it was on release; main.js holds the offset. */
html.is-locked { overflow: hidden; }
html.is-locked body {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
  overflow: hidden;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  overflow-x: clip;
  background: var(--bg);
  color: var(--fg);
  font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}

img { display: block; max-width: none; }
ul { list-style: none; margin: 0; padding: 0; }
a { color: inherit; text-decoration: none; }
hr { border: 0; margin: 0; }

::selection { background: var(--live); color: #000; }
:focus-visible { outline: 2px solid var(--live); outline-offset: 3px; }
/* ...but only for the keyboard. A click already says where the reader is
   looking, and the page moves the caret on their behalf when it opens the menu
   or a window - which is what used to draw a ring nobody asked for. main.js
   says which hand is on the page. */
html[data-input="pointer"] :focus-visible { outline: none; }
* { scrollbar-color: rgba(255, 255, 255, 0.28) transparent; scrollbar-width: thin; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: var(--bg); }
*::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.22); border: 3px solid var(--bg); border-radius: 99px; }

.u-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip-link {
  position: fixed; left: 50%; top: 0; z-index: 100;
  transform: translate(-50%, -140%);
  padding: 0.75rem 1.25rem; background: var(--fg); color: #000;
  font-size: 0.85rem; text-transform: uppercase;
  transition: transform var(--dur-ui) var(--ease-out);
}
/* The skip link is a keyboard affordance and nothing else: it comes down only
   when someone is actually moving with Tab. A touch or a click never brings it
   out - on a phone there is no Tab to skip with, and it was turning up on its
   own over the top of the page. main.js says which hand is on the page. */
html[data-input="key"] .skip-link:focus-visible { transform: translate(-50%, 0); }

/* ---------- The traced canvas ----------
   Everything below is placed with the comp's own coordinates:
   --x / --y position, --w / --h size, --fs / --lh type. A missing
   variable makes its declaration invalid, which falls back to auto
   for size and to the inherited value for type — exactly what nested
   elements want. */

.frame {
  position: relative;
  width: calc(1728 * var(--u));
  /* Each page states its own comp height on .frame, so one canvas serves
     several traced pages; the fallbacks are the landing's. */
  height: calc((var(--frame-h, 4484) + var(--push, 0) + var(--trim, 0)) * var(--u));
  /* The canvas is laid out once per fold, not once per frame: everything that
     moves is transformed, so the frame only has to own the scroll height. It
     grows at once and shrinks after the fold, so the ground is never missing
     from under the footer. */
  transition: height 0s linear var(--dur-fold);
}
.frame.has-open, .frame.is-untrimming { transition-delay: 0s; }
/* Hint the compositor only while a fold is in flight: these are large layers
   and promoting them for good would cost memory on a phone for nothing. */
.frame.is-folding .row,
.frame.is-folding .shift { will-change: transform; }

.pill, .pill__logo, .pill__current,
.btn, .btn__label, .btn__icon,
.t, .chips, .chips li, .tags, .tag, .thumbs, .thumbs img, .thumbs__slot, .thumbs__video,
.row__toggle, .row__shots,
.rule, .hero__bg, .band, .card, .marquee, .footer__dot {
  position: absolute;
  left: calc(var(--x, 0) * var(--u));
  top: calc(var(--y, 0) * var(--u));
  width: calc(var(--w) * var(--u));
  height: calc(var(--h) * var(--u));
}

.t {
  margin: 0;
  font-size: calc(var(--fs) * var(--u));
  line-height: calc(var(--lh) * var(--u));
  height: auto;
}

.rule {
  top: calc((var(--y) - 1) * var(--u));
  width: calc(1728 * var(--u));
  height: max(1px, calc(1 * var(--u)));
  background: var(--line);
}

/* ---------- Navigation ---------- */

.nav {
  position: fixed;
  left: 0; top: 0;
  width: calc(1728 * var(--u));
  height: 0;
  z-index: 60;
  transition: opacity 260ms var(--ease-out);
}
/* The bar stays live over an open window: the menu unfolds on top of it,
   with its own scrim, and the window is still there underneath when the menu
   closes again. */

.nav__scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  -webkit-backdrop-filter: blur(2.65px);
  backdrop-filter: blur(2.65px);
  opacity: 0;
  transition: opacity 320ms var(--ease-out);
}
.nav__scrim[hidden] { display: none; }
.nav__scrim.is-visible { opacity: 1; }

/* White type in difference blending reads black on light ground and white on
   dark, so the CTA stays legible over the sky and over the page below it. */
.nav__cta {
  position: fixed;
  z-index: 51;
  left: calc(var(--x) * var(--u));
  top: calc(var(--y) * var(--u));
  height: calc(var(--h) * var(--u));
  display: flex;
  align-items: center;
  gap: calc(14.016 * var(--u));
  padding-inline: calc(25.484 * var(--u));
  color: #fff;
  mix-blend-mode: difference;
  text-transform: uppercase;
  white-space: nowrap;
}
.nav__cta-label { font-size: calc(var(--fs) * var(--u)); line-height: calc(var(--lh) * var(--u)); }
.nav__cta-arrow { width: calc(24 * var(--u)); height: calc(24 * var(--u)); flex: none; color: currentColor; }
.nav__cta::after {
  content: ""; position: absolute; left: calc(25.484 * var(--u)); right: calc(45 * var(--u));
  bottom: calc(12 * var(--u)); height: 1px; background: currentColor;
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-ui) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .nav__cta:hover::after { transform: scaleX(1); }
}

.pill {
  left: 50%;
  top: calc(var(--y) * var(--u));
  transform: translateX(-50%);
  width: calc(354 * var(--u));
  height: calc(46 * var(--u));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(26 * var(--u));
  padding: calc(11 * var(--u)) calc(18 * var(--u));
  border-radius: calc(5 * var(--u));
  background: var(--glass);
  -webkit-backdrop-filter: blur(calc(3.3 * var(--u)));
  backdrop-filter: blur(calc(3.3 * var(--u)));
  overflow: hidden;
  /* Width is the comp's; height is measured and set by the script, so the
     panel unfolds to exactly its content at any scale. */
  transition: width 460ms var(--ease-drawer), height 460ms var(--ease-drawer);
}
.pill.is-open { width: calc(521 * var(--u)); }

/* Line breaks that belong to one canvas only. */
.u-mob { display: none; }

.pill__row {
  display: flex;
  align-items: center;
  width: 100%;
  height: calc(24 * var(--u));
  flex: none;
}
.pill__logo, .pill__toggle { position: static; flex: none; }
.pill__logo img { width: 100%; height: 100%; }

.pill__current {
  position: static;
  flex: 1;
  height: auto;
  text-align: center;
  white-space: nowrap;
  font-size: calc(var(--fs) * var(--u));
  line-height: calc(var(--lh) * var(--u));
  transition: opacity 140ms ease;
}
.pill__current.is-swapping { opacity: 0; }

/* The toggle: three strokes that converge into one. */
.pill__toggle {
  width: calc(24 * var(--u));
  height: calc(24 * var(--u));
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.burger { display: block; width: 100%; height: 100%; }
.burger line {
  stroke: var(--fg);
  stroke-width: 2;
  stroke-linecap: round;
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 340ms var(--ease-out), opacity 220ms ease;
}
.pill.is-open .burger__top { transform: translateY(6px) scaleX(0.55); opacity: 0; }
.pill.is-open .burger__bot { transform: translateY(-6px) scaleX(0.55); opacity: 0; }
.pill.is-open .burger__mid { transform: scaleX(0.9); }

/* Links: full width of the panel, so their rules bleed past its padding. */
.pill__links {
  width: calc(100% + 36 * var(--u));
  margin-inline: calc(-18 * var(--u));
  flex: none;
}
.pill__links li { padding: calc(10 * var(--u)) calc(18 * var(--u)); }
.pill__links li:not(:last-child) {
  padding-bottom: calc(16 * var(--u));   /* the 1px rule completes the comp's 50 */
  border-bottom: 1px solid rgba(255, 255, 255, 0.38);
}
.pill__links li + li { margin-top: calc(10 * var(--u)); }
.pill__links a {
  display: block;
  font-family: 'Geist', system-ui, sans-serif;
  font-weight: 500;
  font-size: calc(18 * var(--u));
  line-height: calc(23 * var(--u));
  text-align: center;
  transition: color var(--dur-fast) ease;
}

.pill__actions {
  display: flex;
  gap: calc(10 * var(--u));
  width: 100%;
  flex: none;
}
/* Outlined, not filled: the comp now draws these as a hairline box over the
   panel's own glass, so they read as the landing's buttons do. Nothing but the
   stroke - the comp's backdrop blur is left off here, because the panel
   already blurs what is behind it and blurring a second time over its own
   translucent black darkened the box. The arrow is white with them, and
   inverts with the rest on hover. */
.pill__btn {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(14.016 * var(--u));
  padding: calc(11.468 * var(--u)) calc(25.484 * var(--u));
  border: 1px solid #fafafa;
  border-radius: calc(6.371 * var(--u));
  color: #fafafa;
  font-size: calc(15 * var(--u));
  line-height: calc(20 * var(--u));
  text-transform: uppercase;
  white-space: nowrap;
  transition: background var(--dur-ui) ease, color var(--dur-ui) ease,
              border-color var(--dur-ui) ease, transform var(--dur-fast) var(--ease-out);
}
.pill__btn img { width: calc(24 * var(--u)); height: calc(24 * var(--u)); flex: none; }
.pill__btn:active { transform: scale(0.98); }

/* The contents arrive as the panel opens, and leave at once when it closes. */
.pill__links li, .pill__actions {
  opacity: 0;
  transform: translateY(calc(10 * var(--u)));
  transition: opacity 240ms var(--ease-out), transform 240ms var(--ease-out);
}
.pill.is-open .pill__links li,
.pill.is-open .pill__actions { opacity: 1; transform: none; }
.pill.is-open .pill__links li:nth-child(1) { transition-delay: 130ms; }
.pill.is-open .pill__links li:nth-child(2) { transition-delay: 175ms; }
.pill.is-open .pill__links li:nth-child(3) { transition-delay: 220ms; }
.pill.is-open .pill__actions { transition-delay: 265ms; }

@media (hover: hover) and (pointer: fine) {
  .pill__links a:hover { color: var(--live); }
  .pill__btn:hover { background: var(--fg); color: #000; border-color: var(--fg); }
  .pill__btn:hover img { filter: invert(1); }
}

/* Buttons keep the comp's box and the comp's inner coordinates. */
.btn {
  display: block;
  border-radius: calc(6.371 * var(--u));
  border: 1px solid transparent;
  transition: background var(--dur-ui) ease, border-color var(--dur-ui) ease, transform var(--dur-fast) var(--ease-out);
}
.btn__label {
  height: auto;
  font-size: calc(var(--fs) * var(--u));
  line-height: calc(var(--lh) * var(--u));
  text-align: center;
  text-transform: uppercase;
  white-space: nowrap;
}
.btn--glass {
  background: var(--glass-light);
  -webkit-backdrop-filter: blur(calc(4.205 * var(--u)));
  backdrop-filter: blur(calc(4.205 * var(--u)));
}
.btn--outline { border-color: var(--fg); -webkit-backdrop-filter: blur(calc(4.205 * var(--u))); backdrop-filter: blur(calc(4.205 * var(--u))); }
.btn--outline-soft { border-color: rgba(255, 255, 255, 0.46); -webkit-backdrop-filter: blur(calc(4.205 * var(--u))); backdrop-filter: blur(calc(4.205 * var(--u))); }
.btn:active { transform: scale(0.98); }

@media (hover: hover) and (pointer: fine) {
  .pill__toggle:hover .burger line { stroke: var(--live); }
  .btn--glass:hover { background: rgba(234, 234, 234, 0.44); }
  /* Every outlined button here fills white and flips its ink, the way the
     row's View Project already did; a faint wash read as nothing. */
  .btn--outline:hover, .btn--outline-soft:hover {
    background: var(--fg); color: #000; border-color: var(--fg);
  }
  .btn--outline:hover .btn__icon, .btn--outline-soft:hover .btn__icon { filter: invert(1); }
}

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

.hero__bg {
  object-fit: cover;
  z-index: -2;
  transform: scale(1.05);
  animation: heroSettle 1400ms var(--ease-out) 80ms forwards;
}
@keyframes heroSettle { to { transform: scale(1); } }
.label { text-transform: uppercase; }
.intro { font-family: 'Geist', system-ui, sans-serif; font-weight: 400; }
.hero__title { font-weight: 400; text-transform: uppercase; }
.hero__sub { text-align: right; text-transform: uppercase; }
.band { object-fit: cover; }

.chips li { --y: 0; }
.chips--hero li {
  opacity: 0;
  transform: translateY(calc(8 * var(--u))) scale(0.96);
  animation: chipIn 420ms var(--ease-out) forwards;
}
.chips--hero li:nth-child(1) { animation-delay: 560ms; }
.chips--hero li:nth-child(2) { animation-delay: 640ms; }
.chips--hero li:nth-child(3) { animation-delay: 720ms; }
@keyframes chipIn { to { opacity: 1; transform: none; } }
.chip {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%;
  border-radius: calc(4.673 * var(--u));
  background: var(--glass-soft);
  -webkit-backdrop-filter: blur(calc(3.037 * var(--u)));
  backdrop-filter: blur(calc(3.037 * var(--u)));
  font-size: calc(12.08 * var(--u));
  line-height: calc(16 * var(--u));
  text-transform: uppercase;
  transition: background var(--dur-fast) ease;
}
@media (hover: hover) and (pointer: fine) { .chip:hover { background: rgba(255, 255, 255, 0.16); } }
/* The filter's chips are buttons, not links, so they have to be told to look
   like the rest of their family. Pressed, one inverts: the page is showing that
   category and nothing else, which is worth more than a tint. */
button.chip {
  margin: 0; padding: 0;
  border: 0;
  color: inherit;
  /* Not `font: inherit`: the shorthand also resets size and line-height, and
     it out-specifies .chip, which is where the comp's 12.08 lives. Only the
     family and weight have to be taken back off the UA sheet. */
  font-family: inherit;
  font-weight: inherit;
  letter-spacing: inherit;
  cursor: pointer;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
button.chip[aria-pressed="true"] { background: var(--fg); color: #000; }
@media (hover: hover) and (pointer: fine) {
  button.chip[aria-pressed="true"]:hover { background: var(--fg); }
}
.chip:focus-visible { outline: 2px solid var(--live); outline-offset: calc(2 * var(--u)); }

/* ---------- Featured work ---------- */

/* Each row is a zero-height layer whose origin sits on the frame's, so its
   children keep the comp's absolute coordinates. Because the layer coincides
   with the frame, a transform on it moves the whole row without disturbing
   where its absolutely positioned children resolve. */
.row {
  position: absolute;
  left: 0; top: 0;
  width: calc(1728 * var(--u));
  height: 0;
  /* --ry is where the filter has put this row: the distance from its own place
     in the comp to the place it takes once the rows above it have been hidden.
     It is zero with no filter on, so the comp's own coordinates stand. */
  transform: translate3d(0, calc(var(--ry, 0) * var(--u)), 0);
  transition: transform var(--dur-fold) var(--ease-fold), opacity 240ms ease;
}
.row.is-open ~ .row { transform: translate3d(0, calc((var(--ry, 0) + var(--push, 0)) * var(--u)), 0); }
/* Filtered out: gone from the page and from the tab order, but still a row, so
   the ones that stay can slide over the space it leaves. */
.row.is-filtered {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 200ms ease, visibility 0s linear 200ms;
}

/* Everything that lives below the list rides the same push, and rises by
   whatever the filter has taken out of the list above it. */
.shift {
  transition: transform var(--dur-fold) var(--ease-fold);
  transform: translate3d(0, calc((var(--push, 0) + var(--trim, 0)) * var(--u)), 0);
}
.shift-layer { position: absolute; left: 0; top: 0; width: calc(1728 * var(--u)); height: 0; }

.row__name { font-family: 'Geist', system-ui, sans-serif; transition: color var(--dur-ui) ease; }
/* The header parts are decorative; the toggle underneath them takes the click. */
.row__name, .tags, .thumbs { pointer-events: none; }

.row__toggle {
  z-index: 2;
  margin: 0; padding: 0;
  background: transparent;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-ui) ease;
}
.row__toggle:focus-visible { outline: 2px solid var(--live); outline-offset: calc(-4 * var(--u)); }

.row__plus { text-transform: uppercase; white-space: nowrap; transition: color var(--dur-ui) ease; }

/* "( + )" grows into "( VIEW + )" on hover. Both strings are monospace, so the
   word occupies exactly five characters and the brackets never move. */
.row__view {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  max-width: 0;
  opacity: 0;
  transition: max-width 340ms var(--ease-out), opacity 200ms ease;
}
.row__sign { position: relative; display: inline-block; }
.row__sign span { display: block; transition: opacity 200ms ease, transform 360ms var(--ease-out); }
.row__sign span + span { position: absolute; left: 0; top: 0; opacity: 0; transform: rotate(-90deg); }

.tag {
  --y: 0;
  display: flex; align-items: center; justify-content: center;
  padding-top: calc(2 * var(--u));
  background: var(--tag-bg);
  font-size: calc(10.34 * var(--u));
  line-height: calc(13 * var(--u));
  text-transform: uppercase;
}
.thumbs img { --y: 0; object-fit: cover; background: #d9d9d9; transition: opacity var(--dur-ui) ease; }
/* A slot whose piece is a film plays it rather than showing one frame of it.
   The poster is that clip's own first frame, so the still and the loop agree
   and nothing jumps when the file arrives. */
.thumbs__video { --y: 0; object-fit: cover; background: #d9d9d9; display: block; }
.row__track video { display: block; width: 100%; height: 100%; object-fit: cover; }
/* Two boxes in the projects comp are drawn empty - the picture for them has
   not been shot yet - so the slot is kept rather than closed up: the strip
   keeps the comp's rhythm and the image drops straight in when it arrives. */
.thumbs__slot, .shot--slot { background: #d9d9d9; }
.thumbs { transition: transform var(--dur-ui) var(--ease-out), opacity 120ms ease 480ms; }

/* ---------- A row, expanded ---------- */

/* The panel sits above the toggle so its button and its scroller stay
   reachable, but only those two take pointer events - a click on the
   description still folds the row. */
/* Positioned, or its z-index would be inert and the toggle underneath would
   keep taking the clicks meant for the button. The panel carries no offsets,
   so its origin still coincides with the frame's and its absolutely
   positioned children keep the comp's coordinates. */
.row__panel {
  position: relative;
  z-index: 3;
  pointer-events: none;
  visibility: hidden;
  transition: visibility 0s linear var(--dur-fold);
}
.row__cta, .row__shots { pointer-events: auto; }
.row__desc { font-family: 'Geist', system-ui, sans-serif; }
/* The button runs at 0.70 of the comp, so its corner does too. It is the one
   thing in an open row you can actually press, so it inverts on hover rather
   than taking the outline buttons' faint wash. */
.row__cta {
  border-radius: calc(4.46 * var(--u));
  transition: background var(--dur-ui) ease, color var(--dur-ui) ease,
              border-color var(--dur-ui) ease, transform var(--dur-fast) var(--ease-out);
}
.row__cta .btn__icon { transition: filter var(--dur-ui) ease; }
.row__cta:focus-visible { outline: 2px solid var(--live); outline-offset: calc(3 * var(--u)); }

.row__desc, .row__cta {
  opacity: 0;
  transform: translateY(calc(14 * var(--u)));
  transition: opacity 260ms ease, transform var(--dur-fold) var(--ease-fold);
}

/* While a shot is in flight the scroller has to reach --lead pixels above the
   strip, up to the thumbnail row, so the shot can start on its own thumbnail
   without being clipped. That band lies over the row header, so until the
   fold lands the scroller takes no pointer events at all.

   Once it lands the scroller shrinks onto the strip - its floor does not move,
   since the track is bottom-aligned - and takes them back. It has to: a pan is
   given to the scroll container under the finger, and WebKit will not hand it
   to one that is pointer-events:none, however reachable its contents are. That
   is why this only ever worked in Chromium. */
.row__shots {
  --lead: 206;
  --strip: 470;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  overscroll-behavior-y: auto;
  touch-action: pan-x pan-y;
  pointer-events: none;
  /* The track is laid on the scroller's floor rather than stretched over it:
     stretched, its empty upper band would sit on top of the button. */
  display: flex;
  align-items: flex-end;
}
.row.is-settled .row__shots {
  top: calc((var(--y) + var(--lead)) * var(--u));
  height: calc(var(--strip) * var(--u));
  pointer-events: auto;
}
.row__shots::-webkit-scrollbar { display: none; }
.row__track {
  display: flex;
  align-items: flex-end;
  flex: none;
  gap: calc(33.571 * var(--u));
  width: max-content;
  height: calc(470 * var(--u));
  margin: 0;
  padding: 0 calc(40 * var(--u)) 0 0;
  list-style: none;
}
/* Each shot begins as its own thumbnail - --dx/--dy carry the distance between
   the two boxes and --s the ratio of their heights, both from the comp - and
   travels down into the strip as it grows. The thumbnail fades under it over
   the same instant, so what you see is one picture moving, not two swapping. */
.row__track li {
  flex: none;
  width: calc(var(--w) * var(--u));
  height: calc(470 * var(--u));
  opacity: 0;
  transform-origin: 0 0;
  transform: translate3d(calc(var(--dx) * var(--u)), calc(var(--dy) * var(--u)), 0) scale(var(--s));
  transition: opacity 120ms ease 500ms, transform var(--dur-fold) var(--ease-fold);
}
/* No placeholder colour: a shot begins exactly over its own thumbnail, so
   until its bytes arrive the thumbnail underneath is what shows through. */
.row__track img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* ---------- A row whose strip is a typeface ----------
   Nuvra is a typeface, so its row opens on the face itself rather than on
   photographs: the whole alphabet, upper and lower case, set in Nuvra and
   scrolled like any other row's strip. A letter has no thumbnail to fly from,
   so instead of the flight it rises onto the line, one after the next.

   The letters carry no --w/--dx/--s, so the track's own declarations for them
   are invalid and fall back to auto and none - which is exactly what a line of
   type wants. What is left to say is the size, the line it sits on, and the
   rise. */
.row--type .row__track { gap: 0; }
/* Closed, the strip is the alphabet rather than photographs of it: one line at
   124, running off the frame's right edge exactly as the comp draws it. The
   comp lets every row's strip bleed and be cut; this one is no different. */
.thumbs--type {
  font-family: 'Nuvra', 'Geist', serif;
  font-size: calc(124 * var(--u));
  /* The line sits below its own font-size to put the baseline where the comp
     has it, 115 under the strip's top. */
  line-height: calc(137 * var(--u));
  color: var(--fg);
  white-space: nowrap;
  /* No clip of its own. The frame is the full width of the window here, so the
     page already cuts the line on the frame's edge - and it keeps cutting it
     there while the strip slides on hover, which a clip carried by the strip
     would not: that one would travel with it and open a gap at the edge. */
}
.row__track .glyph {
  width: auto;
  font-family: 'Nuvra', 'Geist', serif;
  /* 400 puts a capital at 300 of the strip's 470 and leaves the same 35 of air
     over the capitals as under the descenders: the line is centred on its ink,
     not on the face's own box, which sits low because of the descent. */
  font-size: calc(400 * var(--u));
  line-height: calc(370 * var(--u));
  height: calc(470 * var(--u));
  color: var(--fg);
  transform: none;
}
/* A photograph flies from its own thumbnail; the alphabet has no thumbnails to
   fly from, but closed and open it is the same line of the same face at two
   sizes - so the whole strip makes the flight at once, one transform instead of
   fifty-two. --ts is the ratio of the two sizes, --tdx/--tdy the distance
   between the two lines' origins. The letters only fade in behind it. */
.row--type .row__track {
  transform-origin: 0 0;
  transform: translate3d(calc(var(--tdx) * var(--u)), calc(var(--tdy) * var(--u)), 0) scale(var(--ts));
  transition: transform var(--dur-fold) var(--ease-fold);
}
.row--type.is-open .row__track { transform: none; }
/* The specimen is this row's strip, so the project's own photographs come after
   the alphabet rather than instead of it: scroll past the z and the work is
   there. They ride the strip's one transform like the letters do - they carry
   no --dx of their own, so the track's flight declaration is invalid for them
   and falls back to none - and only want the gutter the letters do without. */
.row--type .row__track li.more { margin-left: calc(33.571 * var(--u)); }
.row.is-open .row--type .row__track li.more,
.row--type.is-open .row__track li.more { transition-delay: 300ms; }
.row.is-open .row__track .glyph {
  transition: opacity 200ms ease calc(var(--i) * 18ms);
}

.row.is-open .row__panel { visibility: visible; transition-delay: 0s; }
.row.is-open .row__desc,
.row.is-open .row__cta { opacity: 1; transform: none; transition-delay: 120ms; }
.row.is-open .row__track li {
  transform: none;
  transition: opacity 120ms ease calc(var(--i) * 55ms),
              transform var(--dur-fold) var(--ease-fold) calc(var(--i) * 55ms);
}
/* Opacity is the one thing that waits for the images: on a cold click the
   thumbnails simply stay until the shots can take over. */
.row.is-open.is-handed .row__track li { opacity: 1; }
.row.is-open.is-handed .thumbs { opacity: 0; transition: opacity 120ms ease 120ms; }
.row.is-open .row__sign span:first-child { opacity: 0; transform: rotate(90deg); }
.row.is-open .row__sign span + span { opacity: 1; transform: none; }

@media (hover: hover) and (pointer: fine) {
  .row:not(.is-open):has(.row__toggle:hover) .row__view { max-width: 5ch; opacity: 1; }
  .row:has(.row__toggle:hover) .row__plus { color: var(--live); }
  .row:not(.is-open):has(.row__toggle:hover) .thumbs { transform: translateX(calc(-18 * var(--u))); }
  .row:has(.row__toggle:hover) .row__name { color: var(--live); }
  /* Wins over .btn--outline:hover, which sits earlier in this file. */
  .row__cta:hover { background: var(--fg); color: #000; border-color: var(--fg); }
  .row__cta:hover .btn__icon { filter: invert(1); }
}

/* ---------- Everything I can do ---------- */

.services__head { margin: 0; font-weight: 400; }
/* The service lists, one block per discipline. */
/* A zero-height layer on the desktop canvas, a sideways rail on the phone. */
.cards { position: absolute; left: 0; top: 0; width: calc(1728 * var(--u)); height: 0; }
.card {
  padding: calc(24 * var(--u)) calc(26.5 * var(--u));
  height: auto;
}
.card__head {
  margin: 0;
  font-size: calc(var(--fs) * var(--u));
  line-height: calc(var(--lh) * var(--u));
  text-transform: uppercase;
}
.card__list {
  margin-top: calc(33.75 * var(--u));   /* the comp's blank line */
  font-size: calc(var(--fs) * var(--u));
  line-height: calc(var(--lh) * var(--u));
  text-transform: uppercase;
  width: calc(266 * var(--u));
}
.card--nowrap .card__list { width: auto; white-space: nowrap; }
.card__list li { transition: color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .card__list li:hover { color: var(--live); transform: translateX(calc(4 * var(--u))); }
}

.ai-note { font-family: 'Geist', system-ui, sans-serif; }
.display {
  font-family: 'Geist', system-ui, sans-serif;
  font-weight: 700;
  text-transform: uppercase;
  height: auto;
}


/* ---------- A project's own page ----------
   A column of writing on the left and the work on the right, piece under
   piece, butted edge to edge the way the comp has them. The writing holds
   still while the work goes past: the block is given the mosaic's whole
   height to live in and sticks to the top inside it, so it comes to rest
   exactly as the last piece arrives. */

.project__info {
  position: absolute;
  left: 0;
  top: calc(var(--y) * var(--u));
  width: calc(1728 * var(--u));
  height: calc(var(--h) * var(--u));
  pointer-events: none;
}
.project__info-in {
  position: sticky;
  /* clear of the bar, which is 26 to 72 */
  top: calc(96 * var(--u));
  /* the writing's own offset, as padding rather than a margin: a margin on the
     first child would collapse through and take the whole block down with it,
     carrying the title away from the top the comp gives it. */
  padding-top: calc(190 * var(--u));
}
.project__info-in > * { pointer-events: auto; }
/* The block states its own --h and --w, and custom properties inherit: without
   this the writing inside would take the block's height for its own on the
   phone canvas, where the shared rule reads --h rather than --mh. Anything
   that carries its own numbers inline still wins over this. */
.project__info-in > * { --h: initial; --mh: initial; --w: initial; --mw: initial; }

.project__title { font-family: 'Geist', system-ui, sans-serif; font-weight: 700; height: auto; }
.project__kicker { font-family: 'Geist', system-ui, sans-serif; height: auto; }
/* The writing and the line under it are one column in normal flow. The comp
   sets that line 72 below the text, and a paragraph more or less has to push
   it down rather than be written over it. */
/* In normal flow, not placed: the block that holds it can then be as tall as
   the writing turns out to be, and still stick. The comp's own numbers become
   the margins that put it where it goes. */
.project__text {
  margin-left: calc(57 * var(--u));
  width: calc(635 * var(--u));
  font-family: 'Geist', system-ui, sans-serif;
  font-size: calc(var(--fs) * var(--u));
  line-height: calc(var(--lh) * var(--u));
}
/* The first paragraph's own margin collapses out through the body and the
   column and takes the whole block down with it, so the paragraphs carry no
   margin of their own and a blank line between them is stated here. */
.project__body p { margin: 0; }
.project__body p + p { margin-top: calc(var(--lh) * var(--u)); }
.project__foot {
  position: relative;
  margin-top: calc(72 * var(--u));
  height: calc(25 * var(--u));
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: calc(20 * var(--u));
  line-height: calc(25 * var(--u));
}
/* The comp writes the age in the same hand as the writing above it; only the
   link beside it is set in the monospace. */
.project__meta { margin: 0; font-family: 'Geist', system-ui, sans-serif; }
.project__see {
  position: absolute; left: calc(var(--see-x, 473) * var(--u)); top: 0;
  display: inline-flex; align-items: center; gap: calc(14 * var(--u));
  text-transform: uppercase;
  border-bottom: 1px solid transparent;
  transition: color var(--dur-ui) ease, border-color var(--dur-ui) ease;
}
.project__see img { width: calc(24 * var(--u)); height: calc(24 * var(--u)); }
@media (hover: hover) and (pointer: fine) {
  .project__see:hover { color: var(--live); border-color: var(--live); }
}

/* The comp's tag pills, at the size a project page gives them. */
.tags--project .tag {
  padding-top: calc(3 * var(--u));
  font-size: calc(17.791 * var(--u));
  line-height: calc(23 * var(--u));
}

.piece {
  position: absolute;
  left: calc(var(--x) * var(--u));
  top: calc(var(--y) * var(--u));
  width: calc(var(--w) * var(--u));
  height: calc(var(--h) * var(--u));
  object-fit: cover;
  display: block;
  background: #d9d9d9;
}
/* The project's own logotype sits in the mosaic as a piece of it, but it is a
   mark and not a picture: it keeps its whole self and the page's own ground. */
.piece--logo { object-fit: contain; background: none; }
/* A sheet of the typeface, set in the typeface. The comp gives each block its
   own size, leading and tracking; the dim letters are the unaccented ones the
   accented sheet is built on. */
.piece--spec {
  margin: 0; background: none; height: auto;
  font-family: 'Nuvra', 'Geist', serif;
  font-size: calc(var(--fs) * var(--u));
  line-height: calc(var(--lh) * var(--u));
  letter-spacing: calc(var(--tr) * var(--u));
  text-align: center;
  color: var(--fg);
  /* the accented sheets are one unbroken run of letters: they turn the line
     wherever they have to, the way the comp sets them */
  overflow-wrap: anywhere;
}
.piece--spec .dim { color: #848484; }

/* A line the comp sets beside a logotype rather than inside it. */
.piece--word {
  display: flex; align-items: center; justify-content: center;
  margin: 0; background: none;
  font-family: 'Geist', system-ui, sans-serif; font-style: italic;
  font-size: calc(23.22 * var(--u)); line-height: calc(30.65 * var(--u));
  letter-spacing: -0.02em; white-space: nowrap;
}

/* ---------- Full-bleed image band ---------- */

/* The band drifts on its own and is a scroller at the same time: the drift is
   scrollLeft rather than a transform, so a hand on it adds to the same number
   instead of fighting an animation. The run is doubled and wrapped around its
   middle, which leaves half a run of slack either way before the seam - more
   than any flick - and the seam itself falls on identical frames. */
.marquee {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  /* a finger that starts on the band can still take the page up or down */
  touch-action: pan-x pan-y;
}
.marquee::-webkit-scrollbar { display: none; }
.marquee__track { display: flex; width: max-content; height: 100%; }
.marquee__track img,
.marquee__track video {
  width: calc(var(--w) * var(--u));
  height: calc(var(--h) * var(--u));
  object-fit: cover; flex: none; background: #d9d9d9;
  display: block;
}

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

.contact__email { text-transform: uppercase; border-bottom: 1px solid transparent; transition: color var(--dur-ui) ease, border-color var(--dur-ui) ease; width: max-content; }
.contact__copy { text-transform: uppercase; }
@media (hover: hover) and (pointer: fine) { .contact__email:hover { color: var(--live); border-color: var(--live); } }

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

.footer__item { text-transform: uppercase; white-space: nowrap; }
.footer__dot { animation: pulse 2.4s ease-in-out infinite; }
@keyframes pulse { 50% { opacity: 0.25; } }

/* ---------- Reveals ---------- */

.js-anim [data-reveal] { opacity: 0; transform: translateY(calc(12 * var(--u))); }
.js-anim [data-reveal].is-in { animation: revealIn 620ms var(--ease-out) forwards; animation-delay: var(--reveal-delay, 0ms); }
@keyframes revealIn { to { opacity: 1; transform: translateY(0); } }

/* The wipe clips the element itself, so it is never the observed target:
   a clipped element reports zero intersection area and would never reveal. */
.js-anim [data-reveal="wipe"] { opacity: 1; transform: none; }

.js-anim [data-reveal="wipe"] > * { display: block; clip-path: inset(0 100% 0 0); }
.js-anim [data-reveal="wipe"].is-in > * {
  clip-path: inset(0 0 0 0);
  transition: clip-path 900ms var(--ease-in-out);
}

/* ---------- About: a pop-up over the landing ----------
   Traced from Figma · About View (450:1399). The window is 1036 x 733 comp
   pixels and rides the same --u as the canvas behind it, so it scales with
   everything else. Two things are deliberate here. The right-hand block is
   positioned against the dialog rather than placed inside the scroller, so it
   stays put while the information moves - one element, no sticky, no second
   scroll container. And the scrollbar is taken away in favour of the rail in
   the left gutter, which says where you are and jumps where you point. */

.sheet {
  position: fixed;
  inset: 0;
  z-index: 55;
  display: none;
  /* The comp hangs the window from y=90, just clear of the pill, rather than
     centring it; holding that and capping the height keeps the pill in front
     of the window instead of on top of it on a short screen. */
  align-items: flex-start;
  justify-content: center;
  padding: calc(90 * var(--u)) 0 calc(24 * var(--u));
  --ink: #fafafa;
}
/* :target is the no-JS way in: the menu's About link is a real fragment link,
   and both close controls point back out of it. With JS the class takes over
   so the window can be faded in and the hash left alone. */
.sheet:target,
.sheet.is-open { display: flex; }

.sheet__scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  -webkit-backdrop-filter: blur(2.65px);
  backdrop-filter: blur(2.65px);
  opacity: 0;
  transition: opacity 200ms var(--ease-shut);
}

.sheet__dialog {
  position: relative;
  max-width: calc(100vw - 32px);
  max-height: calc(100svh - 114 * var(--u));
  padding: calc(11 * var(--u)) calc(18 * var(--u));
  display: flex;
  flex-direction: column;
  border-radius: calc(5 * var(--u));
  background: var(--glass);
  /* The comp blurs the window by 3.3px over a scrim blurred by 2.65. At comp
     size that is enough to wash the page out; at phone size the window covers
     almost the whole screen and the display type behind it still read
     through, so the glass is taken deeper. */
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  color: var(--ink);
  /* The window grows out of the top edge it hangs from, which is where the
     bar is, and falls back into it. Declaring the shut curve here and the
     open one on .is-visible gives each direction its own: a transition is
     the one on the state being moved to. */
  transform-origin: 50% 0;
  opacity: 0;
  transform: translateY(calc(-18 * var(--u))) scale(0.9);
  transition: opacity 180ms var(--ease-shut) 20ms,
              transform 240ms var(--ease-shut);
}

/* Where the glass cannot be made, the window simply turns opaque: the page
   behind must not be readable through the information. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .sheet__scrim { background: rgba(0, 0, 0, 0.72); }
  .sheet__dialog { background: rgba(10, 10, 10, 0.95); }
}

.sheet:target .sheet__scrim,
.sheet.is-visible .sheet__scrim { opacity: 1; transition: opacity 280ms var(--ease-out); }
.sheet:target .sheet__dialog,
.sheet.is-visible .sheet__dialog {
  opacity: 1;
  transform: none;
  transition: opacity 200ms ease-out, transform 480ms var(--ease-pop);
}

/* Each window brings its own box; everything above it is the shell. */
.about .sheet__dialog { width: calc(1036 * var(--u)); height: calc(733 * var(--u)); }
.sheet--touch .sheet__dialog { width: calc(1036 * var(--u)); height: calc(565.935 * var(--u)); }

/* The one scrolling part of a window, wherever a window has one. */
.sheet__scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.sheet__scroll::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* ---------- Header ---------- */

.sheet__head {
  flex: none;
  height: calc(24 * var(--u));
  margin-bottom: calc(26 * var(--u));
  display: flex;
  align-items: center;
  justify-content: flex-end;
}
.sheet__close {
  display: block;
  width: calc(24 * var(--u));
  height: calc(24 * var(--u));
  opacity: 0.75;
  transition: opacity var(--dur-ui) ease;
}
.sheet__close img { width: 100%; height: 100%; }
@media (hover: hover) and (pointer: fine) { .sheet__close:hover { opacity: 1; } }

/* ---------- The scrolling information ---------- */

.about__scroll {
  width: calc(633 * var(--u));
  padding: calc(20 * var(--u)) 0 calc(26 * var(--u)) calc(62 * var(--u));
}

.about__intro {
  width: calc(559 * var(--u));
  font-family: 'Geist', system-ui, sans-serif;
  font-size: calc(15 * var(--u));
  line-height: calc(20 * var(--u));
}
.about__intro p { margin: 0; }
.about__intro p + p { margin-top: calc(20 * var(--u)); }

.about__sections {
  margin-top: calc(86 * var(--u));
  width: calc(571 * var(--u));
  display: flex;
  flex-direction: column;
  gap: calc(32 * var(--u));
}

.about__line {
  width: calc(572 * var(--u));
  height: max(1px, calc(1 * var(--u)));
  margin-bottom: calc(7 * var(--u));
  background: rgba(250, 250, 250, 0.34);
}

.about__row {
  display: grid;
  grid-template-columns: calc(259 * var(--u)) calc(312 * var(--u));
  align-items: start;
}
.about__label {
  margin: 0;
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-weight: 400;
  font-size: calc(15 * var(--u));
  line-height: calc(20 * var(--u));
  text-transform: uppercase;
  white-space: nowrap;
}
.about__body {
  margin: 0;
  font-family: 'Geist', system-ui, sans-serif;
  font-size: calc(15 * var(--u));
  line-height: calc(20 * var(--u));
}
.about__body p { margin: 0; }
.about__list li { margin: 0; }

/* A spell of work starts a new block, so the date that opens one gets the
   blank line the comp leaves above it. */
.about__when {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-weight: 200;
}
.about__body > .about__when + * { margin-top: 0; }
.about__body > .about__when:not(:first-child) { margin-top: calc(20 * var(--u)); }
.about__what { font-weight: 700; }

.about__body--end {
  display: flex;
  justify-content: flex-end;
  align-items: flex-start;
}
.about__dl {
  display: inline-flex;
  align-items: center;
  gap: calc(2 * var(--u));
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-weight: 500;
  text-transform: uppercase;
  transition: color var(--dur-ui) ease;
}
.about__dl img {
  width: calc(20 * var(--u));
  height: calc(20 * var(--u));
  transition: transform var(--dur-ui) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .about__dl:hover { color: var(--live); }
  .about__dl:hover img { transform: translateY(calc(3 * var(--u))); }
}

/* The ten marks are different shapes, so they keep the comp's own x and y
   rather than being handed to a flex row that would align them its way. */
.about__soft {
  position: relative;
  width: calc(181.5 * var(--u));
  height: calc(67.252 * var(--u));
}
.soft {
  position: absolute;
  left: calc(var(--sx) * var(--u));
  top: calc(var(--sy) * var(--u));
  width: calc(var(--sw) * var(--u));
  height: calc(var(--sh) * var(--u));
}
.soft img { width: 100%; height: 100%; }
/* The Adobe marks are a glyph inside an empty square; the square is the grid
   cell and the glyph sits at its own inset within it. */
.soft--inset img {
  position: absolute;
  inset: calc(var(--it) * 1%) calc(var(--ir) * 1%) calc(var(--ib) * 1%) calc(var(--il) * 1%);
  width: auto;
  height: auto;
}

/* ---------- The block that does not move ---------- */

.about__side {
  position: absolute;
  left: calc(717 * var(--u));
  top: calc(61 * var(--u));
  width: calc(239 * var(--u));
  height: calc(469.63 * var(--u));
  max-height: calc(100% - 72 * var(--u));
  display: flex;
  flex-direction: column;
}
.about__photo {
  width: calc(237 * var(--u));
  height: calc(318 * var(--u));
  flex: 0 1 auto;
  min-height: 0;
  object-fit: cover;
}
.about__card { flex: none; margin-top: calc(19 * var(--u)); }
.about__card p { margin: 0; font-size: calc(15 * var(--u)); line-height: calc(20 * var(--u)); }
.about__name {
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-weight: 500;
  font-size: calc(18 * var(--u)) !important;
  line-height: calc(23 * var(--u)) !important;
}
.about__key { font-weight: 700; margin-top: calc(19 * var(--u)) !important; }
.about__val a { transition: color var(--dur-ui) ease; }
@media (hover: hover) and (pointer: fine) { .about__val a:hover { color: var(--live); } }

.about__social {
  /* The comp draws these at 25.305 with a 7.381 gap. A quarter larger reads
     better beside the name and gives a thumb something to aim at; the whole
     row scales together, so it keeps the comp's proportions. */
  --soc: 31.63;
  display: flex;
  gap: calc(9.226 * var(--u));
  margin-top: calc(19 * var(--u));
}
.about__social a {
  display: block;
  width: calc(var(--soc) * var(--u));
  height: calc(var(--soc) * var(--u));
  opacity: 0.8;
  transition: opacity var(--dur-ui) ease, transform var(--dur-ui) var(--ease-out);
}
.about__social img { width: 100%; height: 100%; }
@media (hover: hover) and (pointer: fine) {
  .about__social a:hover { opacity: 1; transform: translateY(calc(-2 * var(--u))); }
}

/* ---------- The guide rail ----------
   Thin on purpose: it should read as a margin mark, not as a control. It
   carries how far down the information you are, and each tick is the fast way
   to a section. */

.about__rail {
  position: absolute;
  left: calc(28 * var(--u));
  top: calc(61 * var(--u));
  bottom: calc(11 * var(--u));
  width: calc(18 * var(--u));
}
.about__rail-track,
.about__rail-fill {
  position: absolute;
  left: 50%;
  top: 0;
  width: 1px;
  margin-left: -0.5px;
}
.about__rail-track { bottom: 0; background: rgba(250, 250, 250, 0.13); }
/* Not a fill from the top but the window you are looking through: its length
   is how much of the information fits on screen, its place is where that is. */
.about__rail-fill {
  top: calc(var(--p0, 0) * 100%);
  height: calc(var(--ph, 0) * 100%);
  background: rgba(250, 250, 250, 0.55);
  transition: top 120ms linear;
}

.about__tick {
  position: absolute;
  left: 0;
  width: 100%;
  height: calc(16 * var(--u));
  top: calc(var(--t, 0) * 100%);
  margin-top: calc(-8 * var(--u));
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
.about__tick::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(5px, calc(7 * var(--u)));
  height: max(1px, calc(1 * var(--u)));
  transform: translate(-50%, -50%);
  background: rgba(250, 250, 250, 0.22);
  transition: width var(--dur-ui) var(--ease-out), background var(--dur-ui) ease;
}
.about__tick.is-current::before { width: max(11px, calc(13 * var(--u))); background: rgba(250, 250, 250, 0.9); }
@media (hover: hover) and (pointer: fine) {
  .about__tick:hover::before { width: max(11px, calc(13 * var(--u))); background: rgba(250, 250, 250, 0.7); }
}

/* ---------- Contact: the second pop-up ----------
   Traced from Figma · Contact View (468:2538). The display line, the note and
   the two buttons run down a 625 column; the marks and the address sit at the
   foot of a 239 one beside it. The phone comp puts the address straight after
   the note, so that is the order of the markup and the wide canvas lifts the
   right-hand pair out of the flow at the comp's own coordinates - the 35 on
   the left is what centring a 625 + 66 + 239 row in 1000 leaves over. */

.touch__body {
  position: relative;
  padding: 0 0 calc(12 * var(--u)) calc(35 * var(--u));
}

.touch__display {
  margin: 0;
  width: calc(625 * var(--u));
  font-family: 'Geist', system-ui, sans-serif;
  font-weight: 700;
  font-size: calc(100 * var(--u));
  line-height: 0.857;
  text-transform: uppercase;
}

.touch__note { margin-top: calc(66 * var(--u)); width: calc(298 * var(--u)); }
.touch__label,
.touch__copy {
  margin: 0;
  font-family: 'Geist Mono', ui-monospace, monospace;
  text-transform: uppercase;
}
.touch__label { font-size: calc(15.485 * var(--u)); line-height: calc(20 * var(--u)); }
.touch__copy {
  margin-top: calc(11 * var(--u));
  font-size: calc(16 * var(--u));
  line-height: calc(21 * var(--u));
}

.touch__mail {
  position: absolute;
  left: calc(726 * var(--u));
  top: calc(292 * var(--u));
  width: calc(239 * var(--u));
  margin: 0;
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: calc(15 * var(--u));
  line-height: calc(20 * var(--u));
}
.touch__mail-key { display: block; font-weight: 700; }
.touch__mail a { display: block; transition: color var(--dur-ui) ease; }

.touch__social {
  position: absolute;
  left: calc(726 * var(--u));
  top: calc(229 * var(--u));
  display: flex;
  gap: calc(12.833 * var(--u));
}
.touch__social a {
  display: block;
  width: calc(44 * var(--u));
  height: calc(44 * var(--u));
  opacity: 0.8;
  transition: opacity var(--dur-ui) ease, transform var(--dur-ui) var(--ease-out);
}
.touch__social img { width: 100%; height: 100%; }

.touch__actions {
  margin-top: calc(66 * var(--u));
  display: flex;
  gap: calc(37 * var(--u));
}
.touch__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: calc(14.016 * var(--u));
  padding: calc(11.468 * var(--u)) calc(25.484 * var(--u));
  border: 1px solid var(--fg);
  border-radius: calc(6.371 * var(--u));
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: calc(19.113 * var(--u));
  line-height: calc(25 * var(--u));
  text-transform: uppercase;
  white-space: nowrap;
  transition: background var(--dur-ui) ease, color var(--dur-ui) ease,
              border-color var(--dur-ui) ease, transform var(--dur-fast) var(--ease-out);
}
.touch__btn img { width: calc(24 * var(--u)); height: calc(24 * var(--u)); flex: none; }
.touch__btn:active { transform: scale(0.98); }

@media (hover: hover) and (pointer: fine) {
  .touch__btn:hover { background: var(--fg); color: #000; border-color: var(--fg); }
  .touch__btn:hover img { filter: invert(1); }
  .touch__mail a:hover { color: var(--live); }
  .touch__social a:hover { opacity: 1; transform: translateY(calc(-2 * var(--u))); }
}

/* ---------- Mobile: the comp has no small frame, so the same
              elements reflow into one column ---------- */

@media (max-width: 1279px) {
  /* The phone comp is a canvas of its own: 402 wide, 4622 tall. Every element
     carries a second set of coordinates under --m*, and each declaration falls
     back to the desktop value where the two comps agree - so nothing can end up
     with an invalid declaration and an auto size. Past 430px the unit stops
     growing and the column centres, so a tablet gets the phone layout at its
     own size rather than blown up to fill the glass.

     The two comps cannot meet seamlessly: a phone pixel is 1/402 of the width
     and a desktop one 1/1728, so the phone canvas is always the larger of the
     two per viewport pixel and they only draw level at a 1728px breakpoint.
     What is left is to make the step small - holding the column at 430 and
     handing over at 1280, where the desktop unit is 0.741, takes it from the
     1.93x it was to 1.28x. */
  :root { --u: calc(min(100vw, 430px) / 402); }

  .stage { display: flex; justify-content: center; }
  .frame {
    width: calc(402 * var(--u));
    height: calc((var(--mframe-h, var(--frame-h, 4406)) + var(--push, 0) + var(--trim, 0)) * var(--u));
  }

  /* Custom properties inherit, so var(--mw, var(--w)) would pick up an
     ancestor's --mw instead of falling back to the element's own --w. Clearing
     them here breaks that chain: an inline value still wins over a stylesheet
     one, so only the elements that were never given mobile coordinates fall
     through to their desktop ones. .tag is the exception - it takes its height
     from .tags by inheritance, the same way it does on the desktop canvas. */
  .pill, .pill__logo, .pill__current,
  .btn, .btn__label, .btn__icon,
  .t, .chips, .chips li, .tags, .thumbs, .thumbs img, .thumbs__slot, .thumbs__video,
  .row__toggle, .row__shots,
  .rule, .hero__bg, .band, .card, .marquee, .footer__dot {
    --mx: initial; --my: initial; --mw: initial; --mh: initial;
    --mfs: initial; --mlh: initial;
  }
  /* .tag keeps inheriting --mh from .tags, the way it inherits --h on the
     desktop canvas, but must not inherit its left or width with it. */
  .tag { --my: 0; --mx: initial; --mw: initial; }

  .pill, .pill__logo, .pill__current,
  .btn, .btn__label, .btn__icon,
  .t, .chips, .chips li, .tags, .tag, .thumbs, .thumbs img, .thumbs__slot, .thumbs__video,
  .row__toggle, .row__shots,
  .rule, .hero__bg, .band, .card, .marquee, .footer__dot {
    left: calc(var(--mx, var(--x, 0)) * var(--u));
    top: calc(var(--my, var(--y, 0)) * var(--u));
    width: calc(var(--mw, var(--w)) * var(--u));
    height: calc(var(--mh, var(--h)) * var(--u));
  }
  /* Every rule that sizes type off --fs needs the mobile pair too, or it keeps
     the desktop comp's numbers on the phone canvas. */
  .t, .btn__label, .pill__current, .card__head, .card__list {
    font-size: calc(var(--mfs, var(--fs)) * var(--u));
    line-height: calc(var(--mlh, var(--lh)) * var(--u));
  }
  .btn__label, .card { height: auto; }
  /* Figma measured these boxes with its own metrics; the names are one line. */
  .row__name { white-space: nowrap; }

  /* On a phone there is no second column for the work to pass, so the writing
     is not pinned: it simply comes first and the mosaic follows it. */
  .project__info {
    top: calc(var(--my) * var(--u));
    width: calc(402 * var(--u));
    height: calc(var(--mh) * var(--u));
  }
  .project__info-in { position: static; padding-top: calc(var(--mtext, 150) * var(--u)); }
  .project__text {
    margin-left: calc(26 * var(--u));
    width: calc(352 * var(--u));
    font-size: calc(var(--mfs) * var(--u));
    line-height: calc(var(--mlh) * var(--u));
  }
  .project__foot {
    margin-top: calc(39 * var(--u));
    height: auto;
    font-size: calc(13.632 * var(--u));
    line-height: calc(15.83 * var(--u));
  }
  .piece--spec {
    font-size: calc(var(--mfs) * var(--u));
    line-height: calc(var(--mlh) * var(--u));
    letter-spacing: calc(var(--mtr) * var(--u));
  }
  /* The age and the link share a line here, the link hung from the right edge
     rather than placed at a measured x: the writing's column is narrower than
     the comp's and the two would otherwise run into each other. */
  .project__see {
    left: auto;
    right: calc(18.088 * var(--u));
    top: 0;
    gap: calc(9.554 * var(--u));
    font-size: calc(13.028 * var(--u));
    line-height: calc(16 * var(--u));
  }
  .project__see img { width: calc(16.359 * var(--u)); height: calc(16.359 * var(--u)); }
  .piece {
    left: calc(var(--mx) * var(--u));
    top: calc(var(--my) * var(--u));
    width: calc(var(--mw) * var(--u));
    height: calc(var(--mh) * var(--u));
  }
  .tags--project .tag {
    padding-top: calc(7.831 * var(--u));
    font-size: calc(12.127 * var(--u));
    line-height: calc(16 * var(--u));
  }
  /* A line set beside a mark belongs to it, so it is scaled with it. */
  .piece--word {
    font-size: calc(var(--mfs) * var(--u));
    line-height: calc(var(--mlh) * var(--u));
  }

  /* The comp puts the filter beside the heading, which only a wide canvas has
     room for. On the phone it wraps under it, laid out rather than placed. */
  /* One line that scrolls rather than two that wrap: the second line costs
     more room at the top of the page than the third row of projects can
     spare, and a row of chips is a thing people already swipe. */
  .chips--filter {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: calc(8 * var(--u));
    overflow-x: auto;
    scrollbar-width: none;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
  }
  .chips--filter::-webkit-scrollbar { display: none; }
  .chips--filter li {
    position: static;
    flex: none;
    width: auto;
    height: calc(36 * var(--u));
  }
  .chips--filter .chip { padding-inline: calc(14 * var(--u)); }

  /* The pills shrank on the wide canvas; at a quarter of the width a phone has,
     they would stop being readable, so they keep the phone comp's size. */
  .tag {
    padding-top: calc(2.5 * var(--u));
    font-size: calc(12.926 * var(--u));
    line-height: calc(16.25 * var(--u));
  }
  /* An <hr> carries no --h, so the shared rule resolves to height:auto - which
     on an empty element is nothing at all. It has to restate its own line. */
  .rule {
    top: calc((var(--my, var(--y)) - 1) * var(--u));
    width: calc(402 * var(--u));
    height: max(1px, calc(1 * var(--u)));
  }
  .u-mob { display: inline; }
  .shift-layer { width: calc(402 * var(--u)); }

  /* The nav is fixed to the viewport, not to the frame, so it needs the
     column's own width to stay centred over it. The pill centres itself inside
     that, so it has to keep its own left against the canvas rule. */
  /* Centred by arithmetic, not by a transform: a transformed ancestor becomes
     the containing block for its position:fixed descendants, and .nav is
     height:0 - which collapsed the menu's scrim to nothing on this canvas and
     left the page behind it undimmed. */
  .nav { width: calc(402 * var(--u)); left: calc(50% - 201 * var(--u)); }
  /* A var() that cannot resolve computes to the property's initial value, not
     to the declaration above it - so the pill has to restate its whole box. */
  .pill { left: 50%; width: calc(354 * var(--u)); height: calc(46 * var(--u)); }
  /* Line breaks the wide comp needs and the narrow one does not. */
  .u-desk { display: none; }
  .nav__cta { display: none; }
  /* The phone comp holds the panel at its closed width: it grows downwards
     and not sideways. */
  .pill { gap: 0; }
  .pill.is-open { width: calc(354 * var(--u)); }
  /* One above the other, each spanning the panel inside its padding. */
  /* The rules stop short of the panel's edge here rather than bleeding past
     it, and the type comes down from 18 to 15. */
  .pill__links {
    width: calc(314 * var(--u));
    margin-inline: 0;
    margin-top: calc(26 * var(--u));
  }
  .pill__links li { padding: calc(5 * var(--u)) calc(18 * var(--u)); }
  .pill__links li:not(:last-child) { padding-bottom: calc(11 * var(--u)); }
  .pill__links li:last-child { padding-block: calc(10 * var(--u)); }
  .pill__links a { font-size: calc(15 * var(--u)); line-height: calc(20 * var(--u)); }

  .pill__actions {
    flex-direction: column;
    gap: calc(10 * var(--u));
    width: calc(282 * var(--u));
    margin-top: calc(12 * var(--u));
  }
  .pill__btn { flex: none; width: 100%; padding-block: calc(8 * var(--u)); }
  /* The comp puts the call on top here, the other way round from the wide
     panel; CSS does that rather than a second copy of the markup. */
  .pill__actions a:nth-child(2) { order: -1; }

  /* Not in the phone comp: the hero photo runs straight into the dark, so it
     is masked away over its last third rather than stopping on an edge. */
  .band { display: none; }
  .hero__bg {
    -webkit-mask-image: linear-gradient(to bottom, #000 56%, rgba(0, 0, 0, 0) 100%);
    mask-image: linear-gradient(to bottom, #000 56%, rgba(0, 0, 0, 0) 100%);
  }

  .hero__title, .hero__sub { text-align: left; }
  .display { letter-spacing: calc(-2 * var(--u)); }

  /* Three discipline cards on a rail you push sideways. */
  .cards {
    position: absolute;
    left: 0;
    top: calc(2221 * var(--u));
    width: calc(402 * var(--u));
    height: calc(371 * var(--u));
    display: flex;
    align-items: flex-start;
    gap: calc(32 * var(--u));
    padding-left: calc(49 * var(--u));
    padding-right: calc(15 * var(--u));
    overflow-x: auto;
    scrollbar-width: none;
    overscroll-behavior-x: contain;
  }
  .cards::-webkit-scrollbar { display: none; }
  /* On the rail the cards need edges to read as separate cards; the desktop
     comp, where they sit in open space, has none. */
  .card {
    position: relative; left: auto; top: auto; flex: none;
    padding: calc(20.14 * var(--u)) calc(21.5 * var(--u));
    border: 1px solid rgba(255, 255, 255, 0.28);
    border-radius: calc(12 * var(--u));
  }

  .card__list { width: calc(219.9 * var(--u)); margin-top: calc(27.9 * var(--u)); }
  .marquee__track img { width: calc(var(--mw) * var(--u)); height: calc(var(--mh) * var(--u)); }

  /* Work rows. The phone comp puts the name and the toggle on one line, the
     tags under them and the thumbnails on a rail; ( VIEW + ) is the resting
     state here, since a finger has no hover to reveal it with. */
  .row { position: absolute; left: 0; top: 0; width: calc(402 * var(--u)); height: 0; }
  /* The desktop's 2px top nudge traces a Figma baseline; on the phone box it
     just leaves more air above the text than below. */
  .tag {
    padding-top: 0;
    font-size: calc(12.926 * var(--u));
    line-height: calc(16.25 * var(--u));
  }
  .thumbs, .row__shots { overflow-x: auto; scrollbar-width: none; }
  .thumbs::-webkit-scrollbar, .row__shots::-webkit-scrollbar { display: none; }
  /* The comp drops the intro on the phone. */
  .intro { display: none; }

  /* The toggle covers the whole row, so without this the strip never sees a
     swipe. Lifted above it, and left free on both axes so the browser can tell
     a sideways pan of the strip from a vertical scroll of the page by the
     gesture itself; main.js forwards a tap back to the toggle. */
  .thumbs {
    /* A row in normal flow, not absolute children inside their own scroller:
       the latter does create scrollable overflow, but it is the brittlest way
       to ask for it. The gap and the widths reproduce the comp's offsets
       exactly, so the shots still take off from where their thumbnail sits. */
    display: flex;
    align-items: flex-start;
    gap: calc(11.585 * var(--u));
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    pointer-events: auto;
    z-index: 4;
    touch-action: pan-x pan-y;
    cursor: pointer;
  }
  .thumbs img, .thumbs__slot, .thumbs__video {
    position: static;
    flex: none;
    width: calc(var(--mw) * var(--u));
    height: calc(var(--mh) * var(--u));
  }
  .thumbs--type {
    display: block;
    /* The same line against the phone's taller band. It does not scroll on its
       own: the strip is where the flight starts, so it always starts on the A. */
    overflow: hidden;
    font-size: calc(141.6 * var(--u));
    line-height: calc(156.5 * var(--u));
  }

  /* Open, the thumbnails are invisible but still a layer: without this they
     sit on the big strip and swallow the swipe meant for it. */
  .row.is-open .thumbs { pointer-events: none; }

  .row__view { max-width: 5ch; opacity: 1; }
  .row.is-open .row__view { max-width: 0; opacity: 0; }
  .row__cta { border-radius: calc(4.46 * var(--u)); }
  .row__shots { --lead: 242; --strip: 320; }
  .row.is-settled .row__shots { top: calc((var(--my) + var(--lead)) * var(--u)); }
  .row__track { gap: calc(22.857 * var(--u)); height: calc(320 * var(--u)); }
  .row__track li {
    width: calc(var(--mw) * var(--u));
    height: calc(320 * var(--u));
    transform: translate3d(calc(var(--mdx) * var(--u)), calc(var(--mdy) * var(--u)), 0) scale(var(--ms));
  }
  .row__track .glyph {
    font-size: calc(272 * var(--u));
    line-height: calc(252 * var(--u));
    height: calc(320 * var(--u));
    transform: none;
  }
  .row--type .row__track {
    transform: translate3d(calc(var(--mtdx) * var(--u)), calc(var(--mtdy) * var(--u)), 0) scale(var(--mts));
  }
  .row--type.is-open .row__track { transform: none; }
  .row--type .row__track li.more { margin-left: calc(22.857 * var(--u)); }


  .footer .chips { display: block; }

  /* ---------- About, on the phone column ----------
     Traced from Figma · the phone About (450:1797). The window goes full
     bleed - 402 comp pixels is the whole column - and sits on the bottom edge
     as a sheet, with the pill still above it. The block that is pinned beside
     the information on the wide canvas becomes the head of the column here,
     portrait beside the name, so it leads the markup; the desktop position is
     the one that is lifted out of the flow.

     The comp leaves no gutter: everything starts at the window's own 18px
     padding and the column is its full 366 wide, which the portrait and the
     name beside it need to the last pixel. So the rail goes under that
     padding, against the window's edge, with marks small enough to stay out
     of the text - a hairline at the margin, which is what it was asked to
     be. */

  .sheet { align-items: stretch; padding: 0; }

  /* The whole screen, with the bar out of the way: on a phone the window is
     the view, not something laid over it. The dash is then the way out, so
     it keeps the top corner to itself. */
  .sheet-is-open .nav,
  html:has(.sheet:target) .nav { opacity: 0; pointer-events: none; }

  /* Both windows take the screen here, so the shell's own box is the one
     that counts; the selector matches the per-window rules it has to beat. */
  .about .sheet__dialog,
  .sheet--touch .sheet__dialog {
    width: calc(402 * var(--u));
    height: 100svh;
  }
  /* Filling the screen, a scale would pull the window's own edges in; from
     the bottom it reads as the sheet it is. */
  .sheet__dialog {
    transform-origin: 50% 100%;
    transform: translateY(calc(46 * var(--u))) scale(0.985);
  }
  .sheet__dialog {
    width: calc(402 * var(--u));
    height: 100svh;
    max-width: none;
    max-height: none;
    /* The comp drops the whole window 36 lower on this canvas: the head sits
       at 47 rather than 11, which on a full-screen sheet is the air the status
       bar and the notch want anyway. */
    padding: max(calc(47 * var(--u)), env(safe-area-inset-top))
             calc(18 * var(--u))
             max(calc(11 * var(--u)), env(safe-area-inset-bottom));
    border-radius: 0;
  }

  .about__scroll {
    width: 100%;
    padding: 0 0 calc(22 * var(--u));
  }

  /* The head is inset further than the information under it - 31 against 18 -
     so the dash comes in off the corner. */
  .sheet__head { margin-inline: calc(13 * var(--u)); }

  /* The dash is the only way out now that the bar is gone, so it gets a
     target a thumb can hit without moving where it is drawn. */
  .sheet__close {
    box-sizing: content-box;
    padding: calc(11 * var(--u));
    margin: calc(-11 * var(--u));
  }

  .about__rail {
    left: 0;
    top: calc(50 * var(--u) + max(calc(47 * var(--u)), env(safe-area-inset-top)));
    bottom: max(calc(11 * var(--u)), env(safe-area-inset-bottom));
    width: calc(14 * var(--u));
  }
  /* Against the window's own edge the marks have to stay out of the text. */
  .about__tick::before { width: max(5px, calc(6 * var(--u))); }
  .about__tick.is-current::before { width: max(8px, calc(9 * var(--u))); }
  @media (hover: hover) and (pointer: fine) {
    .about__tick:hover::before { width: max(8px, calc(9 * var(--u))); }
  }

  /* ---------- The head of the column ---------- */

  .about__side {
    position: static;
    width: 100%;
    height: calc(210 * var(--u));
    max-height: none;
    flex-direction: row;
    align-items: flex-end;
    gap: calc(19 * var(--u));
  }
  .about__photo {
    flex: none;
    width: calc(156 * var(--u));
    height: calc(210 * var(--u));
  }
  /* The comp leaves 116 between the name and the contact, which on a phone
     reads as a hole rather than as air; the contact comes up under the name
     instead and the column is free to end above the portrait. */
  .about__card {
    flex: 1 1 auto;
    min-width: 0;
    height: 100%;
    margin-top: 0;
    display: flex;
    flex-direction: column;
  }
  .about__contact { margin-top: calc(26 * var(--u)); }
  /* The comp sets this at 14.385, which fills its 191 column to the last
     pixel; a hair under keeps it on one line whatever the rasteriser does. */
  .about__name {
    font-size: calc(14 * var(--u)) !important;
    line-height: calc(19 * var(--u)) !important;
    white-space: nowrap;
  }
  .about__card p { font-size: calc(11.987 * var(--u)); line-height: calc(16 * var(--u)); }
  .about__key { margin-top: calc(15 * var(--u)) !important; }

  .about__social {
    --soc: 34.5;
    gap: 0;
    margin-top: 0;
    margin-left: calc(-6 * var(--u));
  }
  .about__social a {
    box-sizing: content-box;
    padding: calc(6 * var(--u));
  }

  /* ---------- The information ---------- */

  .about__intro {
    width: 100%;
    margin-top: calc(30 * var(--u));
  }

  .about__sections { width: 100%; margin-top: calc(23 * var(--u)); }
  .about__line { width: 100%; }

  /* No room for two columns: the label becomes a heading over its block. The
     comp also reads the record in a different order here. */
  .about__row {
    grid-template-columns: 1fr;
    row-gap: calc(10 * var(--u));
  }
  .about__row--cv { grid-template-columns: 1fr; }
  .about__body { width: calc(312 * var(--u)); max-width: 100%; }
  .about__body--end { width: calc(320 * var(--u)); max-width: 100%; }
  .about__soft { width: calc(181.5 * var(--u)); }

  /* ---------- Contact, on the phone column ----------
     Traced from Figma · Mobile Contact (468:3011). One column: the address
     falls in under the note where the comp puts it, the marks centre on their
     own line and the buttons stack. */

  .touch__body {
    position: static;
    padding: 0 0 calc(24 * var(--u));
    display: flex;
    flex-direction: column;
    align-items: flex-start;
  }
  .touch__display { width: 100%; font-size: calc(57.336 * var(--u)); }
  .touch__note { margin-top: calc(23 * var(--u)); width: 100%; }
  .touch__copy {
    margin-top: calc(23 * var(--u));
    font-size: calc(16 * var(--u));
    line-height: calc(21 * var(--u));
  }
  /* One blank line after the note, which is how the comp sets it off. */
  .touch__mail {
    position: static;
    width: 100%;
    margin-top: calc(21 * var(--u));
    font-size: calc(16 * var(--u));
    line-height: calc(21 * var(--u));
  }

  .touch__social {
    position: static;
    align-self: center;
    margin-top: calc(53 * var(--u));
    gap: calc(11 * var(--u));
  }
  /* The comp's 21 of air between the marks, spent instead on a target a thumb
     can hit; the marks keep their own spacing. */
  .touch__social a {
    box-sizing: content-box;
    width: calc(34.047 * var(--u));
    height: calc(34.047 * var(--u));
    padding: calc(5 * var(--u));
  }

  /* The comp leaves 84 under the marks; the padding that makes them a target
     is 10 of it, so the gap is spent before it is set. */
  .touch__actions {
    margin-top: calc(74 * var(--u));
    flex-direction: column;
    gap: calc(18 * var(--u));
  }
  .touch__btn {
    width: calc(227 * var(--u));
    height: calc(46 * var(--u));
    padding-block: 0;
    font-size: calc(17 * var(--u));
    line-height: calc(22 * var(--u));
  }

  #about-cv         { order: 1; }
  #about-education  { order: 2; }
  #about-employment { order: 3; }
  #about-events     { order: 4; }
  #about-skills     { order: 5; }
  #about-software   { order: 6; }
  #about-languages  { order: 7; }
}
/* Between the phone column and the desktop canvas there is nothing to
   interpolate: the two comps are different layouts, so they simply swap. */


/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .marquee--anim .marquee__track { animation: none; }
  .sheet__scrim, .sheet__dialog, .about__rail-fill, .about__tick::before { transition: none; }
  .sheet__dialog, .sheet.is-visible .sheet__dialog { transform: none; transition: none; }
  .footer__dot { animation: none; }
  .js-anim [data-reveal] { transform: none; }
  .js-anim [data-reveal].is-in { animation: revealFade 300ms ease forwards; }
  @keyframes revealFade { to { opacity: 1; } }
  .js-anim [data-reveal="wipe"] > * { clip-path: none; }
  .chips--hero li { opacity: 1; transform: none; animation: none; }
  .pill { transition: none; }
  .pill__links li, .pill__actions { transform: none; transition: opacity 160ms ease; }
  .burger line { transition: opacity 160ms ease; }
  .hero__bg { animation: none; transform: none; }
  .row:has(.row__toggle:hover) .thumbs { transform: none; }
  .row, .shift, .frame, .row__panel { transition: none; }
  .row__desc, .row__cta, .row__track li,
  .row__track .glyph, .row.is-open .row__track .glyph,
  .row--type .row__track { transition: opacity 200ms ease; transform: none; }
  .row.is-open .row__desc, .row.is-open .row__cta, .row.is-open .row__track li { transition-delay: 0s; }
  .row__view { transition: opacity 160ms ease; }
  .row__sign span { transition: opacity 160ms ease; transform: none; }
  .row.is-open .row__sign span:first-child { transform: none; }
}
