/* The new visual world, shared by every page.
 *
 * Paper and near-black, Azeret Mono for the name, menu and labels, Spectral
 * for running text. Type blends by difference so it resolves to near-black on
 * paper and flips to white wherever ink or a photograph runs beneath it.
 */

:root{
  --paper:#eeeeec;
  --ink:#111110;
  --rule:#d6d6d2;
  --ease:cubic-bezier(.22,.61,.36,1);
  --display:'Azeret Mono',ui-monospace,monospace;
  --text:'Spectral',Georgia,'Times New Roman',serif;
}
*{box-sizing:border-box;margin:0;padding:0}
body{
  background:var(--paper); color:var(--ink);
  font-family:var(--text); font-weight:400;
  -webkit-font-smoothing:antialiased; overflow-x:hidden;
}
a{color:inherit;text-decoration:none}
img{max-width:100%}

#ink{position:fixed; inset:0; width:100%; height:100%; display:block}

/* Difference blending only ever wraps type. A container that also holds
   photographs inverts every one of them. */
.blend{color:#fff; mix-blend-mode:difference; position:relative; z-index:6}

/* No z-index on the wrapper: `position:relative; z-index:n` makes a stacking
   context, and an element only blends with the backdrop inside its own
   context — which leaves the type unable to see the ink at all. */
.wrap{position:relative; max-width:1500px; margin:0 auto; padding:0 4vw}

/* ----------------------------------------------------------------- menu -- */
.top{
  position:fixed; top:0; left:0; right:0; z-index:8;
  display:flex; justify-content:space-between; align-items:center;
  gap:2rem; padding:1.9rem 4vw;
  font-family:var(--display); font-size:.7rem; letter-spacing:.2em;
}
.top .mark{font-weight:600; letter-spacing:.24em}
.top nav{display:flex; align-items:center; gap:1.7rem; flex-wrap:wrap; justify-content:flex-end}
.top nav a{position:relative; padding-bottom:.25rem; opacity:.62; transition:opacity .35s var(--ease)}
.top nav a.is{opacity:1}
.top nav a::after{
  content:''; position:absolute; left:0; bottom:0; height:1px; width:100%;
  background:currentColor; transform:scaleX(0); transform-origin:left;
  transition:transform .45s var(--ease);
}
.top nav a.is::after{transform:scaleX(1)}
.top nav a:hover{opacity:1}
.top nav a:hover::after,.top nav a:focus-visible::after{transform:scaleX(1)}
.ig{display:flex; padding-bottom:0}
.ig::after{display:none}
.ig svg{width:1.05rem; height:1.05rem; display:block}

/* -------------------------------------------------------------- headings -- */
.poster{
  font-family:var(--display); font-weight:700;
  font-size:clamp(2.4rem,8.4vw,7.5rem);
  letter-spacing:-.055em; line-height:.82; text-transform:uppercase;
}
/* The Estonian half of a bilingual title, dropped back a tone so the two names
   read as one heading rather than two. Opacity, not a grey: this type is white
   under mix-blend-mode:difference, and a fixed colour would resolve wrong the
   moment a photograph runs beneath it. Same trick as the menu's .62. */
.alt{opacity:.45}

/* Estonian brings Ä, Ö and Ü to a heading, and at .82 the umlauts of one line
   land inside the line above it — "MILLEST ON TEHTUD / VÄIKESED TÜDRUKUD" grew
   two stray dots. Only bilingual headings get the extra leading; the English
   ones have nothing above the cap height and keep the tighter setting. */
.poster:has(.alt){line-height:1.06}

.head{padding:26vh 0 6vh}
.meta{
  margin-top:2rem; display:flex; flex-wrap:wrap; gap:.6rem 2.2rem;
  font-family:var(--display); font-size:.68rem; letter-spacing:.16em;
}
.meta a{border-bottom:1px solid currentColor; padding-bottom:2px}
.label{
  margin:12vh 0 2rem; font-family:var(--display); font-size:.66rem;
  font-weight:400; letter-spacing:.22em;
  border-top:1px solid var(--rule); padding-top:1.2rem;
}
.lede-text{margin-top:2rem; max-width:44ch; font-size:clamp(.95rem,1.1vw,1.12rem); line-height:1.6}

.hero{position:relative; margin:2vh 0 10vh}
.hero img{width:100%; height:min(74svh,860px); object-fit:cover; display:block}

.prose{max-width:60ch; font-size:clamp(1rem,1.15vw,1.15rem); line-height:1.72}
.prose p + p{margin-top:1.4rem}

/* A lead-in for a passage of running text: the same small mono label the
   credits use, set above the paragraph it introduces instead of beside it.
   Some pages carry these as headings rather than as credits, and they were
   sitting in the credits list with an empty label column and no value. */
.prose h2{
  font-family:var(--display); font-weight:400; font-size:.62rem;
  letter-spacing:.15em; text-transform:uppercase; opacity:.66;
  margin-top:2.8rem;
}
.prose h2:first-child{margin-top:0}
.prose h2 + p{margin-top:.7rem}

/* --------------------------------------------------------------- credits -- */
.credits{
  margin-top:9vh; border-top:1px solid var(--rule); padding-top:2.5rem;
  display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:1.1rem 3rem;
}
.credit{display:grid; grid-template-columns:11rem minmax(0,1fr); gap:1rem; align-items:baseline}
.credit dt{font-family:var(--display); font-size:.62rem; letter-spacing:.15em; opacity:.66}
.credit dd{font-size:1rem; line-height:1.45}

/* -------------------------------------------------------------------- cv -- */
/* A CV is a list of labels and content, which is what this site already is:
   the year and the role stay in the mono the labels everywhere else use, the
   title and the venue in the serif that carries every other piece of text.
   Nothing new is invented, the existing two voices are just set into rows. */
.cv{margin-top:.5rem}
/* Two columns, the same label-then-value shape the credits use: the year is
   the label, everything the entry says is the value. The role used to sit in
   a third column pinned to the right, which left a hand's width of nothing
   between a short title and it. It reads on the title's line now, as it does
   on the printed CV. */
.cv-row{
  display:grid; grid-template-columns:4.4rem minmax(0,1fr);
  gap:.3rem 1.5rem; align-items:baseline;
  padding:1.05rem 0; border-top:1px solid var(--rule);
}
.cv-row:last-child{border-bottom:1px solid var(--rule)}
.cv-row .y{
  font-family:var(--display); font-size:.66rem; letter-spacing:.1em;
  font-variant-numeric:tabular-nums; opacity:.72;
}
.cv-row .t{font-size:1.05rem; line-height:1.35}
/* A title that has a page of its own says so: the underline is the same
   inline-link mark the meta line uses, held back until it is hovered. */
.cv-row .t a{
  text-decoration:underline; text-decoration-thickness:1px;
  text-underline-offset:.22em;
  text-decoration-color:color-mix(in srgb,currentColor 38%,transparent);
  transition:text-decoration-color .35s var(--ease);
}
.cv-row .t a:hover,.cv-row .t a:focus-visible{text-decoration-color:currentColor}
.cv-row .r{
  font-family:var(--display); font-size:.6rem; letter-spacing:.14em;
  text-transform:uppercase; opacity:.66; white-space:nowrap;
}
.cv-row .r::before{content:'·'; margin:0 .55em; letter-spacing:0; opacity:.5}
.cv-row .m{grid-column:2; font-size:.92rem; line-height:1.5; opacity:.74}

/* The shorter lists — schooling, prizes, languages — are too small to carry a
   row each, so they sit side by side. Columns, not a grid, for the same reason
   the gallery uses them: a grid row waits for its tallest cell, and the six
   prizes left a hole under every list beside them. */
.cv-facts{margin-top:3rem; columns:4; column-gap:3rem}
@media (max-width:1150px){ .cv-facts{columns:3} }
@media (max-width:900px){ .cv-facts{columns:2} }
.fact{break-inside:avoid; margin-bottom:3.2rem}
.fact h3{
  font-family:var(--display); font-weight:400; font-size:.62rem;
  letter-spacing:.18em; opacity:.66;
  border-top:1px solid var(--rule); padding-top:1.1rem; margin-bottom:1.3rem;
}
.fact ul{list-style:none}
.fact li{
  display:grid; grid-template-columns:minmax(0,1fr) auto; gap:.15rem 1rem;
  font-size:.98rem; line-height:1.45; break-inside:avoid;
}
.fact li + li{margin-top:1.15rem}
.fact .n{grid-column:1; font-size:.88rem; line-height:1.42; opacity:.7}
.fact .yr{
  font-family:var(--display); font-size:.61rem; letter-spacing:.1em;
  font-variant-numeric:tabular-nums; opacity:.62; padding-top:.28rem;
}
.fact p{font-size:.98rem; line-height:1.6}

/* ----------------------------------------------------------------- video -- */
.video{
  position:relative; width:100%; border:0; padding:0; background:none;
  cursor:pointer; display:block; color:inherit; font:inherit;
}
.video img{width:100%; height:min(64svh,720px); object-fit:cover; display:block}
.video:hover img{filter:brightness(.88)}
.video .play{position:absolute; inset:0; display:grid; place-items:center}
.video .play span{
  font-family:var(--display); font-size:.68rem; letter-spacing:.24em; color:#fff;
  padding:1rem 1.7rem; border:1px solid rgba(255,255,255,.75);
  transition:background .45s var(--ease), color .45s var(--ease);
}
.video:hover .play span,.video:focus-visible .play span{background:#fff; color:#111}
.video:focus-visible{outline:2px solid var(--ink); outline-offset:4px}

/* --------------------------------------------------------------- gallery -- */
/* Columns, not a grid: a grid cell has to crop to one common shape, and that
   cropped every portrait to a landscape strip. */
.grid{columns:3; column-gap:.9rem}
@media (min-width:1200px){ .grid{columns:4} }
@media (max-width:700px){ .grid{columns:2} }
.shot{
  border:0; padding:0; background:none; cursor:pointer; display:block;
  width:100%; margin:0 0 .9rem; overflow:hidden; break-inside:avoid;
}
.shot img{width:100%; height:auto; display:block; transition:transform 1.1s var(--ease), filter .5s var(--ease)}
.shot:hover img,.shot:focus-visible img{transform:scale(1.04); filter:brightness(1.05) saturate(1.06)}
.shot:focus-visible{outline:2px solid var(--ink); outline-offset:3px}

/* ----------------------------------------------------------------- press -- */
.press a{
  display:block; padding:1.3rem 0; border-top:1px solid var(--rule);
  font-size:1rem; line-height:1.5; transition:transform .5s var(--ease);
}
.press a:hover{transform:translateX(.9rem)}
.press a:last-child{border-bottom:1px solid var(--rule)}

.back{display:inline-block; margin:10vh 0 8vh; font-family:var(--display);
      font-size:.66rem; letter-spacing:.2em; opacity:.62}
.back:hover{opacity:1}

/* ------------------------------------------------------------ index list -- */
.list{position:relative; padding:0 4vw 20vh; max-width:1500px; margin:0 auto}
.work{
  display:grid; grid-template-columns:3.4rem minmax(0,1fr) auto;
  gap:1.6rem; align-items:start;
  padding:2rem 0; border-top:1px solid var(--rule);
  /* transform belongs to the scroll animation now; the hover rides translate */
  transition:translate .55s var(--ease), opacity .4s var(--ease);
}
.work:last-child{border-bottom:1px solid var(--rule)}
.work:hover{translate:1.2rem 0}
.list.hovering .work{opacity:.28}
.list.hovering .work:hover{opacity:1}
.work .n{font-family:var(--display); font-size:.66rem; letter-spacing:.14em;
         font-variant-numeric:tabular-nums; padding-top:.55rem}
.work .t{font-family:var(--display); font-weight:600;
         font-size:clamp(1.15rem,2.9vw,2.5rem); letter-spacing:-.04em;
         line-height:1.06; text-transform:uppercase; display:block}
.work .d{display:block; margin-top:.85rem; max-width:52ch;
         font-size:clamp(.9rem,1.02vw,1.05rem); line-height:1.55}
.work .v{display:block; margin-top:.7rem; font-family:var(--display);
         font-size:.62rem; letter-spacing:.16em; opacity:.7}
.work .y{font-family:var(--display); font-size:.8rem;
         font-variant-numeric:tabular-nums; padding-top:.45rem}
.work:focus-visible{outline:2px solid currentColor; outline-offset:6px}

/* the CSS fallback for the pointer image; the renderer hides it */
.peek{
  position:fixed; top:0; left:0; z-index:3; pointer-events:none;
  width:min(24vw,360px); aspect-ratio:4/3; overflow:hidden;
  opacity:0; transition:opacity .35s var(--ease);
}
.peek.on{opacity:1}
.peek img{width:100%;height:100%;object-fit:cover;display:block}
html.ink-on .peek{display:none}

/* -------------------------------------------------------------- overlays -- */
/* <dialog> supplies the top layer, the focus trap and Escape with no library —
   which matters, given a version-mismatched modal plugin is what broke this
   site's lightbox in the first place. */
dialog{border:0; padding:0; background:transparent; width:100%; height:100%;
       max-width:100vw; max-height:100vh; overflow:hidden; color:#fff}
dialog::backdrop{background:rgba(8,8,8,.94)}
/* The sheet reserves sideways gestures for paging between photographs; up and
   down stay with the browser. Without this the swipe fights the platform's own
   horizontal gesture and neither wins cleanly. */
.sheet{position:fixed; inset:0; display:grid; place-items:center; padding:6svh 7vw; touch-action:pan-y}
.sheet figure{display:grid; place-items:center; gap:1rem; max-width:100%}
.sheet img{max-width:100%; max-height:80svh; width:auto; height:auto; object-fit:contain; display:block}
.sheet figcaption{font-family:var(--display); font-size:.62rem; letter-spacing:.2em; opacity:.66}
.ctl{position:fixed; border:0; background:none; color:#fff; cursor:pointer;
     font-family:var(--display); font-size:.74rem; letter-spacing:.2em; padding:1rem}
.ctl:hover{opacity:.6}
.ctl:focus-visible{outline:1px solid #fff; outline-offset:2px}
.x{top:1.4rem; right:2vw}
.prev{left:1vw; top:50%; transform:translateY(-50%)}
.next{right:1vw; top:50%; transform:translateY(-50%)}
.frame{width:min(92vw,1400px); aspect-ratio:16/9; border:0; display:block}

/* Rows drift as they cross the screen. `transform` is animated and `translate`
   carries the hover, so the two compose rather than one cancelling the other. */
@media (prefers-reduced-motion: no-preference){
  @supports (animation-timeline: view()){
    .work{
      animation:row-drift linear both;
      animation-timeline:view();
      animation-range:entry 0% exit 100%;
    }
    @keyframes row-drift{
      from{transform:translateX(-1.1rem); opacity:.55}
      40% {transform:translateX(0);       opacity:1}
      to  {transform:translateX(1.6rem);  opacity:.75}
    }
  }
}

/* ---------------------------------------------------------------- motion -- */
@keyframes bloom{ from{filter:grayscale(1) contrast(1.04)} to{filter:none} }
@media (prefers-reduced-motion: no-preference){
  /* Above the fold, a scroll-driven entry animation is finished before anyone
     can watch it, so the hero blooms on load instead. */
  .hero img{animation:bloom 1.9s cubic-bezier(.22,.61,.36,1) .15s both}
  @supports (animation-timeline: view()){
    .video img{animation:bloom linear both; animation-timeline:view();
               animation-range:entry 10% cover 28%}
  }
}

@media (max-width:820px){
  /* The menu wrapped into three ragged lines. Tighter tracking and gaps let
     the five destinations and the icon settle into two even rows that read as
     a decision rather than an accident. */
  .top{position:static; flex-direction:column; gap:.75rem; align-items:flex-start}
  .top .mark{font-size:.66rem}
  .top nav{justify-content:flex-start; gap:.55rem .95rem; font-size:.6rem; letter-spacing:.12em}
  .top nav a{padding-bottom:.2rem}
  .ig svg{width:.95rem; height:.95rem}
  .head{padding-top:4vh}
  .credit{grid-template-columns:1fr; gap:.2rem}
  /* the year keeps its rail, narrower; the entry stacks beside it */
  .cv-row{grid-template-columns:2.9rem minmax(0,1fr); gap:.3rem 1rem}
  /* On a phone there is no room to run the role after the title: it wrapped
     onto its own line anyway, carrying the separating dot to the start of it,
     and it pushed short titles into breaking mid-word. Given its own line it
     needs no separator. */
  .cv-row .r{display:block; white-space:normal; margin-top:.4rem}
  .cv-row .r::before{content:none}
  .cv-facts{columns:1}
  .grid{columns:2; column-gap:.5rem}
  .shot{margin-bottom:.5rem}
  .work{grid-template-columns:2.4rem minmax(0,1fr); gap:1rem}
  .work .y{display:none}
  .peek{display:none}
  .list.hovering .work{opacity:1}
  .prev,.next{top:auto; bottom:1rem; transform:none}
}
@media (prefers-reduced-motion:reduce){ *{transition:none !important} }
