/* ============================================================
   PORTFOLIO WIREFRAME v2 — SHARED DESIGN SYSTEM
   Swiss layout · black / white / grey · rounded frames
   ============================================================ */

/* ---------- 1. TOKENS ---------- */
:root {
  /* colour — strictly monochrome */
  --ink:        #0a0a0a;
  --ink-70:     #4a4a4a;
  --ink-45:     #8a8a8a;
  --ink-25:     #bdbdbd;
  --rule:       #e4e4e4;
  --wire:       #ececec;
  --wire-alt:   #f6f6f6;
  --paper:      #ffffff;

  /* grid */
  --gutter:     clamp(20px, 3.6vw, 56px);

  /* THE EDGE. Two vertical lines govern the whole site: the left one
     runs through the F of your name, the right one through the right
     side of the black disc. Everything flush left sits on the first,
     everything flush right on the second, at every window width.

     .wrap centres a 1600 measure and pads it by the gutter, so above
     1712px the content edge stops moving while a plain gutter would
     keep going — which is exactly how the header drifted out of line
     with the page beneath it. This expression is that same edge, for
     anything that cannot use .wrap because it is fixed to the screen. */
  --col-gap:    clamp(16px, 2vw, 32px);
  --maxw:       1600px;

  /* edge-to-edge media — raise the inset to pull it away from the
     viewport edge (e.g. 24px), and raise the radius to match if you do.
     While it runs to the edge, the corners stay square. */
  --bleed-inset: 0px;
  --bleed-radius: 0px;

  /* corner radius */
  --r-lg:       clamp(16px, 1.7vw, 28px);
  --r-md:       16px;
  --r-sm:       12px;
  --r-pill:     999px;

  /* ONE LINE WEIGHT. Every rule, underline and divider on the site is
     this thick — section dividers, meta bars, list separators, the
     underline that draws under a project name, the marks in the
     transport. There is no heavier rule anywhere; weight is carried by
     colour (--rule for a divider, --ink for a live one), never by
     thickness. */
  --rule-w:     1px;

  /* rhythm */
  --sp-s:       16px;
  --sp-m:       clamp(32px, 5vh, 56px);
  --sp-l:       clamp(72px, 10vh, 128px);
  --sp-xl:      clamp(110px, 17vh, 210px);

  /* type */
  --font: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* Swiss modernist scale — base 13, ratio 1.20, every step derived:
     11 · 13 · 16 · 19 · 23 · 27 · 32 · 39 · 47
     The vw term lands each size exactly on its step at 1440 wide, and
     the floor keeps it readable on a phone. */
  --t-display: clamp(2.1rem, 4.444vw, 4rem);     /* 64 — set by hand, above the scale */
  --t-h1:      clamp(1.7rem,  2.7vw, 2.44rem);   /* 39 */
  --t-h2:      clamp(1.3rem,  1.9vw, 1.69rem);   /* 27 */
  --t-lead:    2rem;                             /* 32 — home, to spec  */
  --t-h3:      clamp(1.2rem,  1.6vw, 1.44rem);   /* 23 */
  --t-lede:    clamp(1.06rem, 1.3vw, 1.19rem);   /* 19 */
  --t-body:    clamp(0.94rem, 1.1vw, 1rem);      /* 16 */
  --t-small:   0.8125rem;                        /* 13 */
  --t-label:   0.6875rem;                        /* 11 */

  /* ONE SPACING SYSTEM. Two steps and nothing else:
     --sp-sec   between sections, everywhere on the site
     --sp-block between frames inside a section
     Every other gap on the site is derived from these or from the
     type scale. Change these two and the whole rhythm moves. */
  --sp-sec:    clamp(88px, 11vh, 150px);
  --sp-block:  clamp(48px, 7vh, 96px);

  /* ONE MOTION SYSTEM. Everything on this site arrives the same way —
     it rises, sharpens and fades in — and the masthead leaves the same
     way in reverse. Same distance, same blur, same curve, sitewide. */
  --m-rise:    32px;
  --m-rise-lg: 64px;          /* frames travel further than text */
  /* nothing on this site is ever blurred. Motion is rise and fade,
     and that is all. */
  --m-blur:    0px;
  --m-dur:     0.95s;

  /* motion */
  --ease:      cubic-bezier(0.16, 1, 0.3, 1);
  --ease-io:   cubic-bezier(0.65, 0, 0.35, 1);
  --dur:       0.8s;

  --head-h:    72px;
}

/* ---------- 2. RESET ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.55;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
img, video { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
h1, h2, h3, h4, p, figure, ul { margin: 0; padding: 0; }
ul { list-style: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
::selection { background: var(--ink); color: var(--paper); }
section[id] { scroll-margin-top: calc(var(--head-h) + 24px); }

/* ---------- 3. LAYOUT ---------- */
.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--col-gap);
}
.rule { height: 1px; background: var(--rule); border: 0; }

.c-1-5  { grid-column: 1 / 6; }
.c-1-6  { grid-column: 1 / 7; }
.c-1-7  { grid-column: 1 / 8; }
.c-1-8  { grid-column: 1 / 9; }
.c-1-12 { grid-column: 1 / -1; }
.c-3-8  { grid-column: 3 / 9; }
.c-4-12 { grid-column: 4 / -1; }
.c-5-12 { grid-column: 5 / -1; }
.c-7-12 { grid-column: 7 / -1; }
.c-8-12 { grid-column: 8 / -1; }

@media (max-width: 900px) {
  .c-1-5, .c-1-6, .c-1-7, .c-1-8, .c-3-8,
  .c-4-12, .c-5-12, .c-7-12, .c-8-12 { grid-column: 1 / -1; }
}

/* ---------- 4. TYPE ---------- */
/* tracking opens as the size comes down — at 47px a heading needs far
   less negative tracking than it did at 176px */
.display { font-size: var(--t-display); line-height: 1.0;  letter-spacing: -0.04em;  font-weight: 700; }
.h1      { font-size: var(--t-h1);      line-height: 1.06; letter-spacing: -0.035em; font-weight: 700; }
.h2      { font-size: var(--t-h2);      line-height: 1.16; letter-spacing: -0.03em;  font-weight: 600; }
.h3      { font-size: var(--t-h3);      line-height: 1.3;  letter-spacing: -0.02em;  font-weight: 600; }
.lede    { font-size: var(--t-lede);    line-height: 1.5;  letter-spacing: -0.015em; }
.body { max-width: 62ch; color: var(--ink-70); }
/* set across the page rather than in a column — for a short block that
   sits under a full-width image and shouldn't read as a text section */
.body--wide { max-width: 100ch; font-size: var(--t-small); line-height: 1.7; }
.body + .body { margin-top: 1em; }
.body strong, .lede strong { color: var(--ink); font-weight: 600; }

/* Justified without the rivers. Justify alone stretches word spaces to
   fill each line; hyphenation lets the browser break long words instead,
   so the gaps stay close to normal. Reverts to ragged-right on narrow
   columns, where no amount of hyphenation saves it. */
.body--justify {
  text-align: justify;
  text-justify: inter-word;
  hyphens: auto;
  -webkit-hyphens: auto;
  hyphenate-limit-chars: 7 3 3;
}
@media (max-width: 760px) {
  .body--justify { text-align: left; hyphens: manual; -webkit-hyphens: manual; }
}

/* Line breaks set by hand. The measure has to be wide enough to hold the
   longest of them or the browser adds breaks of its own on top, which is
   worse than no hand-setting at all. Below 900px the column is too
   narrow for them to survive, so they are dropped and it reflows. */
.body--breaks { max-width: none; }
@media (max-width: 900px) { .body--breaks br { display: none; } }

/* Copy I have written for you rather than taken from you. Magenta so
   nothing suggested can be mistaken for something agreed — delete this
   one rule and every suggestion on the site turns black at once. */
.todo { color: #E6007E; }

.label {
  font-size: var(--t-label);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-45);
}
.num {
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  color: var(--ink-45);
}

/* ---------- 5. HEADER ---------- */
.site-head {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 60;
  height: var(--head-h);
  /* three tracks, not space-between — the nav has to sit on the centre
     of the screen, not on the centre of what is left over */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 20px;
  /* the same edge .wrap arrives at — so the name and the disc are the
     alignment for everything below them, not merely near it */
  padding-inline: max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter)));

  /* Over white at the top of the page there is nothing to be glassy
     about, so it is simply absent. The moment it starts overlapping
     content it becomes glass: more transparent than before, not less,
     with the blur doing the work of keeping the type legible. */
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: var(--rule-w) solid transparent;
  transition: transform 0.5s var(--ease), border-color 0.4s var(--ease),
              background 0.45s var(--ease), backdrop-filter 0.45s var(--ease);
}
.site-head.is-stuck {
  background: rgba(255, 255, 255, 0.55);
  backdrop-filter: saturate(180%) blur(22px);
  -webkit-backdrop-filter: saturate(180%) blur(22px);
}
.site-head > .brand { justify-self: start; }
.site-head > .nav   { justify-self: center; }
.site-head > .cta   { justify-self: end; }
.site-head > .menu-btn { justify-self: end; }
.site-head.is-stuck { border-bottom-color: var(--rule); }

/* everything fixed to the screen sits on the same two lines */
.wf-toggle, .backlink { --e: max(var(--gutter), calc((100vw - var(--maxw)) / 2 + var(--gutter))); }
.site-head.is-hidden { transform: translateY(-110%); }

.brand { font-size: 21px; font-weight: 700; letter-spacing: -0.035em; }

.nav { display: flex; gap: clamp(18px, 2.4vw, 40px); }
.nav a {
  position: relative;
  display: inline-block;
  font-size: var(--t-body);
  font-weight: 500;
  letter-spacing: -0.01em;
  padding-bottom: 3px;
  transition: font-weight 0.25s var(--ease);
}
/* A copy of the label at the heavier weight, zero-height and hidden, so
   the link is already as wide as its semibold self. Without it, going
   bold on hover widens the word and shoves the other two along. */
.nav a::before {
  content: attr(data-label);
  display: block;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: 600;
  pointer-events: none;
}
.nav a:hover { font-weight: 600; }
.nav a::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 100%; height: var(--rule-w); background: var(--ink);
  transform: scaleX(0); transform-origin: right;
  transition: transform 0.45s var(--ease);
}
.nav a:hover::after { transform: scaleX(1); transform-origin: left; }

/* A black disc that opens into the invitation on hover. The open width
   is measured by script and written to --cta-w, because a width cannot
   animate to auto and hard-coding it would break the moment the copy or
   the type size changed. */
.cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 9px;
  height: 35px;
  width: 35px;
  /* 9.5 + 16 + 9.5 = 35, so the arrow is exactly centred while closed */
  padding-inline: 9.5px;
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--r-pill);
  overflow: hidden;
  white-space: nowrap;
  transition: width 0.62s var(--ease), padding-inline 0.62s var(--ease);
}
.cta:hover, .cta:focus-visible {
  width: var(--cta-w, 300px);
  padding-inline: 18px 9.5px;
}
.cta__text {
  font-size: 0.8125rem; font-weight: 600; letter-spacing: -0.01em;
  opacity: 0;
  transform: translateX(8px);
  transition: opacity 0.34s var(--ease) 0.06s, transform 0.5s var(--ease) 0.06s;
}
.cta:hover .cta__text, .cta:focus-visible .cta__text {
  opacity: 1; transform: none;
}
.cta__arrow {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 16px; height: 16px;
  color: var(--paper);
}
.cta__arrow svg { width: 16px; height: 16px; display: block; }
.cta:hover .cta__arrow, .cta:focus-visible .cta__arrow {
  animation: nudge 1.5s var(--ease) infinite;
}
@keyframes nudge {
  0%, 62%, 100% { transform: translateX(0); }
  30%           { transform: translateX(4px); }
}

/* the old pill is still used inside the contact block */
.btn {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 11px 20px;
  background: var(--ink); color: var(--paper);
  border-radius: var(--r-pill);
  font-size: 0.875rem; font-weight: 600; letter-spacing: -0.01em;
  white-space: nowrap;
  transition: transform 0.45s var(--ease), opacity 0.3s;
}
.btn:hover { transform: translateY(-2px); }
.btn__arrow {
  display: grid; place-items: center;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--paper); color: var(--ink);
  font-size: 10px; line-height: 1;
}
/* ---------- MOBILE MENU ----------
   Below 860px the inline nav is replaced by a full-screen menu, so
   Work and Contact stay reachable on a phone. */
.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;
}

.menu-btn {
  display: none;
  position: relative;
  width: 46px; height: 46px;          /* comfortable tap target */
  margin-right: -10px;
  place-items: center;
  z-index: 66;
}
/* Three bars of identical width, the outer two placed by a top offset
   from the middle one rather than by a transform. The transform was
   also what the open state animates, so the two were fighting and the
   bars landed on different sub-pixels — which is why they looked ragged
   and unequal. Offsets position, transforms animate. */
.menu-btn__bars {
  position: relative;
  width: 22px; height: var(--rule-w);
  background: var(--ink);
  transition: background-color 0.2s;
}
.menu-btn__bars::before,
.menu-btn__bars::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%; height: 100%;
  background: var(--ink);
  transition: top 0.3s var(--ease), transform 0.4s var(--ease);
}
.menu-btn__bars::before { top: -6px; }
.menu-btn__bars::after  { top: 6px; }
body.menu-open .menu-btn__bars { background: transparent; }
body.menu-open .menu-btn__bars::before { top: 0; transform: rotate(45deg); }
body.menu-open .menu-btn__bars::after  { top: 0; transform: rotate(-45deg); }

.menu {
  position: fixed; inset: 0; z-index: 64;
  background: var(--paper);
  display: flex; flex-direction: column; justify-content: center;
  gap: clamp(28px, 5vh, 56px);
  opacity: 0; visibility: hidden;
  transform: translateY(-14px);
  transition: opacity 0.45s var(--ease), transform 0.55s var(--ease), visibility 0.45s;
}
body.menu-open .menu { opacity: 1; visibility: visible; transform: none; }

.menu__links { display: grid; gap: 4px; }
.menu__links a {
  display: block;
  font-size: clamp(2.4rem, 12vw, 3.6rem);
  font-weight: 700;
  letter-spacing: -0.045em;
  line-height: 1.1;
  padding: 10px 0;
}
.menu__foot { display: grid; gap: 10px; padding-top: clamp(20px, 4vh, 40px); border-top: var(--rule-w) solid var(--rule); }
.menu__foot a { font-size: 1rem; font-weight: 500; padding: 6px 0; }

@media (max-width: 860px) {
  .nav { display: none; }
  .btn { display: none; }
  .menu-btn { display: grid; }

  /* A three-track grid with the middle track empty cannot hold the bars
     beside the disc — an auto column sized by a hidden nav is zero wide,
     so the bars land in the middle of the screen. A flex row does it
     properly: the name pushed left, the bars and the disc travelling
     together on the right, in that order. */
  .site-head {
    display: flex;
    align-items: center;
    gap: clamp(14px, 4vw, 22px);
  }
  .site-head > .brand    { margin-right: auto; }
  .site-head > .menu-btn { order: 2; margin-right: 0; }
  .site-head > .cta      { order: 3; }

  /* The bars are the one place the hairline is set aside. A 1px stroke
     is a drawing instruction, not a touch control — at arm's length on a
     phone it disappears. 3px, and the stack spans the disc's 35px so the
     two read as a matched pair rather than two unrelated sizes. */
  .menu-btn { width: 40px; height: 35px; }
  /* rounded caps, matching the arrow inside the disc — its stroke is
     drawn round too, so the pair share a finish as well as a height */
  .menu-btn__bars,
  .menu-btn__bars::before,
  .menu-btn__bars::after { width: 26px; height: 3px; border-radius: var(--r-pill); }
  .menu-btn__bars::before { top: -13px; }
  .menu-btn__bars::after  { top: 13px; }
}
@media (min-width: 861px) {
  .menu { display: none; }
}

/* ---------- 6. HERO — the frame rises, then turns ---------- */
/* The block is taller than the screen and its inner sticks, so the extra
   height is spent lifting the frame and turning it rather than moving
   the page. The column inside is laid out normally and slid by script:
   on arrival it sits low, with only the top edge of the frame showing
   under the statement. */
.tw { position: relative; height: 380vh; }
.tw__inner {
  position: sticky; top: 0;
  height: 100svh;
  overflow: hidden;
  /* the turn needs depth, or it reads as a flat horizontal squash */
  perspective: 2200px;
  perspective-origin: 50% 45%;
}
.tw__reel {
  position: absolute; left: 0; right: 0; top: 0;
  padding-top: calc(var(--head-h) + clamp(40px, 9vh, 110px));
}

.tw__copy { padding-bottom: clamp(60px, 11vh, 140px); }

.tw__stage { display: flex; flex-direction: column; gap: 14px; }

.tw__card {
  position: relative;
  width: 100%;
  aspect-ratio: 21 / 9;
  transform-style: preserve-3d;
}
.tw__face {
  position: absolute; inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--wire);
  box-shadow: 0 30px 80px rgba(10,10,10,0.10);
}
.tw__face--back { transform: rotateY(180deg); }
.tw__face video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
/* the front holds two projects — the second is swapped in while the face
   is turned away, so the change is never seen */
.tw__face video.is-off { opacity: 0; }

.tw__cap { display: flex; align-items: baseline; gap: 14px; }

/* Plain. No knockout, no film, no pills — ink on paper at 36, semibold,
   flush left on the same line as your name in the header. The two-tone
   split is the only thing doing any work, and that is enough. */
.ed__statement {
  margin: 0;
  font-size: clamp(1.5rem, 2.5vw, 2.25rem);   /* 36 at 1440 */
  line-height: 1.34;
  letter-spacing: -0.018em;
  font-weight: 600;
  text-align: left;
  max-width: min(880px, 100%);
}
/* scroll-linked, so it is the page that drives it and not a timer */
[data-veil] { will-change: transform, opacity, filter; }
.ed__claim { display: block; }
.ed__dim   { display: block; color: var(--ink-25); }

/* the marks follow the turning, above the work */
.clients { padding-top: var(--sp-l); }
.ed__band { padding-bottom: clamp(14px, 2.4vh, 28px); }

@media (max-width: 900px) { .tw { height: 300vh; } }

/* nothing rises and nothing turns — the first project sits in its frame */
@media (prefers-reduced-motion: reduce) {
  .tw { height: auto; }
  .tw__inner { position: static; height: auto; }
  .tw__reel { position: static; }
  .tw__card { transform: none !important; }
  .tw__face--back { display: none; }
}

/* ---------- 7. WIREFRAME MEDIA SLOT ---------- */
.slot {
  position: relative;
  width: 100%;
  background: var(--wire);
  border-radius: var(--r-lg);
  overflow: hidden;
  display: grid;
  place-items: center;
}
.slot::before {
  content: "";
  position: absolute; inset: 0;
  background-image:
    linear-gradient(to top right, transparent calc(50% - 0.5px), var(--ink-25) 50%, transparent calc(50% + 0.5px)),
    linear-gradient(to bottom right, transparent calc(50% - 0.5px), var(--ink-25) 50%, transparent calc(50% + 0.5px));
  opacity: 0.5;
  transition: opacity 0.6s var(--ease);
}
.slot::after {
  content: "";
  position: absolute; inset: 0;
  border: var(--rule-w) solid var(--ink-25);
  border-radius: inherit;
  pointer-events: none;
}
.slot__meta {
  position: relative; z-index: 2;
  text-align: center;
  padding: 18px 20px;
  background: var(--wire);
  border-radius: var(--r-sm);
  max-width: min(88%, 460px);
}
.slot__type {
  display: block;
  font-size: var(--t-label);
  letter-spacing: 0.16em; text-transform: uppercase; font-weight: 700;
  color: var(--ink); margin-bottom: 8px;
}
.slot__spec {
  display: block;
  font-family: var(--mono);
  font-size: 11px; line-height: 1.65;
  color: var(--ink-45);
}
.slot__note {
  display: block; margin-top: 8px;
  font-size: 11px; line-height: 1.6;
  color: var(--ink-45); font-style: italic;
}

.r-21x9 { aspect-ratio: 21 / 9; }
.r-16x9 { aspect-ratio: 16 / 9; }
.r-3x2  { aspect-ratio: 3 / 2; }
.r-4x3  { aspect-ratio: 4 / 3; }
.r-1x1  { aspect-ratio: 1 / 1; }
.r-4x5  { aspect-ratio: 4 / 5; }
.r-2x3  { aspect-ratio: 2 / 3; }
.r-8x5  { aspect-ratio: 8 / 5; }
.r-5x4  { aspect-ratio: 5 / 4; }
.r-2x1  { aspect-ratio: 2 / 1; }
/* the standing frame's proportion, shared by the work cards */
.r-550x700 { aspect-ratio: 550 / 700; }
/* an exact frame rather than a tidy ratio — 0.820, just short of 4:5 */
.r-1400x1707 { aspect-ratio: 1400 / 1707; }
@media (max-width: 700px) { .r-21x9 { aspect-ratio: 16 / 10; } }

/* a slot holding real media rather than a placeholder */
.slot > img, .slot > video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  z-index: 1;
}
.slot--filled::before { opacity: 0; }
.slot--filled::after { border-color: rgba(10, 10, 10, 0.07); }
.slot--filled .slot__meta {
  z-index: 2;
  background: rgba(255, 255, 255, 0.94);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

/* scroll-driven video frame — forward on scroll down, reverse on scroll up */
.video-frame {
  position: relative;
  width: 100%;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--wire);
}
.video-frame video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: opacity 0.15s linear;
}
.video-frame video.is-hidden { opacity: 0; }

.vf-note {
  position: absolute; z-index: 3;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  text-align: center;
  padding: 18px 20px;
  background: rgba(255,255,255,0.92);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border-radius: var(--r-sm);
  max-width: min(88%, 480px);
  transition: opacity 0.4s var(--ease);
}
body.clean .vf-note { opacity: 0; pointer-events: none; }

/* ---------- SHOWREEL CONTROLS ---------- */
/* click anywhere on the frame to pause or resume */
.reel-toggle {
  position: absolute; inset: 0; z-index: 3;
  display: grid; place-items: center;
  background: none; border: 0; padding: 0;
  cursor: pointer;
}
.reel-toggle:focus-visible { outline: 2px solid var(--paper); outline-offset: -6px; }
/* the spec box must not swallow the click */
[data-showreel] .slot__meta { pointer-events: none; }

.reel-play {
  width: clamp(56px, 5vw, 76px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: rgba(255,255,255,0.92);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  display: grid; place-items: center;
  opacity: 0; transform: scale(0.86);
  transition: opacity 0.35s var(--ease), transform 0.45s var(--ease);
}
.reel-play::after {
  content: "";
  width: 0; height: 0;
  margin-left: 4px;                /* optical centring of the triangle */
  border-left: 17px solid var(--ink);
  border-top: 11px solid transparent;
  border-bottom: 11px solid transparent;
}
[data-showreel].is-paused .reel-play { opacity: 1; transform: none; }


/* a soft scrim so white controls stay legible over any frame */
.reel-scrim {
  position: absolute; inset: auto 0 0 0; z-index: 2;
  height: 34%;
  background: linear-gradient(to top, rgba(0,0,0,0.55), rgba(0,0,0,0));
  pointer-events: none;
}
body.clean .reel-scrim { opacity: 1; }

.reel-progress {
  position: absolute; z-index: 4;
  left: clamp(16px, 1.6vw, 26px);
  right: clamp(16px, 1.6vw, 26px);
  bottom: clamp(16px, 1.6vw, 26px);
  display: flex; align-items: center; gap: 14px;
}
.reel-time {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 11px; letter-spacing: 0.06em;
  color: rgba(255,255,255,0.85);
  font-variant-numeric: tabular-nums;
}
.reel-bar {
  position: relative;
  flex: 1 1 auto;
  height: 18px;                 /* generous hit area */
  padding: 0; border: 0; background: none;
  cursor: pointer;
  display: flex; align-items: center;
}
.reel-bar::before {
  content: "";
  position: absolute; left: 0; right: 0;
  height: var(--rule-w);
  background: rgba(255,255,255,0.3);
  transition: height 0.25s var(--ease);
}
.reel-bar:hover::before { height: 4px; }
.reel-bar__fill {
  position: absolute; left: 0;
  height: var(--rule-w); width: 0%;
  background: var(--paper);
  transition: height 0.25s var(--ease);
}
.reel-bar:hover .reel-bar__fill { height: 4px; }

/* right-hand sound button must not sit on top of the bar */
[data-showreel] .sound-btn { bottom: calc(clamp(16px, 1.6vw, 26px) + 34px); }

@media (max-width: 620px) {
  .reel-time { display: none; }
}

/* sound toggle — a real control, so it stays visible in clean mode */
.sound-btn {
  position: absolute; z-index: 4;
  right: clamp(16px, 1.6vw, 26px); bottom: clamp(16px, 1.6vw, 26px);
  display: inline-flex; align-items: center; gap: 9px;
  padding: 9px 15px;
  background: rgba(10, 10, 10, 0.72);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--paper);
  border-radius: var(--r-pill);
  font-size: var(--t-label); letter-spacing: 0.14em;
  text-transform: uppercase; font-weight: 600;
  transition: background 0.35s var(--ease), transform 0.35s var(--ease);
}
.sound-btn:hover { background: var(--ink); transform: translateY(-2px); }
.sound-btn__icon {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--paper); opacity: 0.45;
  transition: opacity 0.3s, box-shadow 0.3s;
}
.sound-btn[aria-pressed="true"] .sound-btn__icon {
  opacity: 1;
  box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.22);
}

.vf-state {
  position: absolute; z-index: 3;
  left: clamp(14px, 1.4vw, 22px); top: clamp(14px, 1.4vw, 22px);
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 13px;
  background: var(--ink); color: var(--paper);
  border-radius: var(--r-pill);
  font-family: var(--mono); font-size: 10px;
  letter-spacing: 0.12em; text-transform: uppercase;
}
body.clean .vf-state { opacity: 0; }

/* split-screen video slot — two panels in ONE file */
.slot--duo .duo-line {
  position: absolute; left: 50%; top: 0; bottom: 0;
  width: 1px; background: var(--ink-25); z-index: 2;
}
.duo-tag {
  position: absolute; top: 16px; z-index: 2;
  font-family: var(--mono); font-size: 10px;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-45);
}
.duo-tag--l { left: 18px; }
.duo-tag--r { right: 18px; }
body.clean .duo-line, body.clean .duo-tag { opacity: 0; }

body.clean .slot__meta { opacity: 0; }
body.clean .slot::before { opacity: 0.16; }

/* ---------- 8. CLIENT LOGO MARQUEE ---------- */
.marquee {
  position: relative;
  overflow: hidden;
  /* 75px overall: 55px of logo with 9px of air above and below,
     plus the two hairlines. Height is set rather than derived so the
     band stays exactly this deep whatever the marks do. */
  height: 75px;
  display: flex;
  align-items: center;
  border-top: var(--rule-w) solid var(--rule);
  border-bottom: var(--rule-w) solid var(--rule);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
}
.marquee__track {
  display: flex;
  align-items: center;
  width: max-content;
  animation: marquee 34s linear infinite;
}
.marquee:hover .marquee__track { animation-play-state: paused; }
@keyframes marquee { to { transform: translateX(-50%); } }

.marquee__group { display: flex; flex-shrink: 0; align-items: center; }

/* Every V2 file is trimmed to the mark and exported at the same 230px
   height, so height is the optical normaliser — set it on the image
   itself. A percentage height here would be circular (the slot's width
   depends on the image, the image's height depended on the slot) and
   the mark would fall back to its natural 230px and get clipped by the
   band. max-width has to be released too, or the global img rule pulls
   the wider marks back in. */
:root { --logo-h: 44px; }

.logo-slot {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  margin-inline: 75px;        /* 75 + 75 = 150px between neighbours */
  opacity: 0.72;
  transition: opacity 0.4s var(--ease);
}

@media (max-width: 760px) {
  :root { --logo-h: 26px; }
  .marquee { height: 52px; }
  .logo-slot { margin-inline: 34px; }
}
.marquee:hover .logo-slot { opacity: 1; }

/* contain, never cover — the whole file is shown, nothing is cropped */
.logo-slot img {
  height: var(--logo-h);
  width: auto;
  max-width: none;
  object-fit: contain;
}

.marquee__head { display: flex; justify-content: center; padding-bottom: 18px; }
.marquee__cap { display: flex; justify-content: center; padding-top: 22px; }

/* ---------- 9. SECTION INDEX ----------
   A centred display heading was the one thing on the page still shouting
   at a different volume, and pointing the wrong way — everything else is
   flush left. The references label their sections instead: small, tracked,
   ruled, with a count beside it. The work is the headline. */
.sec-index {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 18px;
  margin-top: var(--sp-xl);
  padding-bottom: 16px;
  border-bottom: var(--rule-w) solid var(--rule);
}
/* three quarters of the statement, and derived from it rather than
   typed out — change the statement and these follow */
.sec-index__head {
  font-size: calc(var(--t-display) * 0.75);   /* 48 at 1440 */
  line-height: 1.06;
  letter-spacing: -0.035em;
  font-weight: 600;
}
.sec-index .num { color: var(--ink-45); }
.sec-index__go {
  margin-left: auto;
  font-size: var(--t-small);
  color: var(--ink-45);
  transition: color 0.3s var(--ease);
}
.sec-index__go:hover { color: var(--ink); }

/* the first row follows its index closely — the rule is the separator */
.sec-index + .work .p-row:first-of-type { margin-top: var(--sp-m); }

/* ---------- 10. PROJECT ROWS (edge to edge) ---------- */
.work { padding-bottom: var(--sp-l); }

.p-row {
  display: block;
  padding-inline: var(--bleed-inset);
  margin-top: var(--sp-l);
}
.p-row:first-of-type { margin-top: 0; }

/* a row that sits on the grid instead of running to the viewport edge —
   so it gets margins, and its corners round again */
.p-row--inset { padding-inline: 0; }
.p-row--inset .p-row__media { border-radius: var(--r-lg); }
.p-row--inset .p-row__media .slot { border-radius: var(--r-lg); }
.p-row--inset .p-row__media .slot::after { border-radius: var(--r-lg); }

.p-row__media {
  position: relative;
  border-radius: var(--bleed-radius);
  overflow: hidden;
  transform: translateZ(0);
}
.p-row__media .slot {
  border-radius: 0;
  transition: transform 1.3s var(--ease);
}

/* The showreel is shown, not teased. 1920 x 1080 in a 16:9 frame is an
   exact fit, so nothing needs to be cropped — and neither the drift nor
   the hover scale is allowed near it, because both work by making the
   media larger than its frame. Anything you sit through for 43 seconds
   should be the size it was rendered at. */
.p-row__media--flat .slot { transform: none !important; }
.p-row__media--flat video,
.p-row__media--flat img { height: 100%; inset: 0; }
.p-row__media .slot::after { border-radius: 0; }
.p-row:hover .p-row__media .slot { transform: scale(1.022); }

/* Footage that drifts inside its frame as the row crosses the screen.
   120% tall and hung 10% above the frame, so there is 10% of the frame's
   height in slack at each end. site.js travels it 7% of its own height,
   which is 8.4% of the frame — inside the slack, so the frame's grey
   never shows. Scoped past .slot--filled on purpose: at equal
   specificity the base video rule would win and none of this applies. */
[data-drift] .slot--filled video,
[data-drift] .slot--filled img {
  height: 120%;
  inset: -10% 0 auto 0;
  will-change: transform;
}

.playbar {
  position: absolute; left: 0; bottom: 0;
  height: var(--rule-w); width: 100%;
  background: rgba(10,10,10,0.1);
  opacity: 0; transition: opacity 0.4s var(--ease);
  z-index: 3;
}
.playbar span { display: block; height: 100%; width: 0%; background: var(--ink); }
.p-row:hover .playbar { opacity: 1; }
.p-row:hover .playbar span { animation: play 9s linear infinite; }
@keyframes play { from { width: 0%; } to { width: 100%; } }

.play-flag {
  position: absolute; z-index: 3;
  top: clamp(16px, 1.6vw, 26px); left: clamp(16px, 1.6vw, 26px);
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 14px;
  background: var(--ink); color: var(--paper);
  border-radius: var(--r-pill);
  font-size: var(--t-label); letter-spacing: 0.15em; text-transform: uppercase; font-weight: 600;
  opacity: 0; transform: translateY(-6px);
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease);
}
.p-row:hover .play-flag { opacity: 1; transform: none; }
.play-flag::before {
  content: ""; width: 0; height: 0;
  border-left: 6px solid var(--paper);
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
}

.p-row__foot {
  display: flex; justify-content: space-between; align-items: flex-end; gap: 24px;
  padding-top: 22px;
}
.p-row__lead { display: flex; align-items: baseline; gap: clamp(12px, 1.4vw, 22px); }

.p-row__lead .h2 { position: relative; }
.p-row__lead .h2::after {
  /* set in em so the gap holds at any size — far enough below the
     baseline to clear a descender rather than crowding it */
  content: ""; position: absolute; left: 0; bottom: -0.26em;
  width: 100%; height: var(--rule-w); background: var(--ink);
  transform: scaleX(0); transform-origin: left;
  transition: transform 0.7s var(--ease);
}
.p-row:hover .p-row__lead .h2::after { transform: scaleX(1); }

.tags { display: flex; gap: 8px; flex-wrap: wrap; }
.tag {
  padding: 7px 14px;
  border: var(--rule-w) solid var(--rule);
  border-radius: var(--r-pill);
  font-size: 0.8125rem; font-weight: 500; color: var(--ink-70);
  transition: border-color 0.4s, color 0.4s;
}
.p-row:hover .tag { border-color: var(--ink-25); color: var(--ink); }

@media (max-width: 760px) {
  .p-row__foot { flex-direction: column; align-items: flex-start; gap: 14px; }
}

/* ---------- 10b. THE MASTHEAD ----------
   One screen, three things: the statement, a data line, the marks. The
   statement sits on the optical centre rather than the true one, so the
   white space above and below is not equal — it never is in print. */
.mast {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  padding-top: var(--head-h);
}
.mast__body {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  padding-block: clamp(60px, 10vh, 140px);
}
/* hung off the marquee's top rule: place left, time right */
.mast__foot { flex: 0 0 auto; }
/* hung off the marquee's top rule: place left, time right */
.mast__data {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 20px;
  padding-bottom: 13px;
}
.mast__data .num { color: var(--ink-45); font-variant-numeric: tabular-nums; }

.mast__stage { position: relative; flex: 1 1 auto; display: flex; }
.mast__body  { position: relative; z-index: 2; }

/* ---------- 9b. THE STANDING FRAME ----------
   Your name set either side of a frame that holds its place while the
   page moves past it. The frame never travels and never turns — it is
   the fixed point of the composition — and the only thing that changes
   is what is in it: five plates, cross-faded, one per fifth of the
   scroll. The role sits top right, small, against the weight of the
   name.

   The frame's proportion, 550 × 700, is the proportion the Selected
   work cards use as well, so the landing page and the index are cut to
   the same shape. */
/* 220vh. The name closing and the plates changing are the same
   movement — both run across the same 88% of the scroll — so the last
   plate lands exactly as the last of the name goes. About 240px of
   scroll per plate, up from 140: enough to read each one without
   having to hold still for it. */
.lean { position: relative; height: 220vh; }

.lean__inner {
  position: sticky; top: 0;
  height: 100svh;
  display: grid; place-items: center;
  padding-top: var(--head-h);
  overflow: hidden;
}

.lean__stage {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  column-gap: clamp(18px, 3vw, 56px);
}

/* 5.6vw, not 6.6. The two halves sit in 1fr columns either side of the
   frame, and at 6.6 "Ajlyakin" outgrew its column between 1200 and 1400
   and pushed into the frame. 5.6 clears the column at every width from
   900 up — checked at 940, 1024, 1280, 1440, 1600 and 1920 — and nowrap
   guarantees it never breaks in two rather than overflowing quietly. */
.lean__word {
  margin: 0;
  min-width: 0;
  white-space: nowrap;
  font-size: clamp(2.2rem, 5.6vw, 96px);
  line-height: 0.92;
  letter-spacing: -0.05em;
  font-weight: 700;
}
.lean__word--l { justify-self: end;   text-align: right; }
.lean__word--r { justify-self: start; text-align: left; }
/* the two halves of the name close on the centre as you scroll and pass
   behind the frame — so the frame has to sit above them */
.lean__word { position: relative; z-index: 1; will-change: transform, opacity; }
.lean__card { z-index: 2; }

/* THE ROLE.
   18px, on the left alignment line — the same line your name in the
   header sits on — with its top edge on the top edge of the frame.

   No measuring needed for the vertical: the stage is a centred grid
   whose height is the card's, so the stage's top edge and the card's
   top edge are the same line. top:0 puts the text on it. */
.lean__role {
  position: absolute;
  left: var(--gutter);
  top: 0;
  margin: 0;
  font-size: 18px;
  line-height: 1.45;
  letter-spacing: 0;
  font-weight: 500;
  color: var(--ink-70);
}

/* The height budget is folded into the width rather than clamped after
   it. width + aspect-ratio + max-height is a contradiction — the height
   gets capped, the width does not follow, and the frame goes out of
   proportion on any short screen. Working out what width a 72svh-tall
   frame would have (72svh × 550/700) and taking the smallest of the
   three keeps 550 × 700 exact at every size. */
.lean__card {
  position: relative;
  z-index: 2;
  overflow: hidden;
  width: min(550px, 34vw, calc(72svh * 550 / 700));
  aspect-ratio: 550 / 700;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--wire);
  box-shadow: 0 40px 90px rgba(10, 10, 10, 0.13);
}
/* A cut. No fade, no travel — the next plate is simply there.

   The faces are stacked in the same place and only one is shown, with
   no transition on the property at all: a transition of any length is
   a fade, and any movement is a pan. The scroll decides which one is
   up and the change happens in a single frame. */
.lean__strip { position: absolute; inset: 0; }
.lean__face {
  position: absolute; inset: 0;
  opacity: 0;
  transition: none;
}
.lean__face.is-on { opacity: 1; }
.lean__face > img, .lean__face > video { width: 100%; height: 100%; object-fit: cover; }

/* THE CUE.
   A hairline with a black segment falling down it, and the word Scroll
   beside it — hung on the right alignment line, opposite the role at
   top left, so the two balance across the screen.

   Drawn in the same rule weight as everything else rather than as an
   arrow or a mouse icon, and it leaves the moment you scroll: an
   instruction you have already followed is clutter. */
.scroll-cue {
  position: absolute;
  /* the right alignment line — the one the disc in the header sits on.
     .lean__inner runs the full width, so it needs the same expression
     .wrap arrives at rather than a plain gutter. */
  right: max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter)));
  bottom: var(--cue-b, clamp(18px, 4vh, 44px));
  z-index: 4;
  display: flex;
  align-items: flex-end;      /* word and line share a bottom edge */
  gap: 14px;
  transition: opacity 0.45s var(--ease);
  pointer-events: none;
}
/* sentence case, in the site's small copy style rather than a label */
.scroll-cue .label {
  font-size: var(--t-small);
  letter-spacing: 0;
  text-transform: none;
  font-weight: 500;
  color: var(--ink-45);
  line-height: 1;
}
.scroll-cue__line {
  position: relative;
  width: var(--rule-w);
  height: clamp(34px, 6vh, 52px);
  background: var(--rule);
  overflow: hidden;
}
.scroll-cue__line::after {
  content: "";
  position: absolute; left: 0; top: 0;
  width: 100%; height: 42%;
  background: var(--ink);
  animation: cue 2s var(--ease-io) infinite;
}
@keyframes cue {
  0%        { transform: translateY(-110%); }
  55%, 100% { transform: translateY(260%); }
}

@media (prefers-reduced-motion: reduce) {
  .scroll-cue__line::after { animation: none; transform: none; height: 100%; }
}

/* Which plate is up, in the site's one line weight — centred under the
   frame rather than off to the side.

   The stage is a centred grid whose height is the card's, so its bottom
   edge and the card's bottom edge are the same line; top:100% hangs the
   marks just below it. The 1fr columns either side are equal, so 50% of
   the stage is the card's centre. */
/* Centred under the frame and sitting on the same bottom line as the
   word Scroll, so the two read as one row across the screen. Both are
   children of .lean__inner and both take --cue-b, so they cannot drift
   apart. */
.lean__ticks {
  position: absolute;
  left: 50%;
  bottom: var(--cue-b, clamp(18px, 4vh, 44px));
  transform: translateX(-50%);
  display: flex; gap: 7px;
  z-index: 4;
}
.lean__tick {
  width: 34px; height: var(--rule-w);
  background: var(--rule);
  transition: background-color 0.5s var(--ease);
}
.lean__tick.is-on { background: var(--ink); }

/* the plates use the site's own spec plate — the same component the
   case study pages use, so a slot reads the same wherever it is */
.lean__face.slot { border-radius: 0; }
.lean__face .slot__meta { max-width: min(86%, 380px); }

@media (max-width: 900px) {
  .lean { height: 195vh; }
  /* tighter: the frame is centred by script, and the type sits close
     to it rather than being spaced away from it */
  .lean__stage { grid-template-columns: 1fr; justify-items: center; row-gap: 10px; }
  .lean__word { font-size: clamp(2rem, 11vw, 3.6rem);
                text-align: center !important; justify-self: center !important; }
  .lean__card { width: min(340px, 62vw, calc(46svh * 550 / 700)); }
  .lean__role { position: static; transform: none; text-align: center;
                font-size: 16px; margin-bottom: 0; }
  /* pinned, not stacked: the guide keeps the marks centred under the
     frame and Scroll on the right line, both on one bottom row */
  .lean__ticks { --cue-b: clamp(14px, 3vh, 28px); }
  .scroll-cue  { --cue-b: clamp(14px, 3vh, 28px); }
}

/* A phone on its side is 700-900 wide and under 450 tall: too narrow for
   the full composition and far too short for the stacked one. It gets
   the three-column stage back, with everything sized off the height. */
@media (max-height: 520px) and (min-width: 620px) and (orientation: landscape) {
  .lean { height: 195vh; }
  .lean__inner { padding-top: calc(var(--head-h) * 0.7); }
  .lean__stage { grid-template-columns: 1fr auto 1fr; justify-items: stretch;
                 row-gap: 0; column-gap: clamp(14px, 2.4vw, 32px); }
  .lean__card { width: min(550px, 34vw, calc(58svh * 550 / 700)); }
  .lean__word { font-size: clamp(1.6rem, 4.6vw, 48px);
                text-align: initial; justify-self: auto; }
  .lean__word--l { justify-self: end;   text-align: right; }
  .lean__word--r { justify-self: start; text-align: left; }
  .lean__role { position: absolute; left: var(--gutter); top: 0;
                transform: translateY(-50%); text-align: left; }
  .lean__ticks { position: absolute; left: 50%; top: 100%;
                 transform: translateX(-50%); margin-top: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  .lean { height: auto; }
  .lean__inner { position: static; height: auto; padding-block: 12vh; }
}

/* ---------- 9c. THE STATEMENT BEFORE THE WORK ----------
   Set exactly the way the closing invitation is set: 39, semibold,
   flush left, revealed from behind a mask. The two of them bracket the
   page — one before the work, one after it. */
/* Leading opened from 1.06 to 1.24. At 39 over two lines the display
   leading closed the descenders of the first line onto the ascenders of
   the second, and the mask's overflow was cutting the tails. The mask is
   padded top and bottom and the padding pulled back off the margin, so
   nothing is clipped and the spacing around the block is unchanged. */
.say { padding-block: var(--sp-block) 0; }
/* The break is set by hand in the markup, so the measure has to be wide
   enough to hold the longest of the two lines — otherwise the browser
   adds breaks of its own on top of yours, which reads worse than not
   setting them at all. Line one is 39 characters, about 760px at this
   size, so 900 clears it with room. Below 900 the line cannot fit at
   all, so the break is released and it reflows. */
.say .h1 {
  max-width: min(900px, 100%);
  line-height: 1.24;
  letter-spacing: -0.032em;
  text-wrap: nowrap;
}
@media (max-width: 900px) {
  .say .h1 { text-wrap: balance; }
  .say .h1 br { display: none; }
}
.say .mask {
  padding-block: 0.10em 0.30em;
  margin-block: -0.10em -0.24em;
}

/* ---------- 9d. SELECTED WORK, CUT TO THE SAME SHAPE ----------
   Three cards at 550 × 700 — the proportion of the frame on the landing
   page. The whole section is one row, so the work reads as a set rather
   than a list you scroll through. */
.pw { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: var(--col-gap); margin-top: var(--sp-block); }

.pw__item { display: block; }

/* the discipline, in a pill above its frame */
.pw__tag {
  display: inline-block;
  margin-bottom: 14px;
  padding: 7px 14px;
  border: var(--rule-w) solid var(--rule);
  border-radius: var(--r-pill);
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink-70);
  transition: border-color 0.4s var(--ease), color 0.4s var(--ease);
}
.pw__item:hover .pw__tag { border-color: var(--ink-25); color: var(--ink); }
.pw__frame {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--wire);
}
.pw__frame .slot__meta { max-width: min(88%, 360px); z-index: 3; }

/* THE COVER.
   A <video> only ever shows its poster before it has played once —
   after that, pausing shows whatever frame it stopped on, and setting
   currentTime to 0 shows frame one of the film rather than the image
   you chose. So the cover is a real element sitting over the video,
   and it is hidden only while the film is actually running.

   Tied to the video's own play and pause events rather than to hover,
   so it is right on a touch screen too — where playback is decided by
   what is on screen and there is no hover at all. */
.cover {
  position: absolute; inset: 0;
  z-index: 2;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: opacity 0.28s var(--ease);
}
.is-playing .cover { opacity: 0; }
.pw__frame img, .pw__frame video {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 1.3s var(--ease);
}
.pw__item:hover .pw__frame img,
.pw__item:hover .pw__frame video { transform: scale(1.04); }

.pw__foot { display: flex; align-items: baseline; justify-content: space-between;
            gap: 16px; padding-top: 16px; }
.pw__foot .num { flex: 0 0 auto; }
.pw__lead { display: flex; align-items: baseline; gap: clamp(10px, 1.2vw, 18px); }
.pw__lead h3 { position: relative; margin: 0;
               font-size: var(--t-h2); line-height: 1.16;
               letter-spacing: -0.03em; font-weight: 600; }
.pw__lead h3::after {
  content: ""; position: absolute; left: 0; bottom: -0.26em;
  width: 100%; height: var(--rule-w); background: var(--ink);
  transform: scaleX(0); transform-origin: left;
  transition: transform 0.7s var(--ease);
}
.pw__item:hover .pw__lead h3::after { transform: scaleX(1); }


@media (max-width: 900px) {
  .pw { grid-template-columns: 1fr; gap: var(--sp-block); }
}
/* the three cards hold three across down to 900; below that one, full
   width, which at 320 is a 280 x 356 frame — still the same proportion */
@media (max-width: 380px) {
  :root { --gutter: 18px; }
  .brand { font-size: 18px; }
  .pw__lead h3 { font-size: 1.2rem; }
}

/* ---------- 10b-2. SECTION RHYTHM ----------
   Every section on every page is spaced by the same token, so the gap
   between the masthead and the work is the gap between the work and the
   showreel is the gap before the footer. Section indexes no longer carry
   a margin of their own — the section owns the space. */
/* On the home page the sections meet. #work already holds a full
   screen and centres inside it, so it brings its own air; anything
   added on top only pushed the showreel off the fold. --sp-sec still
   governs the case study pages. */
#work, #showreel { padding-block: 0; }
.mast { padding-block: 0; padding-top: var(--head-h); }
#work .sec-index, #showreel .sec-index { margin-top: 0; }

/* the work holds a full screen — index and film sit on the optical
   centre with the section's own rhythm above and below */
#work {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* ---------- 10c. THE INDEX ----------
   The work as one composition rather than a scroll. The list on the left
   is the transport; the screen on the right holds all three films in a
   single strip and travels between them. Nothing has to be scrolled to
   see the work — the cursor operates it, and the keyboard does too.

   List and screen share a top and a bottom line: the rows are flex
   children of a stretched column, so they divide whatever height the
   screen turns out to be. Change the frame's ratio and the rows follow. */
.ix {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--col-gap);
  align-items: stretch;
  margin-top: var(--sp-m);
}
.ix__list  { grid-column: 1 / 5;  display: flex; flex-direction: column; }
.ix__panel { grid-column: 5 / -1; }

.ix__row {
  position: relative;
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  padding-block: 16px;
  border-top: var(--rule-w) solid var(--rule);
}
.ix__row:last-child { border-bottom: var(--rule-w) solid var(--rule); }

/* the rule above the live row inks itself in, left to right */
.ix__row::before {
  content: "";
  position: absolute; left: 0; top: calc(var(--rule-w) * -1);
  width: 100%; height: var(--rule-w);
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.7s var(--ease);
}
.ix__row.is-on::before { transform: scaleX(1); }

.ix__meta { display: flex; align-items: baseline; gap: 14px; }
.ix__num {
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  color: var(--ink-25);
  transition: color 0.5s var(--ease);
}
.ix__disc {
  font-size: var(--t-small);
  color: var(--ink-25);
  transition: color 0.5s var(--ease);
}
.ix__row.is-on .ix__num  { color: var(--ink); }
.ix__row.is-on .ix__disc { color: var(--ink-45); }

/* Weight is the emphasis, not size or colour alone. Plus Jakarta is
   loaded as a variable font, so the axis itself is animated — the name
   thickens from 300 to 600 rather than snapping between two cuts. */
.ix__title {
  font-size: var(--t-h1);              /* 39 */
  line-height: 1.06;
  letter-spacing: -0.035em;
  font-weight: 400;
  font-variation-settings: "wght" 300;
  color: var(--ink-25);
  transition: color 0.5s var(--ease), font-variation-settings 0.6s var(--ease);
}
.ix__row.is-on .ix__title { color: var(--ink); font-variation-settings: "wght" 600; }

.ix__thumb { display: none; }

/* the screen */
.ix__screen {
  position: relative;
  display: block;
  aspect-ratio: 21 / 9;                /* the source ratio exactly — nothing is cropped */
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--wire);
}
/* a hairline inside the corner, so the frame reads as glass on white
   instead of a shape that stops */
.ix__screen::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(10, 10, 10, 0.08);
  pointer-events: none;
  z-index: 2;
}
.ix__strip {
  position: absolute; left: 0; top: 0;
  width: 100%; height: 300%;
  transition: transform 0.95s var(--ease);
  will-change: transform;
}
.ix__cell { position: relative; width: 100%; height: 33.3333%; }
.ix__cell video,
.ix__cell img { width: 100%; height: 100%; object-fit: cover; }

/* transport under the screen */
.ix__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding-top: 18px;
}
.ix__ticks { display: flex; gap: 7px; }
.ix__tick {
  width: 38px; height: var(--rule-w);
  background: var(--rule);
  transition: background-color 0.5s var(--ease);
}
.ix__tick.is-on { background: var(--ink); }
.ix__go {
  font-size: var(--t-small);
  color: var(--ink-45);
  transition: color 0.4s var(--ease);
}
.ix__screen:hover + .ix__bar .ix__go,
.ix__go:hover { color: var(--ink); }

/* Under 900 there is no cursor to operate it with, so the mechanism is
   dropped rather than faked: the screen goes, and each row carries its
   own still. Same list, same order, no hover. */
@media (max-width: 900px) {
  .ix { display: block; }
  .ix__panel { display: none; }
  .ix__row {
    flex: none;
    display: block;
    padding-block: 22px;
  }
  .ix__row::before { display: none; }
  .ix__title, .ix__num, .ix__disc { color: var(--ink); }
  .ix__title { font-variation-settings: "wght" 600; }
  .ix__thumb {
    display: block;
    width: 100%;
    aspect-ratio: 21 / 9;
    object-fit: cover;
    border-radius: var(--r-md);
    margin-top: 16px;
    background: var(--wire);
  }
}

@media (prefers-reduced-motion: reduce) {
  .ix__strip, .ix__title, .ix__row::before { transition: none; }
}

/* ---------- 10d. RESPONSE ----------
   Four sizes were checked by hand: 375, 768, 1024 and 1440. Below 900
   the index loses its screen; below 760 the header loses its nav; the
   spacing tokens do the rest on their own, because both are viewport
   relative and both have a floor. */
@media (max-width: 900px) {
  /* stacked, the last card's name and its underline ran straight into
     the showreel's label. The section takes its own bottom margin back. */
  #work { min-height: 0; display: block; padding-bottom: var(--sp-sec); }
  .ed__statement { max-width: 100%; }
}
@media (max-width: 640px) {
  :root { --sp-sec: 76px; --sp-block: 40px; --m-rise: 22px; --m-rise-lg: 40px; --m-blur: 8px; }
  .ed__statement br { display: none; }      /* let it reflow to the phone's measure */
  .ed__statement { text-wrap: balance; }
}

/* ---------- 11. CASE STUDY ---------- */
.cs-head { padding-top: calc(var(--head-h) + clamp(48px, 9vh, 110px)); padding-bottom: var(--sp-m); }
/* .cs-title and .mask are both single-class selectors, and .mask is
   further down the file — so .mask's -0.18em descender pull-back was
   winning and the title's gap was collapsing to nothing, which is why
   the rule below sat on the descenders. Two classes settles it. */
.cs-title.mask { margin-bottom: 30px; }
.cs-kicker { display: flex; align-items: center; gap: 12px; margin-bottom: 22px; }

/* The bar is hung off the rule above it: the first field starts exactly
   where the rule starts, the last one ends exactly where it ends, and
   the space left over is divided equally between them.

   A grid cannot do this. Equal grid columns align the left edge of each
   column, not the ink — so the last field's text starts at 77% and
   stops well short of the right end of the rule. space-between measures
   the fields, then shares out what is left, so both ends land on the
   rule and every gap is identical. The last field is set right so its
   final letter lands on the rule's end rather than its column's. */
.meta-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: clamp(20px, 3vw, 48px);        /* the floor; space-between adds the rest */
  padding-top: 20px;
  border-top: var(--rule-w) solid var(--rule);
}
.meta-row > div { flex: 0 0 auto; }
.meta-row > div:last-child { text-align: right; }

.meta-row div .label { display: block; margin-bottom: 6px; }
.meta-row div p { font-size: var(--t-small); }
.meta-row div p span { display: block; }

@media (max-width: 900px) {
  /* space-between stops helping once the fields wrap — they would land
     on ragged rows. Two even columns instead. */
  .meta-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 24px; }
  .meta-row > div:last-child { text-align: left; }
}
@media (max-width: 560px) {
  .meta-row { grid-template-columns: 1fr; row-gap: 20px; }
}


/* case study titles sit at the top of the scale; the extra leading is
   for the ones that run to two lines */
.display--tight { font-size: var(--t-display); line-height: 1.08; }

/* contribution list inside the intro triad */
.role-list { margin-top: 12px; display: grid; gap: 0; }
.role-list li {
  font-size: var(--t-small);
  color: var(--ink-70);
  padding: 7px 0;
  border-top: var(--rule-w) solid var(--rule);
}
.role-list li:last-child { border-bottom: var(--rule-w) solid var(--rule); }

.intro-triad {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(28px, 3.6vw, 64px);
  padding-block: var(--sp-l);
}
.intro-triad section { border-top: var(--rule-w) solid var(--ink); padding-top: 20px; }
.intro-triad h3 { margin-bottom: 14px; }
.intro-triad p { color: var(--ink-70); font-size: var(--t-small); line-height: 1.72; }
@media (max-width: 900px) { .intro-triad { grid-template-columns: 1fr; gap: 40px; } }

.module { padding-block: var(--sp-m); }
.module--tight { padding-block: 8px; }
.module--bleed { padding-inline: var(--bleed-inset); }
/* square corners while it touches the viewport edge */
.module--bleed .slot,
.module--bleed .slot::after,
.module--bleed .video-frame { border-radius: var(--bleed-radius); }
/* caption text always sits flush to the right edge of the media above it */
.module__cap { display: flex; justify-content: space-between; gap: clamp(16px, 2vw, 32px); margin-top: 14px; }
.module__cap .num { flex: 0 0 auto; }
.module__cap p {
  font-size: var(--t-small);
  color: var(--ink-45);
  max-width: 52ch;
  text-align: right;
  margin-left: auto;
}
@media (max-width: 620px) { .module__cap { flex-direction: column; gap: 6px; } }

.pair { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: var(--col-gap); }
.trio { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: var(--col-gap); }
.quad { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--col-gap); }
@media (max-width: 760px) { .pair, .trio, .quad { grid-template-columns: 1fr; } }

/* ---------- INTERACTIVE PATTERN FIELD ----------
   The pattern units sit on black and scale with the cursor. Scaling is
   written straight onto each polygon by script every frame, so the only
   job here is the look and the transform origin. */
.svg-field {
  position: relative;
  width: 100%;
  overflow: hidden;
  background: #000;
  cursor: crosshair;
}
.svg-field svg {
  display: block;
  width: 100%; height: 100%;
}
.svg-field [data-shape] {
  fill: var(--paper);
  transform-box: fill-box;      /* each unit scales from its own centre */
  transform-origin: center;
  /* deliberately no will-change — with 720 units it would ask the
     browser for 720 compositor layers and cost more than it saves */
}

.svg-field__hint {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--mono);
  font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
  color: rgba(255,255,255,0.5);
  pointer-events: none;
  transition: opacity 0.6s var(--ease);
}
.svg-field.is-live .svg-field__hint { opacity: 0; }

@media (hover: none) {
  .svg-field { cursor: default; }
  .svg-field__hint::after { content: " — drag"; }
}

/* ---------- INTERACTIVE SVG ----------
   Only works with SVG written into the page, not an <img src="x.svg">.
   An image is a sealed box — the browser cannot reach the shapes inside
   it. Inline, every path is a real element that can be hovered.

   transform-box: fill-box makes each shape scale from its own centre
   rather than the centre of the whole canvas.                        */
.svg-play {
  position: relative;
  width: 100%;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--wire-alt);
}
.svg-play svg { display: block; width: 100%; height: auto; }

.svg-play [data-shape] {
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 0.55s var(--ease), opacity 0.4s var(--ease);
  cursor: pointer;
}
.svg-play [data-shape]:hover { transform: scale(1.22); }

/* everything else steps back so the hovered form reads clearly */
.svg-play svg:has([data-shape]:hover) [data-shape]:not(:hover) { opacity: 0.28; }

@media (hover: none) {
  .svg-play [data-shape] { cursor: default; }
  .svg-play svg:has([data-shape]:hover) [data-shape]:not(:hover) { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .svg-play [data-shape]:hover { transform: none; }
}

/* ---------- COMPOSED FRAME GROUPS ----------
   In both, one frame carries the aspect ratio and sets the row height;
   the others stretch to meet it. That keeps the group's top and bottom
   edges flush at every width without hard-coding any heights. */

/* one large frame, two stacked beside it */
.frame-3 {
  display: grid;
  grid-template-columns: 2.7fr 1fr;
  gap: var(--col-gap);
}
.frame-3__side {
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: var(--col-gap);
  min-height: 0;
}
.frame-3__side .slot { height: 100%; }

/* one tall frame, one wide beside it */
.frame-2 {
  display: grid;
  grid-template-columns: 1fr 2.8fr;
  gap: var(--col-gap);
}
.frame-2 > .slot:first-child { height: 100%; }

@media (max-width: 760px) {
  .frame-3, .frame-2 { grid-template-columns: 1fr; }
  .frame-3__side { grid-template-rows: auto auto; }
  .frame-3__side .slot { height: auto; aspect-ratio: 1 / 1; }
  .frame-2 > .slot:first-child { height: auto; aspect-ratio: 4 / 5; }
}

/* shares the trio's column rhythm so a 2-column media block lines up
   exactly with two of the three images below it */
.trio-span {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--col-gap);
  align-items: start;
}
/* text holds column 1; media and its caption occupy columns 2–3 */
.trio-span > .split__text { grid-column: 1 / 2; }
.trio-span > .span-2 { grid-column: 2 / span 2; }
.trio-span > .module__cap { margin-top: 14px; }

/* mirrored — media on the left, text on the right. Uses the same column
   split as .frame-3 so the media matches that group's large frame exactly. */
.trio-span--flip { grid-template-columns: 2.7fr 1fr; }

/* A wider text column, for a block whose line breaks are set by hand.
   At three equal columns the copy has 424px and the longest of those
   lines wants ~480 — the browser would add breaks of its own on top of
   the intended ones, which reads worse than not setting them at all. */
.trio-span--copy { grid-template-columns: 1.35fr 1fr 1fr; }
/* the same allowance on the flipped version, where the copy is the
   narrow column on the right */
.trio-span--flip.trio-span--copy { grid-template-columns: 1.8fr 1fr; }
.trio-span.trio-span--flip > .span-2 { grid-column: 1; }
.trio-span.trio-span--flip > .split__text { grid-column: 2; }
@media (max-width: 760px) {
  .trio-span--flip { grid-template-columns: 1fr; }
  .trio-span.trio-span--flip > .span-2,
  .trio-span.trio-span--flip > .split__text { grid-column: auto; }
}
@media (max-width: 760px) {
  .trio-span { grid-template-columns: 1fr; }
  .trio-span > .span-2 { grid-column: auto; }
  .trio-span > .split__text { margin-bottom: 24px; }
}

/* NOTE: these column rules are scoped to .split on purpose. Left
   unscoped they leak into any other grid using .split__text — which
   silently broke the 3-column .trio-span layout. */
.split { display: grid; grid-template-columns: repeat(12, minmax(0,1fr)); gap: var(--col-gap); align-items: center; }
.split > .split__text { grid-column: 1 / 5; }
.split > .split__media { grid-column: 6 / -1; }
.split--flip > .split__text { grid-column: 9 / -1; }
.split--flip > .split__media { grid-column: 1 / 8; }
@media (max-width: 900px) {
  .split > .split__text, .split > .split__media,
  .split--flip > .split__text, .split--flip > .split__media { grid-column: 1 / -1; }
  .split > .split__text { margin-bottom: 24px; }
}

.pull {
  font-size: clamp(1.5rem, 3.4vw, 2.9rem);
  line-height: 1.16; letter-spacing: -0.035em; font-weight: 600;
  max-width: 40ch;
  padding-block: var(--sp-l);
}

/* manual line breaks — honoured on desktop, ignored on narrow screens
   where a forced break would leave an awkward orphan */
.br-lg { display: inline; }
@media (max-width: 760px) { .br-lg { display: none; } }

/* a caption whose line breaks are set by hand */
.cap--breaks { max-width: none !important; }

/* Captions are set to one rule across the whole site: under about sixty
   characters they stay on a single line, and anything longer breaks once
   at its natural clause boundary with the halves kept close in length.
   The breaks are dropped on narrow screens, where they would fight the
   column — text-wrap: balance then does the same job automatically, so
   a caption never ends with one word stranded on its own line. */
.module__cap p { text-wrap: balance; }
@media (max-width: 760px) { .cap--br { display: none; } }

.stat-row {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--col-gap);
  padding-block: var(--sp-m);
  border-top: var(--rule-w) solid var(--rule); border-bottom: var(--rule-w) solid var(--rule);
}
.stat-row strong {
  display: block; font-size: clamp(1.7rem, 3.4vw, 3rem);
  font-weight: 700; letter-spacing: -0.045em; line-height: 1; margin-bottom: 8px;
}
@media (max-width: 700px) { .stat-row { grid-template-columns: repeat(2, 1fr); row-gap: 32px; } }

.checklist {
  margin-block: var(--sp-l);
  border: var(--rule-w) solid var(--ink);
  border-radius: var(--r-lg);
  padding: clamp(24px, 3.2vw, 44px);
  background: var(--wire-alt);
}
body.clean .checklist { display: none; }
.checklist h3 { margin-bottom: 20px; }
.checklist li {
  display: grid; grid-template-columns: 40px 1fr auto; gap: 16px;
  align-items: baseline; padding: 11px 0;
  border-top: var(--rule-w) solid var(--rule);
  font-size: var(--t-small);
}
.checklist li:last-child { border-bottom: var(--rule-w) solid var(--rule); }
.checklist li .num { color: var(--ink); }
.checklist li em { font-style: normal; color: var(--ink-45); font-family: var(--mono); font-size: 11px; }
.checklist__total { margin-top: 20px; font-family: var(--mono); font-size: 11px; color: var(--ink-45); line-height: 1.7; }
@media (max-width: 620px) {
  .checklist li { grid-template-columns: 32px 1fr; }
  .checklist li em { grid-column: 2; }
}

.next { display: block; padding-block: var(--sp-l); border-top: var(--rule-w) solid var(--ink); }
.next__inner { display: flex; justify-content: space-between; align-items: baseline; gap: 24px; }
.next .h1 { transition: transform 0.7s var(--ease); }
.next:hover .h1 { transform: translateX(clamp(8px, 1.4vw, 22px)); }
@media (max-width: 700px) { .next__inner { flex-direction: column; gap: 12px; } }

.backlink {
  position: fixed; z-index: 55; left: var(--e, var(--gutter)); bottom: 24px;
  display: inline-flex; align-items: center; gap: 10px;
  padding: 11px 18px;
  background: var(--ink); color: var(--paper);
  border-radius: var(--r-pill);
  font-size: var(--t-label); letter-spacing: 0.15em; text-transform: uppercase; font-weight: 600;
  transition: transform 0.5s var(--ease);
}
.backlink:hover { transform: translateX(-4px); }

/* ---------- 12. CONTACT + FOOTER ----------
   Two columns under one rule: the invitation and its supporting line on
   the left, the ways to reach you on the right. The values step down
   from 27 to 16 — a phone number is information, not a headline, and at
   27 it was competing with the project names. */
.contact { padding-block: var(--sp-block) var(--sp-block); }

.contact__top {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: clamp(24px, 4vw, 64px);
  padding-bottom: clamp(28px, 4vh, 48px);
}
.contact__top .h1 { max-width: 14ch; }

/* the standing invitation, set right and underlined on its own baseline */
.contact__link {
  flex: 0 0 auto;
  display: inline-flex; align-items: baseline; gap: 8px;
  padding-bottom: 6px;
  border-bottom: var(--rule-w) solid var(--ink);
  font-size: var(--t-body);
  transition: gap 0.45s var(--ease);
}
.contact__link:hover { gap: 14px; }

.contact__body {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 7fr);
  gap: clamp(24px, 4vw, 72px);
  padding-top: clamp(28px, 4vh, 48px);
  border-top: var(--rule-w) solid var(--rule);
}
.contact__note {
  font-size: var(--t-small);
  line-height: 1.65;
  color: var(--ink-45);
  max-width: 34ch;
}

.contact__list { display: grid; gap: 10px; align-content: start; }
.contact__item {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  padding: clamp(18px, 2.4vh, 26px) clamp(18px, 2vw, 28px);
  background: var(--wire-alt);
  border-radius: var(--r-md);
  transition: background-color 0.45s var(--ease), padding-left 0.45s var(--ease);
}
a.contact__item:hover { background: var(--wire); padding-left: clamp(24px, 2.6vw, 38px); }
.contact__item .label { flex: 0 0 auto; }
.contact__val {
  font-size: var(--t-body);
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.3;
  text-align: right;
  word-break: break-word;
  margin-left: auto;
}
.contact__go {
  flex: 0 0 auto;
  font-size: var(--t-small);
  color: var(--ink-45);
  transition: transform 0.45s var(--ease), color 0.45s var(--ease);
}
a.contact__item:hover .contact__go { color: var(--ink); transform: translate(3px, -3px); }

@media (max-width: 900px) {
  .contact__top { flex-direction: column; align-items: flex-start; gap: 22px; }
  .contact__top .h1 { max-width: none; }
  .contact__body { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .contact__item { flex-wrap: wrap; gap: 6px 14px; }
  .contact__val { text-align: left; margin-left: 0; width: 100%; order: 3; }
}

.site-foot { padding-block: var(--sp-block) 40px; border-top: var(--rule-w) solid var(--rule); }
.foot-base { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; }

/* ---------- 13. WIREFRAME TOGGLE ---------- */
.wf-toggle {
  position: fixed; z-index: 70; right: var(--e, var(--gutter)); bottom: 24px;
  display: inline-flex; align-items: center; gap: 10px;
  padding: 11px 16px;
  border: var(--rule-w) solid var(--ink); border-radius: var(--r-pill);
  background: var(--paper);
  font-size: var(--t-label); letter-spacing: 0.13em; text-transform: uppercase; font-weight: 600;
}
.wf-toggle__dot { width: 7px; height: 7px; background: var(--ink); border-radius: 50%; transition: opacity 0.3s; }
.wf-toggle__text { display: inline-block; min-width: 62px; text-align: left; }
body.clean .wf-toggle__dot { opacity: 0.2; }

/* ---------- 14. CURSOR ---------- */
.cursor {
  position: fixed; top: 0; left: 0; z-index: 80;
  width: 96px; height: 96px; margin: -48px 0 0 -48px;
  border-radius: 50%; background: var(--ink); color: var(--paper);
  display: grid; place-items: center;
  font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; font-weight: 600;
  pointer-events: none; transform: scale(0);
  transition: transform 0.45s var(--ease);
  will-change: transform;
}
.cursor.is-on { transform: scale(1); }
@media (hover: none), (max-width: 900px) { .cursor { display: none; } }

/* ---------- 15. MOTION ---------- */
/* Text and small parts rise a short way. Media frames get a longer
   travel and come up slightly under size, so a picture arriving on
   screen is an event rather than a fade. */
.reveal {
  opacity: 0;
  transform: translateY(var(--m-rise));
  transition: opacity var(--m-dur) var(--ease),
              transform var(--m-dur) var(--ease);
}
.reveal.is-in { opacity: 1; transform: none; }
.reveal[data-d="1"].is-in { transition-delay: 0.08s; }
.reveal[data-d="2"].is-in { transition-delay: 0.16s; }
.reveal[data-d="3"].is-in { transition-delay: 0.24s; }

/* any reveal that is a frame, or holds one */
.reveal.slot,
.reveal .slot,
.reveal.p-row .p-row__media,
.reveal.module,
.svg-field.reveal {
  /* frames rise and come up slightly under size, and that is all —
     no blur on media, the way it was before. A picture arriving is an
     event; softening it only makes it look like it is loading. */
  transform: translateY(var(--m-rise-lg)) scale(0.955);
  opacity: 0;
  filter: none;
  transition: opacity calc(var(--m-dur) * 1.15) var(--ease),
              transform calc(var(--m-dur) * 1.25) var(--ease);
}
.reveal.is-in.slot,
.reveal.is-in .slot,
.reveal.is-in.p-row .p-row__media,
.reveal.is-in.module,
.svg-field.reveal.is-in {
  transform: none;
  opacity: 1;

}
/* the parent has already animated; without this the two stack up and
   the frame travels twice as far as intended */
.reveal.p-row.is-in, .reveal.module.is-in { transform: none; }

/* overflow:hidden is what makes the reveal work, but at these tight
   line-heights the descenders (j, y, g) fall outside the line box and
   get sliced. Pad the box enough to contain them, then pull the same
   amount back off the margin so the layout spacing is unchanged. */
.mask {
  display: block;
  overflow: hidden;
  padding-bottom: 0.24em;
  margin-bottom: -0.18em;
}
.mask > span { display: block; transform: translateY(105%); }


.mask.is-in > span { transform: none; transition: transform 1s var(--ease); }
.mask[data-d="1"].is-in > span { transition-delay: 0.09s; }
.mask[data-d="2"].is-in > span { transition-delay: 0.18s; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
  .reveal, .mask > span { opacity: 1; transform: none; filter: none; }
  [data-veil] { opacity: 1 !important; transform: none !important; filter: none !important; }
  .marquee__track { animation: none; }
}
