/* =========================================================================
   Viktor Stojkov / Videographer
   -------------------------------------------------------------------------
   Palette      : #000000  #5682B1  #739EC9  #FFE8DB
   Type         : Satoshi (display + body), JetBrains Mono (micro labels)
   Theme        : locked dark. The brand palette is built on black, so a light
                  variant would not be the same brand. color-scheme is dark and
                  every surface is black or a small lift off it.
   Radius       : three documented tiers, applied without exception.
                    --r-media  anything framing a picture or a video
                    circles    the play control only
                    0          all UI chrome: buttons, inputs, nav, form
   Accent       : the blue pair only, with cream carrying the type. --coral
                  appears solely as a form error signal, never as decoration.
   Mark         : the torn playhead is the one polychrome thing here and the
                  one place #C6FF3D, #FF3D6E and #9B5CFF appear. They belong to
                  the mark, not to the palette: nothing else on the page may
                  use them, or the mark stops being the loud thing.
   z-index      : nav 70 / mobilemenu 80 / lightbox 90 / viewer 95 / grain 100
   ========================================================================= */

/* ---------- fonts ---------- */
@font-face{font-family:Satoshi;src:url(../assets/fonts/Satoshi-400.woff2) format('woff2');font-weight:400;font-display:swap;font-style:normal}
@font-face{font-family:Satoshi;src:url(../assets/fonts/Satoshi-500.woff2) format('woff2');font-weight:500;font-display:swap;font-style:normal}
@font-face{font-family:Satoshi;src:url(../assets/fonts/Satoshi-700.woff2) format('woff2');font-weight:700;font-display:swap;font-style:normal}
@font-face{font-family:Satoshi;src:url(../assets/fonts/Satoshi-900.woff2) format('woff2');font-weight:900;font-display:swap;font-style:normal}
@font-face{font-family:'JetBrains Mono';src:url(../assets/fonts/JetBrainsMono-400.woff2) format('woff2');font-weight:400;font-display:swap;font-style:normal}
@font-face{font-family:'JetBrains Mono';src:url(../assets/fonts/JetBrainsMono-500.woff2) format('woff2');font-weight:500;font-display:swap;font-style:normal}

/* ---------- tokens ---------- */
:root{
  --ink-000:#000000;
  --ink-100:#000000;   /* palette base */
  --ink-200:#0B0F14;   /* smallest lift off black that still reads as a surface */
  --ink-300:#131A22;

  --blue-700:#3E608A;  /* darkened accent, for rules and fills */
  --blue-500:#5682B1;  /* palette, accent      5.2:1 on black */
  --blue-300:#739EC9;  /* palette, light accent 7.5:1 on black */
  --cream:#FFE8DB;     /* palette, high emphasis 17.8:1 on black */

  --text-hi:#FFE8DB;   /* 17.8:1 on --ink-100 */
  --text-mid:#B0A49D;  /* 8.7:1  on --ink-100 */
  --text-lo:#7E7570;   /* 4.7:1  on --ink-100 */

  --line:rgba(255,232,219,.14);
  --line-strong:rgba(255,232,219,.26);

  --coral:#FF6B5B;     /* form errors only */

  --radius:0px;        /* UI chrome */
  --r-media:clamp(12px,1.1vw,18px);
  --shell:1400px;
  --gutter:clamp(20px,5vw,64px);
  --sec-y:clamp(6rem,12vw,11rem);
  --nav-h:68px;

  --ease:cubic-bezier(.16,1,.3,1);
  /* --ease is an expo-out: 75% of the way in the first 60ms of 300. That is
     right for something flying in from off screen and wrong for a hover pop,
     where it reads as an instant snap with a long invisible tail. This one is
     even: 56% at 60ms, 74% at 90ms. */
  --ease-pop:cubic-bezier(.25,.8,.35,1);
  /* For anything that ping-pongs. --ease-pop is an ease-out: it spends 56% of
     the travel in the first fifth and 1% in the last, so on a loop the piece
     reaches its extreme early and then sits there. Measured on the mark's tear:
     motionless from 1000ms to 2400ms of a 3200ms round trip. This one is
     symmetric, 7/25/35/25/7 across the five fifths, so the turnarounds ease and
     the middle keeps moving. */
  --ease-loop:cubic-bezier(.45,0,.55,1);
  --dur:.55s;

  --font:Satoshi,ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
  --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
}

/* ---------- reset ---------- */
*,*::before,*::after{box-sizing:border-box}
*{margin:0;padding:0}
html{
  color-scheme:dark;
  background:var(--ink-100);
  -webkit-text-size-adjust:100%;
  scroll-behavior:smooth;
}
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto!important}
.lenis.lenis-stopped{overflow:hidden}

body{
  font-family:var(--font);
  font-size:clamp(1rem,.96rem + .2vw,1.0625rem);
  line-height:1.65;
  color:var(--text-hi);
  /* Transparent so the fixed grid canvas behind it is visible. Sections that
     must hide the grid paint their own opaque background. */
  background:transparent;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  /* No overflow-x:hidden here on purpose. It turns <body> into a scroll
     container, which silently breaks position:sticky and ScrollTrigger pins.
     Sections that can overflow clip themselves instead. */
}
img,svg,video{display:block;max-width:100%}
img{height:auto}
button,input,select,textarea{font:inherit;color:inherit;background:none;border:none;border-radius:var(--radius)}
button{cursor:pointer}
a{color:inherit;text-decoration:none}
ul,ol{list-style:none}
::selection{background:var(--cream);color:var(--ink-100)}

/* Nothing is selectable and nothing flashes a tap highlight. Both strips are
   dragged with the pointer, and a drag that leaves a trail of highlighted text
   behind it reads as a broken page rather than a moving one.

   Form fields are the one exception, and they have to be: a field you cannot
   select inside is a field you cannot correct a typo in. ::selection above
   still styles them, which is now the only place it applies.

   The cost is that the email and the phone number can no longer be swiped and
   copied. Both are mailto: and tel: links, so tapping still opens a mail
   client or dials, but copying by hand is gone. */
html{
  -webkit-user-select:none;
  -moz-user-select:none;
  user-select:none;
  -webkit-tap-highlight-color:transparent;
}
input,textarea,select,[contenteditable="true"]{
  -webkit-user-select:text;
  -moz-user-select:text;
  user-select:text;
}

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

.u-sr{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}
.skip{
  position:fixed;top:10px;left:10px;z-index:200;
  padding:.7rem 1.1rem;background:var(--cream);color:var(--ink-100);
  font-weight:700;transform:translateY(-160%);transition:transform .2s var(--ease);
}
.skip:focus-visible{transform:none}

.shell{
  width:100%;max-width:var(--shell);margin-inline:auto;
  padding-inline:var(--gutter);
}

/* ---------- type ---------- */
.eyebrow{
  font-family:var(--mono);font-size:.6875rem;font-weight:500;
  letter-spacing:.2em;text-transform:uppercase;color:var(--blue-500);
  margin-bottom:1.25rem;
}
.sec-title{
  font-size:clamp(2.5rem,1.4rem + 5vw,5rem);
  font-weight:900;letter-spacing:-.035em;line-height:1.02;
  text-wrap:balance;
}
.sec-lede{
  margin-top:1.5rem;max-width:56ch;
  color:var(--text-mid);font-size:clamp(1.0625rem,1rem + .3vw,1.1875rem);
}
.sec-head{margin-bottom:clamp(3rem,6vw,5rem)}

/* line-reveal wrapper: .ln clips, inner span translates */
.ln{display:block;overflow:hidden;padding-bottom:.08em;margin-bottom:-.08em}
.ln > span{display:block;will-change:transform}

/* ---------- buttons ---------- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:.6rem;
  padding:1rem 1.75rem;
  font-weight:700;font-size:.9375rem;letter-spacing:-.005em;
  white-space:nowrap;                 /* CTA labels never wrap */
  border:1px solid transparent;
  transition:background .25s var(--ease),color .25s var(--ease),
             border-color .25s var(--ease),transform .12s var(--ease);
}
.btn svg{width:1.15em;height:1.15em;fill:currentColor;flex:none}
.btn:active{transform:translateY(1px)}
.btn--sm{padding:.7rem 1.2rem;font-size:.875rem}
.btn--block{width:100%}

.btn--primary{background:var(--cream);color:var(--ink-100)}   /* 17.8:1 */
.btn--primary:hover{background:#fff4ee}

.btn--ghost{border-color:var(--line-strong);color:var(--text-hi)}
.btn--ghost:hover{border-color:var(--cream);background:rgba(255,232,219,.08)}

/* Solid accent fill. Dark text rather than white: white on --blue-500 is
   4.0:1, which is under AA for text this size, while black on it is 5.2:1. */
.btn--accent{background:var(--blue-500);color:var(--ink-100);border-color:var(--blue-500)}
.btn--accent:hover{background:var(--cream);color:var(--ink-100);border-color:var(--cream)}

.link-out{
  display:inline-flex;align-items:center;gap:.55rem;
  margin-top:2.25rem;padding-bottom:.35rem;
  font-weight:700;color:var(--cream);
  border-bottom:1px solid var(--line-strong);
  transition:border-color .25s var(--ease),gap .25s var(--ease);
}
.link-out svg{width:1.05em;height:1.05em;fill:currentColor}
.link-out:hover{border-color:var(--cream);gap:.9rem}

/* =========================================================================
   KINETIC GRID  (canvas background, behind everything, never clickable)
   ========================================================================= */
#grid{
  position:fixed;inset:0;z-index:-1;
  width:100%;height:100%;
  pointer-events:none;
  display:block;
}
@media (prefers-reduced-transparency:reduce){
  #grid{display:none}
}

/* No full-width rules divide the sections: spacing does that work instead.
   Borders that remain belong to components, the frames, tiles, form fields,
   the contact card and the lists inside it, not to the seams between sections.
   ========================================================================= */

/* =========================================================================
   NAV
   ========================================================================= */
.nav{
  position:fixed;inset:0 0 auto;z-index:70;
  height:var(--nav-h);
  transition:transform .4s var(--ease);
}
/* The tint and the blur live on their own layer so it can be masked. Applying
   backdrop-filter to the bar itself ends the blur dead on 68px, and that hard
   cutoff reads as a rule across the page whenever there is busy footage behind
   it. Fading the layer out means there is no edge at all, and the nav's own
   content stays unmasked and fully opaque. */
.nav::before{
  content:"";position:absolute;inset:0;z-index:-1;
  background:linear-gradient(to bottom,rgba(0,0,0,.9),rgba(0,0,0,0));
  backdrop-filter:blur(14px) saturate(140%);
  -webkit-backdrop-filter:blur(14px) saturate(140%);
  -webkit-mask-image:linear-gradient(to bottom,#000 40%,transparent 100%);
  mask-image:linear-gradient(to bottom,#000 40%,transparent 100%);
  transition:background .4s var(--ease);
}
.nav[data-solid="true"]::before{
  background:linear-gradient(to bottom,rgba(0,0,0,.96),rgba(0,0,0,.35));
}
.nav[data-hidden="true"]{transform:translateY(-100%)}

.nav__inner{
  height:100%;max-width:var(--shell);margin-inline:auto;
  padding-inline:var(--gutter);
  display:flex;align-items:center;gap:2rem;
}
.nav__brand{
  display:flex;align-items:center;gap:.6rem;
  font-weight:700;font-size:.9375rem;letter-spacing:-.01em;margin-right:auto;
}
/* No fill here. The mark is polychrome now — a cream head, a cream band with
   blue ticks, an acid band, a magenta band with a violet tick — and a single
   fill would flatten all of that to one colour. The old rule swapped blue for
   cream on hover; the mark answers a hover by tearing further apart instead.
   overflow:visible so the pieces can travel outside the 20px box. */
.nav__mark svg{width:20px;height:20px;overflow:visible}
.nav__mark svg g,
.nav__mark svg path{
  transition:transform .5s var(--ease);
  transform-box:view-box;transform-origin:100px 100px;
}
.nav__brand:hover .m-head{transform:translate(9px,-7px)}
.nav__brand:hover .m-b1{transform:translateX(-15px)}
.nav__brand:hover .m-b2{transform:translateX(17px)}
.nav__brand:hover .m-b3{transform:translate(-9px,6px)}

.nav__links{display:flex;gap:2rem}
.nav__links a{
  font-size:.9375rem;font-weight:500;color:var(--text-mid);
  transition:color .25s var(--ease);
}
.nav__links a:hover{color:var(--text-hi)}

.nav__toggle{display:none;padding:.5rem;margin-right:-.5rem;color:var(--text-hi)}
.nav__toggle svg{width:24px;height:24px;fill:currentColor}

@media (max-width:860px){
  :root{--nav-h:60px}
  .nav__links,.nav__cta{display:none}
  .nav__toggle{display:block}
}

/* mobile menu */
.mobilemenu{
  position:fixed;inset:0;z-index:80;
  background:var(--ink-100);
  display:flex;flex-direction:column;justify-content:center;
  padding:var(--gutter);
}
.mobilemenu[hidden]{display:none}
.mobilemenu__close{
  position:absolute;top:calc((60px - 40px)/2);right:calc(var(--gutter) - .5rem);
  padding:.5rem;color:var(--text-hi);
}
.mobilemenu__close svg{width:24px;height:24px;fill:currentColor}
.mobilemenu__links{display:flex;flex-direction:column;gap:.25rem}
.mobilemenu__links a{
  font-size:clamp(2rem,10vw,2.75rem);font-weight:900;letter-spacing:-.035em;
  padding:.35rem 0;border-bottom:1px solid var(--line);
}
.mobilemenu__links a:last-child{color:var(--cream);border-bottom:none;margin-top:1rem}

/* =========================================================================
   HERO  (full-bleed media, asymmetric overlaid type)
   ========================================================================= */
.hero{
  position:relative;
  /* Opaque: this is a video slot, so the grid must not show through it. */
  background:var(--ink-100);
  min-height:100dvh;
  display:flex;align-items:flex-end;
  padding:var(--nav-h) var(--gutter) clamp(3rem,7vh,5.5rem);
  overflow:hidden;
}
.hero__media{position:absolute;inset:0;z-index:0}
.hero__media .ph{position:absolute;inset:-8% 0;height:auto}

/* The reel is letterboxed: 2.52:1 content sitting inside a 16:9 frame, with
   black bars taking 14.7% off the top and bottom. Scaling by 1/0.705 pushes
   the bars out of view so the hero is genuinely full bleed. Measured from the
   poster; if a future cut is letterboxed differently, remeasure and change
   this one number. */
.hero__still{
  --bar-crop:1.44;
  position:absolute;inset:0;
  width:100%;height:100%;
  object-fit:cover;
  transform:scale(var(--bar-crop));
}
.hero__scrim{
  position:absolute;inset:0;
  background:
    linear-gradient(to top,rgba(0,0,0,.96) 0%,rgba(0,0,0,.55) 42%,rgba(0,0,0,.25) 70%,rgba(0,0,0,.6) 100%),
    radial-gradient(120% 80% at 15% 90%,rgba(0,0,0,.85),transparent 65%);
}
.hero__copy{
  position:relative;z-index:1;
  width:100%;max-width:var(--shell);margin-inline:auto;
}
.hero__title{
  font-size:clamp(3rem,1.1rem + 8.6vw,8.5rem);
  font-weight:900;letter-spacing:-.045em;line-height:.94;
  max-width:14ch;
}
/* The headline is written as two lines: the fixed phrase, then the rotating
   word. On a phone the clamp's 3rem floor stops the type shrinking with the
   screen, so the phrase wrapped and the headline came out three lines deep.
   This tracks the viewport instead, sized so "Visuals that hold" fits the
   column with about four percent to spare at every width down to 320.
   Measured, not guessed: the phrase needs 7.2x the font size in width, and the
   column is the viewport less two gutters. A longer phrase needs remeasuring. */
@media (max-width:460px){
  .hero__title{font-size:calc(13.4vw - 5.8px)}
}
/* Rotating headline word. The first word stays in normal flow so it sets the
   line box; the others stack on top of it. Transforms do not reflow, so the
   in-flow word can slide out without moving anything else. */
.hero__rot{
  position:relative;display:block;
  overflow:hidden;
  padding-bottom:.1em;margin-bottom:-.1em;
}
.hero__word{display:block;will-change:transform,opacity}
.hero__word:not(:first-child){position:absolute;left:0;top:0;width:100%}

.hero__sub{
  margin-top:1.75rem;max-width:44ch;
  color:var(--text-mid);
  font-size:clamp(1.0625rem,1rem + .4vw,1.25rem);
}
.hero__actions{display:flex;flex-wrap:wrap;gap:.9rem;margin-top:2.5rem}
@media (max-width:480px){
  .hero__actions{display:grid;grid-template-columns:1fr}  /* ragged button widths look accidental at this size */
}

/* =========================================================================
   VIDEO PLACEHOLDERS
   Swap any .ph for a real <video> / <iframe>. The aspect modifier stays.
   ========================================================================= */
.ph{
  position:relative;
  border-radius:var(--r-media);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:.6rem;
  border:1px solid var(--line);
  background:
    radial-gradient(110% 85% at var(--ph-x,50%) var(--ph-y,0%),rgba(86,130,177,.22),transparent 62%),
    repeating-linear-gradient(115deg,rgba(255,232,219,.026) 0 2px,transparent 2px 10px),
    linear-gradient(165deg,var(--ink-300),var(--ink-100) 75%);
  overflow:hidden;
}
/* Each plate is lit from a different corner so the grid does not read as one
   flat texture repeated five times. */
.reels .tile:nth-child(6n+1) .ph{--ph-x:24%}
.reels .tile:nth-child(6n+2) .ph{--ph-x:70%;--ph-y:10%}
.reels .tile:nth-child(6n+3) .ph{--ph-x:32%;--ph-y:4%}
.reels .tile:nth-child(6n+4) .ph{--ph-x:64%;--ph-y:14%}
.reels .tile:nth-child(6n+5) .ph{--ph-x:18%;--ph-y:6%}
.reels .tile:nth-child(6n+6) .ph{--ph-x:80%}
.ph::before{                       /* play control: the one circle in the system */
  content:"";
  width:clamp(48px,4.6vw,68px);aspect-ratio:1;
  border-radius:50%;
  border:1px solid rgba(255,232,219,.34);
  background:rgba(255,232,219,.14);
  backdrop-filter:blur(6px);
  -webkit-backdrop-filter:blur(6px);
  transition:border-color .35s var(--ease),background-color .35s var(--ease),
             transform .35s var(--ease);
}
.ph::after{                        /* the triangle, centred inside the square above */
  content:"";
  position:absolute;top:50%;left:50%;
  translate:-38% -50%;
  width:0;height:0;
  border-style:solid;
  border-width:.55rem 0 .55rem .95rem;
  border-color:transparent transparent transparent var(--cream);
  opacity:.85;
  transition:opacity .35s var(--ease),translate .35s var(--ease);
}
.ph--flat::before,.ph--flat::after{display:none}
.ph__label{
  position:absolute;left:1.25rem;bottom:1.15rem;
  font-family:var(--mono);font-size:.75rem;letter-spacing:.06em;
  color:var(--text-hi);
}
.ph__meta{
  position:absolute;right:1.25rem;bottom:1.15rem;
  font-family:var(--mono);font-size:.75rem;letter-spacing:.06em;
  color:var(--text-lo);
}
/* The hero plate is a backdrop, not a control. Its play affordance is the
   "Watch the reel" button, so the plate carries no glyph and no slate text. */
.ph--hero::before,.ph--hero::after,
.ph--hero .ph__label,.ph--hero .ph__meta{display:none}
.ph--flat .ph__label,.ph--flat .ph__meta{color:var(--text-mid)}

/* Poster frame behind the play control. The image is positioned so it does
   not disturb the flex centring of the control, and the z-indexes keep it
   under both the circle and the duration. */
.ph--poster img{
  position:absolute;inset:0;z-index:0;
  width:100%;height:100%;object-fit:cover;
}
.ph--poster::before{position:relative;z-index:2}
.ph--poster::after{z-index:3}
.ph--poster .ph__meta{
  z-index:2;
  padding:.15rem .4rem;
  background:rgba(0,0,0,.62);   /* the duration has to hold over any frame */
  color:var(--text-hi);
}
.tile:hover .ph--poster img{transform:scale(1.03)}
.ph--poster img{transition:transform .6s var(--ease)}

.ph--16x10{aspect-ratio:16/10}
.ph--16x9{aspect-ratio:16/9}
.ph--4x5{aspect-ratio:4/5}
.ph--1x1{aspect-ratio:1}
.ph--21x9{aspect-ratio:21/9}

/* =========================================================================
   MARQUEE  (one per page)
   ========================================================================= */
.marquee{
  position:relative;
  padding-block:clamp(1.4rem,3vw,2.2rem);
  overflow:hidden;
  /* Blurs the grid canvas behind the band so the type is not competing with a
     mesh running straight through it. The tint gives the blur something to
     hold onto; without it the effect barely reads on near-black. */
  background:rgba(0,0,0,.4);
  backdrop-filter:blur(14px) saturate(120%);
  -webkit-backdrop-filter:blur(14px) saturate(120%);
}
@media (prefers-reduced-transparency:reduce){
  .marquee{background:var(--ink-100);backdrop-filter:none;-webkit-backdrop-filter:none}
}
.marquee__row{display:flex;width:max-content;will-change:transform}
.marquee__set{
  display:flex;align-items:center;gap:clamp(1.5rem,3vw,2.75rem);
  padding-right:clamp(1.5rem,3vw,2.75rem);
}
.marquee__set li{
  font-size:clamp(1.5rem,1rem + 2.2vw,3rem);
  font-weight:700;letter-spacing:-.03em;
  color:var(--text-hi);white-space:nowrap;
}
.marquee__sep svg{width:.7em;height:.7em;fill:var(--blue-500)}

/* =========================================================================
   WORK  (bento: 5 items, 5 cells, no empty tiles)
   ========================================================================= */
/* Half the usual rhythm on the way out of Recent projects. Adjacent sections
   each contribute their own padding, so the space between two of them is twice
   --sec-y; halving both sides is what actually halves the gap. The run from
   Recent projects through Stills into My creative process is tightened this
   way. The top of Recent projects still uses the full value, since that gap is
   against the marquee and is not part of the run. */
.work{padding-block:var(--sec-y) calc(var(--sec-y) / 2);overflow:hidden}

/* Inline reel strip. Same drift as the stills: it moves right to left on its
   own and the pointer can grab it. Every tile shares one height and takes its
   width from its own orientation, so 9:16 and 16:9 pieces sit in one row
   uncropped, and each tile steps further down than the one before it. */
.reels{position:relative}
.reels__viewport{
  overflow:hidden;
  cursor:grab;
  touch-action:pan-y;      /* we take horizontal, the page keeps vertical */

  /* Tiles fade in from the right and out to the left, matching the drift.
     The fade resolves exactly at the content edge, the same line the headings
     and the buttons sit on, so a tile is fully opaque from the moment it
     reaches the text column and only ever fades outside it. */
  --edge:calc(max((100% - var(--shell)) / 2, 0px) + var(--gutter));
  -webkit-mask-image:linear-gradient(to right,
    transparent 0,
    #000 var(--edge),
    #000 calc(100% - var(--edge)),
    transparent 100%);
  mask-image:linear-gradient(to right,
    transparent 0,
    #000 var(--edge),
    #000 calc(100% - var(--edge)),
    transparent 100%);
}
.reels__viewport[data-drag="active"]{cursor:grabbing}

.reels__track{
  /* Top aligned, not centred: a caption that wraps to two lines makes its tile
     taller, and centring would push that tile's poster out of line with the
     rest. Aligning at the top keeps every poster on the same line and lets the
     captions hang. */
  display:flex;align-items:flex-start;
  gap:var(--gap);
  width:max-content;will-change:transform;
  /* No padding-inline here. The track is what loops, so anything inside it has
     to repeat with the tiles: an inset before the first tile does not, and it
     reappeared as an empty band every time the wrap came round, which read as
     the last video vanishing. On a 3440 screen that band was over 1000px. The
     strip is edge-masked anyway, so the tiles start flush and fade instead. */
  /* room for a hovered card to grow without the viewport clipping it */
  padding-block:clamp(20px,2.4vw,40px);
}

.reels{--gap:clamp(18px,2.2vw,44px)}
.reels__more{margin-top:clamp(2rem,4vw,3.5rem)}
.reels .tile{
  --unit:calc((min(100vw, var(--shell)) - 2 * var(--gutter) - 4 * var(--gap) - 12px) / 5);
  --tile-h:min(calc(var(--unit) * 16 / 9), 62vh);
  flex:none;
}
.reels .tile > .ph{height:var(--tile-h);width:auto}
/* Pin the tile to the plate's width. The tile is a grid, so its column sizes
   to the widest child's max-content, and a long caption is a single unwrapped
   run: "Real estate Diamond apartments" pushed its tile to 276px while its
   neighbours sat at 227, so the poster no longer filled the tile and the strip
   spacing went ragged. With a definite width the caption wraps instead. */
.reels .tile--v{width:calc(var(--tile-h) * 9 / 16)}
.reels .tile--h{width:calc(var(--tile-h) * 16 / 9)}
.tile--v > .ph{aspect-ratio:9/16}
.tile--h > .ph{aspect-ratio:16/9}

/* ---- tile: no card, just the plate and its caption ---- */
.tile{
  position:relative;
  display:grid;grid-template-rows:auto auto;
}
/* Above the play control, not level with it. The circle and triangle are
   ::before/::after on .ph--poster at z-index 2 and 3, and a pseudo-element is
   painted by its generator, so hit testing over the glyph returned the poster
   div rather than this button: clicking the play control itself did nothing
   while clicking anywhere else on the tile worked. */
.tile__hit{position:absolute;inset:0;z-index:4}

.tile__foot{padding-top:1rem;transition:opacity .35s var(--ease)}
.tile__title{font-size:clamp(1rem,.95rem + .3vw,1.1875rem);font-weight:700;letter-spacing:-.02em;line-height:1.3}
.tile__meta{
  margin-top:.2rem;
  font-family:var(--mono);font-size:.7rem;letter-spacing:.05em;
  color:var(--text-mid);
}
.tile__yr{margin-left:.8rem;color:var(--text-lo)}

/* Pop. The card lifts and sits above its neighbours. Used by the strip on the
   home page and by the archive grid on work.html. */
.tile{transition:transform .34s var(--ease-pop)}
.tile:hover,
.tile:focus-within{transform:scale(1.06);z-index:3}
.tile:hover .ph::before,
.tile:focus-within .ph::before{
  background-color:var(--cream);border-color:var(--cream);transform:scale(1.08);
}
.tile:hover .ph::after,
.tile:focus-within .ph::after{border-left-color:var(--ink-100);opacity:1}
.tile:focus-within .ph{outline:2px solid var(--cream);outline-offset:4px}

@media (max-width:1024px){
  .reels .tile{--unit:calc((100vw - 2 * var(--gutter) - 3 * var(--gap) - 12px) / 4)}
}
@media (max-width:760px){
  .reels .tile{--unit:calc((100vw - 2 * var(--gutter) - 2 * var(--gap) - 12px) / 3)}
}
@media (max-width:520px){
  .reels .tile{--unit:calc((100vw - 2 * var(--gutter) - var(--gap)) / 2)}
}

/* =========================================================================
   ARCHIVE PAGE  (work.html)
   ========================================================================= */
.page-head{
  padding-top:calc(var(--nav-h) + clamp(4rem,9vw,7.5rem));
  padding-bottom:clamp(2.5rem,5vw,4rem);
}
.page-head .sec-lede{max-width:48ch}

.archive{padding-bottom:var(--sec-y)}

.filters{
  display:flex;flex-wrap:wrap;gap:.5rem;
  margin-bottom:clamp(2.5rem,5vw,4rem);
}

/* One line on a phone, swiped rather than wrapped. Four chips carrying their
   counts come to roughly 470px, so they cannot be made to fit 350px without
   shrinking the labels past reading size. The row scrolls inside itself, which
   keeps the page's own horizontal scroll at zero, and it bleeds into the
   gutters so the last chip is visibly cut rather than looking like the end. */
@media (max-width:640px){
  .filters{
    flex-wrap:nowrap;
    overflow-x:auto;
    scrollbar-width:none;
    margin-inline:calc(var(--gutter) * -1);
    padding-inline:var(--gutter);
    -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 40px),transparent 100%);
    mask-image:linear-gradient(to right,#000 calc(100% - 40px),transparent 100%);
  }
  .filters::-webkit-scrollbar{display:none}
  .filter{flex:none;white-space:nowrap}
}
.filter{
  padding:.7rem 1.15rem;
  border:1px solid var(--line);
  color:var(--text-mid);
  font-size:.875rem;font-weight:500;
  transition:color .25s var(--ease),border-color .25s var(--ease),
             background-color .25s var(--ease);
}
.filter:hover{color:var(--text-hi);border-color:var(--line-strong)}
.filter[aria-pressed="true"]{
  background:var(--cream);color:var(--ink-100);border-color:var(--cream);
}
.filter__n{font-family:var(--mono);font-size:.7rem;opacity:.65;margin-left:.45rem}

.archive__grid{
  display:grid;grid-template-columns:repeat(4,1fr);
  gap:clamp(18px,2.2vw,44px);
  align-items:start;
}
.archive__grid .tile > .ph{width:100%;height:auto}

/* Photography: a masonry tile field running the full width of the viewport.
   Photos keep their own aspect rather than being cropped to a common shape,
   and the column gap and the row margin are the same value so the spacing
   between tiles is even in both directions. */
.photogrid{
  --pg-gap:clamp(10px,1.1vw,20px);
  padding-inline:var(--pg-gap);
  columns:5;
  column-gap:var(--pg-gap);
}
.photogrid .frame{
  display:block;width:100%;
  aspect-ratio:auto;
  margin:0 0 var(--pg-gap);
  break-inside:avoid;
  border:1px solid var(--line);
  background:none;
}
.photogrid .frame img{width:100%;height:auto;object-fit:initial}

@media (max-width:1500px){ .photogrid{columns:4} }
@media (max-width:1100px){ .photogrid{columns:3} }
@media (max-width:760px){  .photogrid{columns:2} }
@media (max-width:480px){  .photogrid{columns:2} }

.stills__more{margin-top:clamp(2rem,4vw,3.5rem)}

/* Landscape pieces sit in their own row below the reels: a 16:9 tile in a
   column sized for 9:16 would leave most of the cell empty. */
.archive__row{
  display:grid;grid-template-columns:repeat(3,1fr);
  gap:clamp(18px,2.2vw,44px);
  align-items:start;
  margin-top:clamp(2.5rem,5vw,4.5rem);
}
.archive__row .tile > .ph{width:100%;height:auto}
.archive__row .tile[hidden],
.archive__grid .tile[hidden]{display:none}
[data-archive-group][hidden]{display:none}
.tile--h > .ph{aspect-ratio:16/9}
.tile--v > .ph{aspect-ratio:9/16}
.archive__grid .tile[hidden]{display:none}
.archive__empty{color:var(--text-mid)}

@media (max-width:1024px){
  .archive__grid{grid-template-columns:repeat(3,1fr)}
  .archive__row{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:760px){
  .archive__grid{grid-template-columns:repeat(2,1fr)}
  .archive__row{grid-template-columns:1fr}
}

/* =========================================================================
   STILLS  (copy on the left, drifting strip on the right)
   The strip drifts right to left on its own, the pointer can grab and throw
   it, and a click opens the still full screen. No colour treatment: the
   frames are shown as shot.
   ========================================================================= */
/* Both sides halved: Stills sits in the middle of the tightened run. */
.stills{padding-block:calc(var(--sec-y) / 2);overflow:hidden}
.stills__grid{
  display:grid;
  grid-template-columns:minmax(260px,26%) minmax(0,1fr);
  gap:clamp(2rem,4vw,4.5rem);
  align-items:center;
  padding-left:calc(max((100% - var(--shell)) / 2, 0px) + var(--gutter));
}
.stills__head .sec-lede{max-width:34ch}

.stills__viewport{
  position:relative;
  overflow-x:auto;overflow-y:hidden;   /* native scroller when JS is off */
  cursor:grab;
  touch-action:pan-y;      /* we take horizontal, the page keeps vertical */
  scrollbar-width:none;
  /* The strip drifts right to left, so the left edge is where a frame leaves.
     Cutting it on a hard line reads as a clipped element; fading it reads as
     the strip continuing past the copy. Only the leaving edge is faded: the
     right edge runs off the viewport and has no line to soften. */
  --fade:clamp(48px,7vw,140px);
  -webkit-mask-image:linear-gradient(to right,transparent 0,#000 var(--fade));
  mask-image:linear-gradient(to right,transparent 0,#000 var(--fade));
}
.stills__viewport::-webkit-scrollbar{display:none}
.stills__viewport[data-drag="on"]{overflow:hidden}
.stills__viewport[data-drag="active"]{cursor:grabbing}

.stills__track{
  display:flex;align-items:center;
  gap:clamp(14px,1.6vw,26px);
  width:max-content;will-change:transform;
  /* The viewport clips on the y axis, so a frame growing on hover had its top
     and bottom sliced off. This is the room to grow into, the same as the reels
     track carries. */
  padding-block:clamp(12px,1.6vw,24px);
}

.frame{
  position:relative;flex:none;
  border-radius:var(--r-media);
  width:clamp(200px,20vw,300px);
  aspect-ratio:4/5;
  border:1px solid var(--line);
  overflow:hidden;
  background:var(--ink-200);
}
.frame img{
  width:100%;height:100%;object-fit:cover;
  -webkit-user-drag:none;user-select:none;
  transition:transform .3s var(--ease);
}
.frame__hit{position:absolute;inset:0;z-index:2}
/* Press feedback, on pointer-down rather than on release. Without this a tap
   on a phone did nothing at all until the overlay opened, because hover is the
   only state these carried and a touch screen has no hover. The press is fast
   on the way in and settles back at the normal 0.3s.

   Two shapes, because a press has to read as a press in both places: where the
   card is already lifted to 1.06 by hover it sinks back toward the page, and
   where it is sitting flat on a touch screen it pushes in below 1. */
@media (hover:hover){
  .tile:has(.tile__hit:active),
  .frame:has(.frame__hit:active){transform:scale(1.03);transition-duration:.1s}
}
@media (hover:none){
  .tile:has(.tile__hit:active),
  .frame:has(.frame__hit:active){transform:scale(.97);transition-duration:.1s}
}

/* Same gesture as a video tile, same 0.3s: the frame pops and lifts over its
   neighbours. Pointer only, since a touch device has no hover to leave and the
   card would stay stuck large after a tap. */
.frame{transition:transform .34s var(--ease-pop)}
@media (hover:hover){
  .frame:hover{transform:scale(1.04);z-index:3}
}
.frame:focus-within{border-color:var(--cream)}

@media (max-width:860px){
  .stills__grid{
    grid-template-columns:minmax(0,1fr);
    gap:clamp(2rem,5vw,3rem);
    padding-right:var(--gutter);
  }
  .stills__viewport{margin-right:calc(var(--gutter) * -1)}
  .frame{width:min(62vw,300px)}
}

/* =========================================================================
   PROCESS  (sticky stack)
   ========================================================================= */
/* Closes the tightened run; the far side of the stack keeps its own spacing. */
.process{padding-top:calc(var(--sec-y) / 2)}
.process__title{margin-bottom:clamp(3rem,7vw,6rem)}
.stack{position:relative}
.stack__card{
  position:sticky;top:0;
  min-height:100dvh;
  display:flex;align-items:center;
  /* Opaque: each card is a video slot, so the grid must not show through. */
  background:var(--ink-100);
  overflow:hidden;
  will-change:transform,opacity;
}

/* Backdrop photo per step. The scrim is heavy enough that the cream copy keeps
   its contrast over any image dropped in here, which matters because these are
   meant to be swapped for real set photography. */
.stack__bg{position:absolute;inset:0;z-index:0}
.stack__bg img{
  width:100%;height:100%;object-fit:cover;
  filter:grayscale(.55) contrast(1.05) brightness(.5);
}
.stack__bg::after{
  content:"";position:absolute;inset:0;
  background:
    linear-gradient(100deg,rgba(0,0,0,.94) 0%,rgba(0,0,0,.86) 42%,rgba(0,0,0,.6) 100%),
    linear-gradient(to top,rgba(0,0,0,.7),transparent 55%);
}
.stack__inner{position:relative;z-index:1;max-width:var(--shell)}
.stack__h{
  font-size:clamp(3rem,1.5rem + 7vw,7.5rem);
  font-weight:900;letter-spacing:-.045em;line-height:.95;
  color:var(--cream);
}
.stack__p{margin-top:1.75rem;max-width:52ch;color:var(--text-mid);font-size:clamp(1.0625rem,1rem + .35vw,1.25rem)}
.stack__list{
  display:grid;gap:.55rem;
  margin-top:2.25rem;padding-top:1.75rem;
  border-top:1px solid var(--line);
  max-width:52ch;
}
.stack__list li{
  position:relative;padding-left:1.4rem;
  font-size:.9375rem;color:var(--text-hi);
}
.stack__list li::before{
  content:"";position:absolute;left:0;top:.72em;
  width:8px;height:1px;background:var(--blue-500);
}

/* =========================================================================
   STUDIO  (asymmetric split)
   ========================================================================= */
.studio{padding-block:var(--sec-y)}
.studio__grid{
  display:grid;grid-template-columns:repeat(12,1fr);
  gap:clamp(2.5rem,5vw,5rem);align-items:center;
}
.studio__media{
  grid-column:1/6;
  position:relative;aspect-ratio:11/14;overflow:hidden;
  border-radius:var(--r-media);
  border:1px solid var(--line);background:var(--ink-200);
}
.studio__media img{width:100%;height:112%;object-fit:cover;will-change:transform}
.studio__copy{grid-column:7/13}
.studio__copy p{margin-top:1.5rem;color:var(--text-mid);max-width:52ch}

.facts{margin-top:2.75rem;border-top:1px solid var(--line)}
.facts > div{
  display:grid;grid-template-columns:minmax(90px,150px) 1fr;
  gap:1.5rem;padding:1rem 0;border-bottom:1px solid var(--line);
}
.facts dt{font-family:var(--mono);font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;color:var(--blue-500);padding-top:.2rem}
.facts dd{color:var(--text-hi);font-size:.9375rem}

@media (max-width:900px){
  /* Collapse to a single track, not just a single span. Twelve 1fr tracks keep
     their eleven gaps even when every child spans 1/13, and at the phone gap of
     40px that is 440px of gap alone, which is wider than the viewport. Spanning
     the full row does not help while the row itself cannot fit. */
  .studio__grid{grid-template-columns:1fr}
  .studio__media,.studio__copy{grid-column:auto}
  /* Stays portrait on a phone. The slot used to go 4/3 here, which was fine
     for the landscape placeholder but centre-crops a real 4/5 portrait into a
     letterbox across the middle of the face. */
  .studio__media{aspect-ratio:4/5}
}

/* =========================================================================
   CONTACT
   ========================================================================= */
.contact{
  position:relative;
  padding-block:var(--sec-y);
}
.contact .shell{position:relative;z-index:1}
/* The headline shares its column with the contact details, so the full display
   scale would wrap it to three ragged lines. */
.contact .sec-title{font-size:clamp(2.25rem,1.3rem + 3.2vw,3.75rem)}


.field{display:grid;gap:.5rem;margin-bottom:1.5rem}
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:1.25rem}
.field label{
  font-family:var(--mono);font-size:.6875rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--cream);     /* 17.8:1 */
}
.field input,.field select,.field textarea{
  width:100%;padding:.9rem 1rem;
  background:var(--ink-200);
  border:1px solid var(--line-strong);
  color:var(--text-hi);
  transition:border-color .25s var(--ease),background-color .25s var(--ease);
}
.field textarea{resize:vertical;min-height:8.5rem;line-height:1.6}
.field select{
  appearance:none;cursor:pointer;
  background-image:linear-gradient(45deg,transparent 50%,var(--blue-500) 50%),
                   linear-gradient(135deg,var(--blue-500) 50%,transparent 50%);
  background-position:calc(100% - 20px) calc(50% + 2px),calc(100% - 14px) calc(50% + 2px);
  background-size:6px 6px,6px 6px;
  background-repeat:no-repeat;
  padding-right:2.75rem;
}
.field select option{background:var(--ink-200);color:var(--text-hi)}
.field input:hover,.field select:hover,.field textarea:hover{border-color:rgba(255,232,219,.4)}
.field input:focus-visible,.field select:focus-visible,.field textarea:focus-visible{
  border-color:var(--cream);outline-offset:2px;
}
.field__help{font-family:var(--mono);font-size:.6875rem;letter-spacing:.04em;color:var(--text-mid)} /* 7.4:1 */
.field__err{
  display:flex;align-items:center;gap:.45rem;
  font-size:.8125rem;color:var(--coral);              /* 6.6:1 */
}
.field__err[hidden]{display:none}
.field__err svg{width:15px;height:15px;fill:currentColor;flex:none}
.field[data-invalid] input,.field[data-invalid] select,.field[data-invalid] textarea{border-color:var(--coral)}



.btn[data-submit]{margin-top:.5rem;position:relative}
.btn__spin{width:1.15em;height:1.15em;fill:currentColor;display:none}
.btn[data-state="sending"] .btn__spin{display:block;animation:spin .8s linear infinite}
.btn[data-state="sending"]{pointer-events:none;opacity:.75}
@keyframes spin{to{transform:rotate(360deg)}}

.form__status{
  display:flex;align-items:flex-start;gap:.6rem;
  margin-top:1.25rem;padding:1rem 1.15rem;
  border:1px solid var(--line-strong);
  font-size:.9375rem;color:var(--text-hi);
}
.form__status[hidden]{display:none}
.form__status svg{width:19px;height:19px;flex:none;margin-top:.15rem}
.form__status[data-tone="ok"]{border-color:var(--blue-500)}
.form__status[data-tone="ok"] svg{fill:var(--cream)}
.form__status[data-tone="bad"]{border-color:var(--coral)}
.form__status[data-tone="bad"] svg{fill:var(--coral)}

@media (max-width:520px){
  .field-row{grid-template-columns:1fr;gap:0}
}


/* ---- contact card -------------------------------------------------------
   Composition from the supplied ContactCard: one bordered card, plus marks
   sitting on the corners, copy and contact details across two thirds, the
   form walled off in the last third. Its grid is reproduced exactly:
   one column, then two at md, then three at lg. Corners stay square, since
   every control inside is square. */
.ccard{
  position:relative;
  display:grid;grid-template-columns:1fr;
  border:1px solid var(--line-strong);
  background:rgba(0,0,0,.85);
  backdrop-filter:blur(10px);
  -webkit-backdrop-filter:blur(10px);
}
.ccard__mark{
  position:absolute;display:block;
  width:22px;height:22px;color:var(--text-lo);
  pointer-events:none;
}
.ccard__mark svg{width:100%;height:100%;fill:currentColor}
.ccard__mark:nth-child(1){top:-11px;left:-11px}
.ccard__mark:nth-child(2){top:-11px;right:-11px}
.ccard__mark:nth-child(3){bottom:-11px;left:-11px}
.ccard__mark:nth-child(4){bottom:-11px;right:-11px}

.ccard__intro{padding:clamp(1.5rem,4vw,3rem)}
.ccard__intro .sec-lede{margin-top:1.25rem;max-width:52ch}

.ccard__info{
  display:grid;gap:.5rem 1.5rem;
  margin-top:clamp(2rem,4vw,3rem);
}
.cinfo{display:flex;align-items:center;gap:.9rem;padding:.6rem 0}
.cinfo__ico{
  display:grid;place-items:center;flex:none;
  width:44px;height:44px;
  background:rgba(255,232,219,.06);
  border:1px solid var(--line);
  color:var(--blue-300);
}
.cinfo__ico svg{width:19px;height:19px;fill:currentColor}
.cinfo__label{font-weight:600;font-size:.9375rem;line-height:1.3}
.cinfo__value{
  display:inline-block;
  font-family:var(--mono);font-size:.75rem;letter-spacing:.02em;
  color:var(--text-mid);
  border-bottom:1px solid transparent;
  transition:color .25s var(--ease),border-color .25s var(--ease);
}
.cinfo__value:hover{color:var(--cream);border-bottom-color:var(--cream)}

.ccard__form{
  display:flex;align-items:center;
  padding:clamp(1.5rem,4vw,2rem);
  border-top:1px solid var(--line);
  background:rgba(255,232,219,.03);
}
.ccard__form .form{width:100%}

@media (min-width:768px){
  .ccard{grid-template-columns:repeat(2,1fr)}
  .ccard__info{grid-template-columns:repeat(2,1fr)}
  .ccard__form{border-top:none;border-left:1px solid var(--line)}
}
@media (min-width:1100px){
  .ccard{grid-template-columns:repeat(3,1fr)}
  .ccard__intro{grid-column:span 2}
  .ccard__info{grid-template-columns:repeat(3,1fr)}
}

/* =========================================================================
   FOOTER
   ========================================================================= */
.foot{padding-block:clamp(2.5rem,5vw,3.5rem)}
.foot__inner{
  display:flex;align-items:center;justify-content:space-between;
  flex-wrap:wrap;gap:1.5rem;
}
.foot__id{display:grid;gap:.2rem}
.foot__name{font-weight:700;letter-spacing:-.02em}
/* Where he is, on every page. Search engines weight a stated location heavily
   for a service business and nothing on the site said one. */
.foot__where{font-family:var(--mono);font-size:.6875rem;letter-spacing:.06em;color:var(--text-mid)}
.foot__social{display:flex;gap:.5rem}
.foot__social a{
  display:grid;place-items:center;width:38px;height:38px;
  border:1px solid var(--line);color:var(--text-mid);
  transition:color .25s var(--ease),border-color .25s var(--ease);
}
.foot__social a:hover{color:var(--cream);border-color:var(--line-strong)}
.foot__social svg{width:18px;height:18px;fill:currentColor}
.foot__legal{font-family:var(--mono);font-size:.6875rem;letter-spacing:.06em;color:var(--text-mid)}

/* =========================================================================
   LIGHTBOX  (borderless: title above the frame, video floating on the scrim)
   ========================================================================= */
.lb{position:fixed;inset:0;z-index:90;display:grid;place-items:center;padding:var(--gutter)}
.lb[hidden]{display:none}
.lb__scrim{
  position:absolute;inset:0;
  background:rgba(0,0,0,.92);
  backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
}
/* Width comes from the video, not a fixed box, so the title lands exactly
   above its left edge and the arrows hug its sides. A 9:16 piece is far
   narrower than a 16:9 one and both should look deliberate. */
.lb__panel{position:relative;width:auto;max-width:100%}

/* Title sits above the frame, hard left, with the close opposite it. No rule
   under it and no box around the whole thing. */
.lb__bar{
  display:flex;align-items:center;justify-content:space-between;gap:1rem;
  padding:0 0 .85rem;
}
.lb__title{font-size:1.0625rem;font-weight:700;letter-spacing:-.02em;color:var(--cream)}
.lb__close{
  display:grid;place-items:center;width:36px;height:36px;
  border:1px solid var(--line);color:var(--text-mid);
  transition:color .25s var(--ease),border-color .25s var(--ease),
             background-color .25s var(--ease);
}
.lb__close:hover{color:var(--ink-100);background:var(--cream);border-color:var(--cream)}
.lb__close svg{width:16px;height:16px;fill:currentColor}

.lb__frame{position:relative}
.lb__stage{position:relative}
.lb__video{
  display:block;
  width:auto;height:auto;
  max-width:min(1100px,92vw);
  max-height:78vh;
  background:var(--ink-000);
  border-radius:var(--r-media);
}
/* The placeholder has an aspect ratio but no intrinsic size, so it needs a
   width of its own or it collapses in a shrink-to-fit panel. */
.lb__stage .ph{width:min(900px,86vw)}

.lb__nav{
  position:absolute;top:50%;translate:0 -50%;z-index:2;
  display:grid;place-items:center;
  width:46px;height:46px;
  border:1px solid var(--line-strong);
  background:rgba(0,0,0,.72);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  color:var(--text-hi);
  transition:color .25s var(--ease),background-color .25s var(--ease),
             border-color .25s var(--ease),opacity .25s var(--ease);
}
.lb__nav svg{width:17px;height:17px;fill:currentColor}
.lb__nav:hover{color:var(--ink-100);background:var(--cream);border-color:var(--cream)}
.lb__nav--prev{left:0;translate:-50% -50%}
.lb__nav--next{right:0;translate:50% -50%}
.lb__nav[hidden]{display:none}

@media (max-width:560px){
  .lb__nav{width:40px;height:40px}
  /* No room to sit outside the frame at this width, so tuck them back in. */
  .lb__nav--prev{left:.4rem;translate:0 -50%}
  .lb__nav--next{right:.4rem;translate:0 -50%}
}

/* =========================================================================
   STILL VIEWER  (full screen, warps out of the thumbnail, blurred backdrop)
   ========================================================================= */
.viewer{
  position:fixed;inset:0;z-index:95;
  display:grid;
  /* Explicit tracks, same reason as .viewer__panel below: an auto row would
     be sized by the image and stop height:100% from clamping anything. */
  grid-template-columns:minmax(0,1fr);
  grid-template-rows:minmax(0,1fr);
  place-items:center;
}
.viewer[hidden]{display:none}
.viewer__scrim{
  position:absolute;inset:0;
  background:rgba(0,0,0,.82);
  backdrop-filter:blur(22px) saturate(120%);
  -webkit-backdrop-filter:blur(22px) saturate(120%);
}
.viewer__panel{
  position:relative;
  width:min(1500px,100%);height:100%;
  display:grid;
  /* minmax(0,1fr) rather than an auto track: an auto row would size itself to
     the image, and the image's own max-height:100% would then resolve against
     that grown row and never clamp. */
  grid-template-columns:minmax(0,1fr);
  grid-template-rows:minmax(0,1fr);
  place-items:center;
  padding:clamp(3.5rem,7vw,6rem) clamp(1rem,7vw,7rem);
  pointer-events:none;                /* clicks fall through to the scrim */
}
.viewer__panel > *{pointer-events:auto}
/* Fills the grid area and letterboxes inside it. Sizing from the image's own
   intrinsic box instead would collapse to 0x0 for the frame between setting
   src and decode, which also breaks the warp measurement. */
.viewer__img{
  width:100%;height:100%;
  border-radius:var(--r-media);
  object-fit:contain;
  transform-origin:center center;
  will-change:transform,opacity,filter;
  /* object-fit leaves transparent letterboxing inside this box, and that was
     eating clicks meant for the scrim behind it. Controls sit above and keep
     their own pointer events. */
  pointer-events:none;
}

.viewer__nav,.viewer__close{
  position:absolute;
  display:grid;place-items:center;
  width:46px;height:46px;
  border:1px solid var(--line-strong);
  background:rgba(0,0,0,.72);
  color:var(--text-hi);
  transition:color .25s var(--ease),border-color .25s var(--ease),
             background-color .25s var(--ease);
}
.viewer__nav svg,.viewer__close svg{width:18px;height:18px;fill:currentColor}
.viewer__nav:hover,.viewer__close:hover{
  color:var(--ink-100);background:var(--cream);border-color:var(--cream);
}
.viewer__nav--prev{left:clamp(.5rem,2vw,2rem);top:50%;translate:0 -50%}
.viewer__nav--next{right:clamp(.5rem,2vw,2rem);top:50%;translate:0 -50%}
.viewer__close{top:clamp(1rem,3vw,2rem);right:clamp(.5rem,2vw,2rem)}
.viewer__count{
  position:absolute;bottom:clamp(1rem,3vw,2rem);left:50%;translate:-50% 0;
  font-family:var(--mono);font-size:.75rem;letter-spacing:.12em;
  color:var(--text-mid);
}

@media (max-width:640px){
  .viewer__nav{bottom:clamp(1rem,3vw,2rem);top:auto;translate:0 0}
  .viewer__nav--prev{left:calc(50% - 60px)}
  .viewer__nav--next{right:calc(50% - 60px)}
  .viewer__count{bottom:calc(clamp(1rem,3vw,2rem) + 62px)}
}

/* =========================================================================
   GRAIN  (fixed layer only, never on a scrolling container)
   ========================================================================= */
.grain{
  position:fixed;inset:0;z-index:100;pointer-events:none;
  opacity:.055;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* =========================================================================
   MOTION GATES
   Anything scripted starts hidden only when JS is present (js-on), so the
   page still reads with JS disabled.
   ========================================================================= */
.js-on [data-fade]{opacity:0}
.js-on [data-split] .ln > span{transform:translateY(105%)}
.js-on [data-reveal]{opacity:0;transform:translateY(28px)}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .js-on [data-fade],
  .js-on [data-reveal]{opacity:1;transform:none}
  .js-on [data-split] .ln > span{transform:none}
  /* The marquee and the stills strip are intentionally NOT reset here: both
     are slow linear horizontal drifts that the script keeps running for
     everyone. See the two-tier note at the top of js/main.js. */
  /* The wipe is excluded too, and has to be. Every :not() here adds its
     argument's specificity, so this selector outscores a plain class and was
     beating the wipe's own reduced-motion rules even though those carry
     !important. The loader therefore cut in and out instantly for anyone with
     reduced motion switched on instead of cross-fading the way its rules
     intend. Excluded here, governed there.

     The tile and the frame are excluded for the same reason. Their only
     transition is the hover pop, and forcing that to zero duration does not
     remove the movement, it makes it snap. A card answering the pointer over
     its own footprint is not what this setting is defending against; the size
     of that pop is reduced further down instead. The mark's own pieces join
     them, so the brand tears rather than jumping apart.

     One :not() taking a list, rather than a chain of them. A chain adds each
     argument's specificity and this selector had climbed to six classes,
     which is how it kept silently outranking the very rules meant to exempt
     things from it. A list scores as its most specific argument, so it stays
     at one class and a deliberate exemption can actually win. */
  *:not(.marquee__row,.stills__track,.wipe,.wipe__panel,.wipe__edge,.wipe__mark,.tile,.frame,.m-head,.m-b1,.m-b2,.m-b3),
  *:not(.marquee__row,.stills__track,.wipe,.wipe__panel,.wipe__edge,.wipe__mark,.tile,.frame,.m-head,.m-b1,.m-b2,.m-b3)::before,
  *:not(.marquee__row,.stills__track,.wipe,.wipe__panel,.wipe__edge,.wipe__mark,.tile,.frame,.m-head,.m-b1,.m-b2,.m-b3)::after{
    animation-duration:.01ms!important;animation-iteration-count:1!important;
    transition-duration:.01ms!important;
  }
}

@media (prefers-reduced-transparency:reduce){
  .nav,.lb__scrim{backdrop-filter:none;-webkit-backdrop-filter:none;background:var(--ink-100)}
  .lb__scrim{background:rgba(0,0,0,.98)}
}

/* prefers-contrast: more. The translucent chrome goes near-solid and picks up a
   defined border, since a blurred surface is exactly what fails first when
   someone needs contrast. Sits after the reduced-transparency block so the two
   compose rather than fight. */
@media (prefers-contrast:more){
  .nav::before{background:var(--ink-100);backdrop-filter:none;-webkit-backdrop-filter:none}
  .nav{border-bottom:1px solid var(--cream)}
  .ccard{background:var(--ink-100);backdrop-filter:none;-webkit-backdrop-filter:none;border-color:var(--cream)}
  .lb__scrim,.viewer__scrim{background:var(--ink-100);backdrop-filter:none;-webkit-backdrop-filter:none}
  .btn--ghost{border-color:var(--cream)}
  .filter{border-color:var(--line-strong)}
  .ph__meta{background:var(--ink-100)}
}

/* =========================================================================
   PAGE WIPE  (z-index 300, above everything including the grain)

   The archive pages used to arrive with the grid half decoded and flash as the
   photographs filled in. This covers that gap with something deliberate: a hard
   edged black panel that sweeps across, the way a scene transition wipes in a
   film, with the mark held in the middle while the new page gets itself ready.

   The panel goes out and comes back. It sweeps in from the left to cover the
   page being left, and on the next page the same edge retraces its path to
   uncover it. It used to carry on in the same direction instead, which read as
   the same animation playing twice rather than one movement returning.

   Timing is not a fixed wait. The panel holds until the images that will
   actually be on screen have decoded, with a floor so it never blinks past too
   fast to read, and a ceiling so a slow connection cannot strand anyone behind
   it. Typical crossing is about two seconds.
   ========================================================================= */
.wipe{
  position:fixed;inset:0;z-index:300;
  display:none;                      /* JS-off never sees it: see the inline script */
  pointer-events:none;
}
.is-wiping .wipe{display:block;pointer-events:auto}

/* The sweep is diagonal: a 45 degree edge, with a 4px white line riding on it.
   Both the panel's clip and the line are driven by one registered custom
   property, so they cannot drift apart — the line sits exactly on the boundary
   because it is positioned from the same number.

   The travel is 100vw + 100vh rather than 100vw: a 45 degree edge has to cross
   the height as well as the width before the far corner is covered. */
@property --wipe-p{syntax:'<number>';inherits:true;initial-value:0}

/* Where the edge is, as a length along the top. --wipe-p is the 0..1 progress
   of the animation; this turns it into the one position that the clip and the
   white line both read. The two states differ only in which way it counts. */
.wipe{--wipe-x:calc(var(--wipe-p) * (100vw + 100vh))}

.wipe__panel,
.wipe__mark{
  /* covering: everything left of an edge already past the far corner */
  clip-path:polygon(0 0, 200vmax 0, 200vmax 100%, 0 100%);
}
.wipe__panel{
  position:absolute;inset:0;
  background:var(--ink-100);
  will-change:clip-path;
}
.wipe__edge{
  position:absolute;top:0;left:0;
  width:4px;height:300vmax;
  background:#fff;
  transform-origin:top center;
  /* rotate(45deg) points a vertical bar down and to the left, which is the way
     the clip boundary runs; the translate puts its top on that boundary */
  transform:translateX(var(--wipe-x)) rotate(45deg);
  opacity:0;
  will-change:transform;
}

/* Both states use the SAME clip: black is whatever is behind the edge. What
   differs is which way the edge travels, so the reveal retraces the cover
   rather than carrying on past it. Viktor saw the two passes as one animation
   played twice in the same direction; run backwards, they read as one movement
   out and back. */
.is-wiping[data-wipe="in"] .wipe__panel,
.is-wiping[data-wipe="in"] .wipe__mark,
.is-wiping[data-wipe="out"] .wipe__panel,
.is-wiping[data-wipe="out"] .wipe__mark{
  clip-path:polygon(0 0, var(--wipe-x) 0, calc(var(--wipe-x) - 100vh) 100%, 0 100%);
}
.is-wiping[data-wipe="in"] .wipe__edge,
.is-wiping[data-wipe="out"] .wipe__edge{opacity:1}

/* Leaving: the edge crosses from the left and black fills in behind it. */
.is-wiping[data-wipe="in"] .wipe{animation:wipe-sweep .58s var(--ease) both}

/* Arriving: the edge retraces its path, so the black retreats the way it came
   and the page is uncovered from the right.

   Counted down rather than run with animation-direction:reverse. Reversing an
   animation reverses its timing function too, and --ease is an expo-out: played
   backwards it becomes an expo-in that barely moves for most of its length and
   then snaps. The position counts down while the curve stays an ease-out. */
.is-wiping[data-wipe="out"] .wipe{
  animation:wipe-sweep .68s var(--ease) both;
  --wipe-x:calc((1 - var(--wipe-p)) * (100vw + 100vh));
}

@keyframes wipe-sweep{from{--wipe-p:0}to{--wipe-p:1}}

.wipe__mark{
  position:absolute;inset:0;
  display:flex;align-items:center;justify-content:center;
  opacity:1;
  /* Deliberately not promoted, for either property. Its four pieces animate
     transforms inside it, and a composited parent whose contents move has to
     be re-rasterised every frame. That is what made the mark flicker. The
     panel carries the will-change instead: it has no moving children. */
}
/* The mark carries the screen alone now, so it is sized to be the subject
   rather than a label sitting above a name. */
.wipe__mark svg{width:clamp(96px,11vw,168px);height:auto}

/* The mark never fades, in either direction. It is cut by the same edge as the
   panel, so it belongs to the panel and leaves when the panel passes over it.

   It used to fade in on arrival and out on reveal, which caused both of the
   things it was supposed to prevent. The fade-in was a flash on every single
   page load. And the fade-out was worse: the arrival rule stopped matching the
   moment data-wipe was set, so the animation was removed, opacity snapped back
   to its base 1, and only then transitioned to 0. The logo brightened before it
   left. Sharing the clip removes every one of those states. */
.is-wiping[data-wipe="out"] .wipe__mark .m-head,
.is-wiping[data-wipe="out"] .wipe__mark .m-b1,
.is-wiping[data-wipe="out"] .wipe__mark .m-b2,
.is-wiping[data-wipe="out"] .wipe__mark .m-b3{animation:none}

/* Reduced motion keeps the sweep and loses the loop, rather than losing both.

   The two are not the same risk. The sweep is one brief pass of a solid panel,
   over in about half a second, which is a scene transition and not the sort of
   sustained travel this setting exists to prevent. An endlessly repeating tear
   is a different thing: a small oscillation running the whole time the loader
   is up is precisely what the guidance names, so that is what goes. The mark
   breathes on opacity instead, which is the substitution the guidance asks for
   rather than removing feedback altogether.

   This is a judgement about which half is actually harmful, not a decision to
   ignore the setting. Turning the whole loader static is easy to defend on
   paper and leaves someone with the setting on staring at a black rectangle. */

/* The hover pop under reduced motion. Cutting a 4% scale to zero duration does
   not remove the movement, it makes it snap, which is the harsher of the two.
   This setting is aimed at large travelling surfaces and parallax, not at a
   card answering the pointer over itself. Kept smooth and made smaller.

   html.js-on is needed to outrank the reset above: every :not() in that
   selector adds specificity, so a plain class loses to it even with
   !important. */
@media (prefers-reduced-motion:reduce) and (hover:hover){
  html.js-on .tile:hover,
  html.js-on .tile:focus-within{transform:scale(1.03)}
  html.js-on .frame:hover{transform:scale(1.02)}
}

/* The mark's tear is a transform, so the reduced-motion reset would hold it at
   0.01ms and make it jump apart. It is a small movement inside a 20px brand,
   not a travelling surface, so it is kept smooth rather than removed. */
@media (prefers-reduced-motion:reduce){
  html.js-on .nav__mark svg g,
  html.js-on .nav__mark svg path{transition-duration:.5s!important}
}

/* The mark tears apart and back together while the loader is up. Each piece
   runs the same cycle on its own delay, so the tear ripples down the mark
   instead of every piece moving as one, and alternate brings it back together
   rather than snapping home at the end of each pass.

   Travel is larger than the nav's tear because this mark renders at 141px
   rather than 20: the same viewBox units read as a far smaller gesture there. */
.wipe__mark{--tear:1}
.wipe__mark .m-head,
.wipe__mark .m-b1,
.wipe__mark .m-b2,
.wipe__mark .m-b3{
  transform-box:view-box;transform-origin:100px 100px;
  /* Promote the pieces that move rather than the box around them: a layer on
     the parent has to be repainted every time a child shifts inside it. */
  will-change:transform;
  animation:1.6s var(--ease-loop) infinite alternate both;
}
/* --tear-shift carries the loop's phase across a navigation. Each document
   starts its own animations at zero, so the tear used to snap back to the
   beginning the instant the new HTML committed, about 600ms into a transition
   that is supposed to read as continuous. The arriving page's inline script
   sets this to minus however long the loop had already been running on the page
   being left, so the tear picks up where that one stopped.

   A negative delay rather than setting currentTime afterwards: this is in
   effect at the first style resolution, so there is no frame at zero to jump
   away from. Wrapped to the 3.2s round trip by the script, which is a whole
   alternate cycle, so the phase is identical and the number stays small. */
.wipe__mark .m-head{animation-name:mk-head;animation-delay:var(--tear-shift,0s)}
.wipe__mark .m-b1{animation-name:mk-b1;animation-delay:calc(var(--tear-shift,0s) + .08s)}
.wipe__mark .m-b2{animation-name:mk-b2;animation-delay:calc(var(--tear-shift,0s) + .16s)}
.wipe__mark .m-b3{animation-name:mk-b3;animation-delay:calc(var(--tear-shift,0s) + .24s)}

@keyframes mk-head{from{transform:none}to{transform:translate(calc(14px * var(--tear)),calc(-11px * var(--tear)))}}
@keyframes mk-b1{from{transform:none}to{transform:translateX(calc(-22px * var(--tear)))}}
@keyframes mk-b2{from{transform:none}to{transform:translateX(calc(25px * var(--tear)))}}
@keyframes mk-b3{from{transform:none}to{transform:translate(calc(-14px * var(--tear)),calc(9px * var(--tear)))}}

@media (prefers-reduced-motion:reduce){
  /* The tear still runs, at a little over a third of the travel. Stopping it
     dead left the loader with nothing moving at all, which was worse than the
     movement it was avoiding; this keeps the mark alive without throwing it
     around. One number, so it is easy to retune or set to 0.

     Placed after the base --tear, not before: both are one class, so the
     later one wins and putting this first silently did nothing. */
  .wipe__mark{--tear:.38}
}
