/* ==========================================================
   Jonny Warschauer — editor & colorist portfolio
   Light gallery: paper ground, ink type, the footage does the talking.
   ========================================================== */

:root {
  --paper:     #f4f1ea;
  --ink:       #14110f;
  --muted:     #4a4238;   /* 4.60:1 over the backdrop at its worst viewport, 9.4:1 on bare paper */
  --hairline:  rgba(20, 17, 15, .12);
  --placeholder: #e8e4db;

  --gutter:    clamp(1rem, 4vw, 3.5rem);
  --measure:   1280px;

  --ease:      cubic-bezier(.22, .61, .36, 1);
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

body.is-locked { overflow: hidden; }

img { display: block; max-width: 100%; height: auto; }

a { color: inherit; }

/* --- backdrop ------------------------------------------------------------ */

/* A blurred colour field, not a scene.

   Two layers doing different jobs: a 2KB still that everyone gets, and the
   video on top for anyone whose visit can carry 4.5MB. Blurred this hard they
   look the same — the video buys motion and nothing else — so the page never
   depends on it arriving.

   The contrast maths is load-bearing. The source's darkest region sits at
   luma 35 even after blurring, so it is tone-lifted (contrast down, brightness
   up) before compositing; that is what buys usable opacity at all.

   Measured at real viewport sizes, the two knobs do different jobs: opacity
   sets how dark the ground gets, blur sets how much shape survives — and blur
   barely moves the darkest value. So the blur is low (6px) and costs almost
   nothing, while the 45% is what the contrast budget is actually spent on. The
   ground bottoms out near rgb(178,177,174), which is why --muted is #4a4238.
   The opacity, the tone-lift and that token are one calculation. Change any of
   them and re-measure the credit lines at a real viewport size — blur is an
   absolute length, so a small test canvas will lie to you, and `cover` crops
   differently per aspect ratio, so check a wide 2560 and a tall narrow one:
   those are the worst cases, not the laptop you are sitting at. */
.backdrop {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;              /* clips the fringe the blur throws past the edges */
  pointer-events: none;
  opacity: .45;
}

.backdrop__layer {
  position: absolute;
  inset: 0;
  background: url("assets/backdrop-still.jpg") center / cover no-repeat;
  /* Soft enough to stay a ground, sharp enough that the horizon reads. The
     lift keeps the dark rock from dragging the page down with it. */
  filter: blur(6px) saturate(.9) contrast(.55) brightness(1.45);
  transform: scale(1.18);
}

.backdrop__video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;                    /* cross-fades over the still once it can play */
  transition: opacity 1.6s var(--ease);
}

.backdrop__video.is-ready { opacity: 1; }

/* --- shared -------------------------------------------------------------- */

.section {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.eyebrow {
  margin: 0 0 1.75rem;
  padding-bottom: .875rem;
  border-bottom: 1px solid var(--hairline);
  font-size: .6875rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Read aloud, never drawn. Used to tell screen reader users that a link leaves
   the site — sighted visitors get that from the tab opening. */
.u-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.meta {
  margin: .375rem 0 0;
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- anchor scrolling ---------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* Keeps a section heading from landing underneath the sticky bar. */
.section { scroll-margin-top: clamp(4.5rem, 9vw, 5.75rem); }

/* --- sticky topbar ------------------------------------------------------- */

/* A condensed second masthead: it stays out of the way while you read down the
   page and returns on the first upward gesture, so the wordmark and the section
   links are never more than one scroll away. */
.topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  border-bottom: 1px solid var(--hairline);
  /* Stills pass under the bar and stay legible through it — the work keeps
     showing even in the chrome. */
  background: rgba(244, 241, 234, .76);
  backdrop-filter: blur(16px) saturate(150%);
  -webkit-backdrop-filter: blur(16px) saturate(150%);
  transform: translateY(-100%);
  visibility: hidden;          /* also keeps it out of tab order while away */
  /* Same deferral the lightbox uses: visibility waits for the slide out, but
     flips instantly on the way in. */
  transition: transform .45s var(--ease), visibility 0s linear .45s;
}

.topbar.is-visible {
  transform: translateY(0);
  visibility: visible;
  transition: transform .45s var(--ease), visibility 0s linear 0s;
}

/* Without the blur, the type needs a nearly solid ground to sit on. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .topbar { background: rgba(244, 241, 234, .97); }
}

.topbar__inner {
  max-width: var(--measure);
  margin: 0 auto;
  padding: .5rem var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(1rem, 4vw, 2rem);
}

.topbar__home {
  display: block;
  line-height: 0;
  opacity: .85;
  transition: opacity .25s var(--ease);
}

.topbar__home:hover,
.topbar__home:focus-visible { opacity: 1; }

/* The wordmark file carries ~28% transparent margin of its own, so the mark
   reads smaller than this height implies. Sized against the ink, not the box. */
.topbar__home img {
  width: auto;
  height: clamp(40px, 6vw, 50px);
}

.topbar__nav {
  display: flex;
  gap: clamp(1rem, 3vw, 1.75rem);
}

.topbar__nav a {
  font-size: .6875rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
  transition: border-color .25s var(--ease);
}

.topbar__nav a:hover,
.topbar__nav a:focus-visible { border-bottom-color: var(--ink); }

/* --- masthead ------------------------------------------------------------ */

.masthead {
  max-width: var(--measure);
  margin: 0 auto;
  padding: clamp(3rem, 9vw, 6.5rem) var(--gutter) clamp(2.5rem, 6vw, 4.5rem);
  text-align: center;
}

/* The wordmark is the page's h1 — the heading wrapper is structural only. */
.masthead__title { margin: 0; }

.masthead__mark {
  width: clamp(200px, 40vw, 300px);
  height: auto;
  margin: 0 auto;
}

.masthead__role {
  margin: 1.25rem 0 0;
  font-size: .75rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--muted);
}

.masthead__nav {
  display: flex;
  justify-content: center;
  gap: clamp(1.25rem, 4vw, 2.25rem);
  margin-top: 2rem;
}

.masthead__nav a {
  font-size: .75rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
  transition: border-color .25s var(--ease), color .25s var(--ease);
}

.masthead__nav a:hover,
.masthead__nav a:focus-visible { border-bottom-color: var(--ink); }

/* --- work grid ----------------------------------------------------------- */

.grid {
  display: grid;
  grid-template-columns: 1fr;
  column-gap: clamp(1.25rem, 3vw, 2.25rem);
  row-gap: clamp(2.5rem, 5vw, 3.75rem);
}

@media (min-width: 900px) {
  .grid { grid-template-columns: repeat(2, 1fr); }
}

/* The grid item. The play link lives inside it rather than wrapping it, so a
   laurel line can carry a link of its own without nesting one <a> in another. */
.tile { display: block; }

.tile__watch {
  display: block;
  color: inherit;
  text-decoration: none;
}

.tile__frame {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--placeholder);
}

/* Stills arrive at mixed aspect ratios, so every one is cropped to the
   same 16:9 window rather than letting the grid go ragged. */
.tile__frame > img:first-child {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .8s var(--ease);
}

.tile__scrim {
  position: absolute;
  inset: 0;
  background: rgba(10, 8, 7, .3);
  opacity: 0;
  transition: opacity .4s var(--ease);
}

/* The play glyph is translucent white, so it only reads once the scrim
   is under it — both fade in together. */
.tile__play {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 60px;
  height: 60px;
  opacity: 0;
  transform: translate(-50%, -50%) scale(.9);
  transition: opacity .4s var(--ease), transform .4s var(--ease);
}

/* Festival laurel artwork, pinned to the still's top-right corner. It comes
   after the scrim in the markup, so it stays bright over the darkened hover
   state, and it sits outside the still, so it holds still while the still zooms.
   Laurels are white type, so the size floor is set by legibility, not layout:
   below ~96px wide the festival name stops being readable. The shadow is only
   there for a still that turns out bright in that corner. */
.tile__badge {
  position: absolute;
  top: 4.5%;
  right: 3.5%;
  width: clamp(96px, 26%, 172px);
  pointer-events: none;
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, .4));
}

.tile__watch:hover .tile__frame > img:first-child,
.tile__watch:focus-visible .tile__frame > img:first-child { transform: scale(1.035); }

.tile__watch:hover .tile__scrim,
.tile__watch:focus-visible .tile__scrim { opacity: 1; }

.tile__watch:hover .tile__play,
.tile__watch:focus-visible .tile__play {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

.tile__watch:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 6px;
}

.tile__title {
  margin: 1rem 0 0;
  font-size: 1.0625rem;
  font-weight: 500;
  letter-spacing: .01em;
  line-height: 1.3;
}

/* A festival, premiere, or award. Same size as .meta so the baselines agree;
   the label reads as a label through case and tracking, not size. */
.laurel {
  margin: .5rem 0 0;
  font-size: .6875rem;
  letter-spacing: .04em;
  line-height: 1.5;
}

/* Inline links inside prose and laurel lines: the same hairline the contact
   list uses, so every link on the page behaves one way. */
.about__text a,
.laurel__link {
  text-decoration: none;
  border-bottom: 1px solid var(--hairline);
  padding-bottom: 1px;
  transition: border-color .25s var(--ease);
}

.about__text a:hover,
.about__text a:focus-visible,
.laurel__link:hover,
.laurel__link:focus-visible { border-bottom-color: var(--ink); }

/* The laurel line is 11px metadata, which is a small thing to hit with a
   thumb. This grows the touch target past 24px without touching the type or
   pushing the underline off the baseline. The vertical reach is kept inside
   the line's own top margin so it never steals a tap from the video link. */
.laurel__link { position: relative; }

.laurel__link::after {
  content: "";
  position: absolute;
  inset: -.35rem -.3rem;
}

.laurel__label {
  margin-right: .2em;
  color: var(--muted);
  letter-spacing: .1em;
  text-transform: uppercase;
}

/* A project with no video link yet: still and credits, no play affordance. */
.tile__watch--unlinked { cursor: default; }

.tile__watch--unlinked:hover .tile__frame > img:first-child { transform: none; }

/* --- about --------------------------------------------------------------- */

.about { margin-top: clamp(4.5rem, 10vw, 8rem); }

/* One centred column: the bio, then the line that introduces the photo, then
   the photo. Everything else on the page runs left-aligned, so centring is
   what marks this as the personal section. */
.about__body { text-align: center; }

.about__text {
  max-width: 46ch;
  margin: 0 auto;
}

.about__text p {
  margin: 0 0 1.25rem;
  font-size: clamp(1.0625rem, 1.5vw, 1.1875rem);
  line-height: 1.65;
}

.about__text p:last-child { margin-bottom: 0; }

.about__figure {
  margin: clamp(2.25rem, 5vw, 3.5rem) auto 0;
  width: 100%;
  max-width: 300px;
}

/* The caption introduces the photo rather than labelling it, so it stays level
   and typographic. The tilt belongs to the print underneath. */
.about__caption {
  margin: 0 0 1.125rem;
  font-size: .75rem;
  line-height: 1.45;
  color: var(--muted);
}

/* The headshot is a polaroid with its own white border baked in, so it gets no
   extra frame — just a tilt and a shadow to sit it on the paper.

   Four layers, not one: a tight contact shadow where the print meets the page,
   two mid layers, and a wide ambient. A single soft blur has no contact edge
   and the print reads as hovering rather than lying down. The colour is a warm
   brown pulled toward the paper's own hue — neutral black at low alpha turns
   cream grey and looks like dirt on it. */
.about__portrait {
  --tilt: -1.5deg;
  width: 100%;
  /* The file runs to 2352px, but the print's bottom edge is at 2308 — the last
     44px are scan background, a soft grey falloff below the paper. Cropped here
     rather than in the file: this is the only copy of the headshot, and a lossy
     re-encode to remove 1.9% would cost more than it saves. Re-crop by changing
     the 2308, or bake it into the asset and set this back to 1500 / 2352. */
  aspect-ratio: 1500 / 2308;
  object-fit: cover;
  object-position: top;
  transform: rotate(var(--tilt));
  /* Slow on the way back: it responds to you promptly and relaxes lazily. */
  transition: transform .7s var(--ease);
  box-shadow:
    0 1px 2px rgba(60, 46, 32, .18),
    0 6px 11px rgba(60, 46, 32, .12),
    0 18px 32px rgba(60, 46, 32, .11),
    0 40px 70px rgba(60, 46, 32, .12);
}

/* Hovering the print straightens it, as if you had nudged it square.

   The hover lives on the figure, not on the image: the image is the thing that
   rotates, and at this size straightening moves its own corners about 7px. A
   cursor sitting in that sliver would fall outside the moment it straightened,
   ending the hover, re-tilting it, and flickering forever. The figure is never
   transformed, so its hit area holds still. */
.about__figure:hover .about__portrait {
  transform: rotate(0deg);
  transition-duration: .35s;
}

/* --- contact ------------------------------------------------------------- */

.contact { margin-top: clamp(4.5rem, 10vw, 8rem); }

.contact__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .5rem clamp(1.5rem, 4vw, 3rem);
}

.contact__list a {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-size: 1.0625rem;
  text-decoration: none;
}

/* The rule sits on the label, not the link: carried by the whole link it would
   run under the icon too and read as a strikethrough on the glyph. */
.contact__label {
  border-bottom: 1px solid var(--hairline);
  padding-bottom: 2px;
  transition: border-color .25s var(--ease);
}

.contact__list a:hover .contact__label,
.contact__list a:focus-visible .contact__label { border-bottom-color: var(--ink); }

.contact__list a:hover .icon,
.contact__list a:focus-visible .icon { color: var(--ink); }

/* --- icons --------------------------------------------------------------- */

/* One set, drawn on a single 24 grid at one stroke weight, so five marks from
   five different sources still read as one hand. They inherit colour from the
   link, so hover carries the icon with the text. */
.icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.icon {
  flex: none;
  width: 1.1em;
  height: 1.1em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--muted);
  transition: color .25s var(--ease);
}

/* The one filled mark in the set — Instagram's lens dot reads as a hole at
   this size unless it is solid. */
.icon__dot {
  fill: currentColor;
  stroke: none;
}

/* --- footer -------------------------------------------------------------- */

.footer {
  max-width: var(--measure);
  margin: clamp(4.5rem, 10vw, 8rem) auto 0;
  padding: 1.75rem var(--gutter) 3rem;
  border-top: 1px solid var(--hairline);
}

.footer p {
  margin: 0;
  font-size: .6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- lightbox ------------------------------------------------------------ */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: var(--gutter);
  background: rgba(14, 11, 9, .94);
  opacity: 0;
  visibility: hidden;          /* also removes it from tab order when closed */
  /* visibility is deferred until the fade-out finishes, but flips instantly on
     open — otherwise the close button is still unfocusable when it gets focus. */
  transition: opacity .3s var(--ease), visibility 0s linear .3s;
}

.lightbox.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity .3s var(--ease), visibility 0s linear 0s;
}

.lightbox__frame {
  width: min(1100px, 100%);
  aspect-ratio: 16 / 9;
  background: #000;
}

.lightbox__frame iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

.lightbox__close {
  position: absolute;
  top: clamp(.75rem, 2vw, 1.5rem);
  right: clamp(.75rem, 2vw, 1.5rem);
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
  color: #f4f1ea;
  font-size: 2rem;
  line-height: 1;
  cursor: pointer;
  opacity: .7;
  transition: opacity .2s var(--ease);
}

.lightbox__close:hover,
.lightbox__close:focus-visible { opacity: 1; }

.lightbox__close:focus-visible {
  outline: 2px solid #f4f1ea;
  outline-offset: 2px;
}

/* --- motion preferences -------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: .01ms !important;
    animation-duration: .01ms !important;
  }
  .tile__watch:hover .tile__frame > img:first-child { transform: none; }

  /* Without a transition this would snap square, which is a jump, not a nudge. */
  .about__figure:hover .about__portrait { transform: rotate(var(--tilt)); }

  /* Only the motion is unwelcome, not the colour. The still stays; the video
     is hidden and script.js never fetches it either. */
  .backdrop__video { display: none; }
}
