/* ---------------------------------------------------------------------------
   Prince aka Lama — hero  black space, infinite grid.
   Type: Zapf Humanist 601 is Bitstream's cut of Hermann Zapf's Optima, so
   Optima (present on macOS) is the exact match; Marcellus is the web fallback.
--------------------------------------------------------------------------- */

:root {
  --black: #000000;
  --white: #ffffff;
  --gray-6: #c5c5c5;
  --gray-7: #8a8a8a;

  --font: "Optima", "Marcellus", "Palatino Linotype", Palatino, ui-serif, serif;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  --pad-x: 20px;
  --pad-y: 20px;
}

@media (min-width: 1024px) {
  :root { --pad-x: 40px; --pad-y: 40px; }
}

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

html, body {
  height: 100%;
  margin: 0;
  background: var(--black);
  color: var(--white);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow: hidden;
  overscroll-behavior: none;
}

::selection { background: var(--white); color: #232323; }

a { color: inherit; text-decoration: none; }

/* --- header ------------------------------------------------------------- */

/* Centred lockup: the name over its role, both widely tracked. Kept as a
   column so the two lines stay optically stacked at any width. */
.app-header {
  position: fixed;
  inset: var(--pad-y) var(--pad-x) auto;
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7em;
  text-align: center;
  pointer-events: none;
  mix-blend-mode: difference;
}

.brand {
  pointer-events: auto;
  display: block;
  text-transform: uppercase;
}

/* Letter-spacing is applied after every glyph including the last, which pushes
   a centred line visually left by one space. The matching text-indent puts it
   back on the axis. */
.brand__name {
  display: block;
  font-size: clamp(19px, 3.2vw, 34px);
  line-height: 1;
  letter-spacing: 0.34em;
  text-indent: 0.34em;
  color: var(--white);
}

.brand__role {
  margin: 0;
  font-size: clamp(9px, 1.05vw, 12px);
  line-height: 1;
  letter-spacing: 0.4em;
  text-indent: 0.4em;
  text-transform: uppercase;
  color: var(--gray-7);
}

/* --- lockup ------------------------------------------------------------- */

/* The header is normally held back until the grid is revealed. While the
   opening plays, the lockup assembles over it instead — so it is shown from
   the first frame. Declared here, before the lightbox's own rule of equal
   specificity, so opening the lightbox still hides the header. */
body.is-lockup .app-header {
  opacity: 1;
  transition: opacity 0.9s var(--ease);
}

/* Each letter is pinned to the width of its final glyph, so scrambled
   substitutes cannot shift a centred line. */
.lockup__ch {
  display: inline-block;
  text-align: center;
  white-space: pre;
}

/* Scrambling letters sit back; a letter brightens as it locks. */
.lockup__ch:not(.is-set) { color: var(--gray-7); }
.lockup__ch.is-set { color: inherit; transition: color 0.5s var(--ease); }

body.is-lockup .brand__role {
  opacity: 0;
  transition: opacity 1.1s var(--ease);
}

body.is-lockup .brand__role.is-in { opacity: 1; }

/* --- intro -------------------------------------------------------------- */

.intro {
  position: fixed;
  inset: 0;
  z-index: 20;
  pointer-events: none;
  background: var(--black);
  transition: opacity 0.9s var(--ease);
}

.intro__canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The field is already drawing the same tiles underneath by the time this
   runs. The fade covers the last of the difference: the opening flattens onto
   one plane, the field's are out at their own depths, so the two disagree by a
   pixel or two at the outer edges of frame. */
.intro.is-gone {
  opacity: 0;
  visibility: hidden;
}

/* --- stage / the field --------------------------------------------------- */

/* Nothing here is dragged with a mouse — moving the cursor leans the camera
   and the wheel travels — so the pointer stays a pointer. Touch is the one
   place a finger takes hold of the room, and it gets the grab cursor with it. */
.stage {
  position: fixed;
  inset: 0;
  z-index: 10;
  overflow: hidden;
  cursor: default;
  touch-action: none;
  opacity: 0;
}

/* No transition: the opening hands over on a frame where both canvases draw
   the same geometry, so the wall must appear at full strength immediately.
   Fading it in would leave a hole in the middle of the handover. */
body.is-revealed .stage {
  opacity: 1;
}

.app-header,
.app-menu,
.sound-toggle,
.hint {
  opacity: 0;
  transition: opacity 0.9s var(--ease) 0.35s;
}

body.is-revealed .app-header,
body.is-revealed .app-menu,
body.is-revealed .sound-toggle,
body.is-revealed .hint {
  opacity: 1;
}

body.is-revealed .hint.is-hidden { opacity: 0; }

.stage.is-dragging { cursor: grabbing; }

/* The field is a single WebGL canvas — see js/wall.js. Nothing inside it is a
   DOM node, so hover state and captions are surfaced by the readout below. */
.wall__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

body.is-pointing .stage { cursor: pointer; }
body.is-pointing .stage.is-dragging { cursor: grabbing; }

/* The hovered work, named. Sits with the hint, above the vignette. */
.wall-readout {
  position: fixed;
  left: 28px;
  bottom: 26px;
  transform: translateY(6px);
  z-index: 32;
  margin: 0;
  pointer-events: none;
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.86);
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.9);
  opacity: 0;
  transition: opacity 0.28s var(--ease), transform 0.28s var(--ease);
  white-space: nowrap;
}

body.is-revealed .wall-readout.is-on {
  opacity: 1;
  transform: translateY(0);
}

body.is-lightbox-open .wall-readout { opacity: 0; }

/* On a phone there is no cursor to name what it is over, and the menu already
   owns that corner. */
@media (max-width: 700px) {
  .wall-readout { display: none; }
}

/* Where hover videos are parked while their frames are drawn on the wall.
   Not display:none — that stops playback in some browsers. */
.wall-sink {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  z-index: -1;
}

/* --- no-WebGL fallback ---------------------------------------------------
   Same items, plain grid. See startFallback() in js/wall.js. */
.wall-fallback {
  position: absolute;
  inset: 0;
  overflow: auto;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 14px;
  padding: 96px 24px 120px;
  -webkit-overflow-scrolling: touch;
}

.wall-fallback__cell {
  position: relative;
  display: block;
  padding: 0;
  border: 0;
  background: #0a0a0a;
  cursor: pointer;
  aspect-ratio: 4 / 5;
  overflow: hidden;
}

.wall-fallback__cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.wall-fallback__label {
  position: absolute;
  left: 10px;
  bottom: 10px;
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--white, #fff);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
}

body.is-fallback .stage { cursor: default; }
body.is-fallback .hint { display: none; }

/* Radial falloff so the field reads as depth rather than wallpaper, plus
   top/bottom scrims that keep the header and menu legible over bright media.
   It sits above both the opening and the grid, so it does not appear out of
   nowhere at the handover. */
.vignette {
  position: fixed;
  inset: 0;
  z-index: 30;
  pointer-events: none;
  background:
    linear-gradient(to bottom, rgba(0, 0, 0, 0.7) 0, rgba(0, 0, 0, 0) 140px),
    linear-gradient(to top, rgba(0, 0, 0, 0.7) 0, rgba(0, 0, 0, 0) 160px),
    radial-gradient(
    ellipse 75% 75% at 50% 50%,
    rgba(0, 0, 0, 0) 42%,
    rgba(0, 0, 0, 0.28) 74%,
    rgba(0, 0, 0, 0.72) 100%
  );
}

.tile {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  overflow: hidden;
  background: #0a0a0a;
  /* Overlapping tiles need a seam, or neighbours merge into one silhouette. */
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.85), 0 18px 50px rgba(0, 0, 0, 0.55);
  will-change: transform, opacity;
  transform-origin: 50% 50%;
  -webkit-user-select: none;
  user-select: none;
}

.tile__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transform: scale(1.04);
  transition: opacity 0.9s var(--ease), transform 1.2s var(--ease),
              filter 0.6s var(--ease);
  filter: grayscale(1) brightness(0.82);
  pointer-events: none;
  -webkit-user-drag: none;
}

.tile__media.is-loaded { opacity: 1; transform: scale(1); }

/* Video layer sits over the poster and fades in once it can play. */
.tile__video { z-index: 2; opacity: 0; filter: grayscale(0) brightness(1); }
.tile__video.is-playing { opacity: 1; }

.tile:hover .tile__media { filter: grayscale(0) brightness(1); }

.tile__label {
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 3;
  width: 100%;
  padding: 0.9em 1em;
  display: flex;
  align-items: baseline;
  /* One child now — the brand — and it stays in the corner it has always
     occupied. space-between would flush it left. */
  justify-content: flex-end;
  gap: 1em;
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  opacity: 0;
  transform: translateY(0.4em);
  transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
  background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
  pointer-events: none;
}

.tile:hover .tile__label { opacity: 1; transform: translateY(0); }

.tile__caption { color: var(--white); font-size: 10px; }

.tile__badge {
  position: absolute;
  top: 0.9em;
  right: 1em;
  z-index: 3;
  font-size: 9px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--gray-6);
  opacity: 0.7;
}

/* Suppress hover affordances while the user is actively dragging. */
.stage.is-dragging .tile__label,
.stage.is-dragging .tile:hover .tile__media { pointer-events: none; }
.stage.is-dragging .tile:hover .tile__media { filter: grayscale(1) brightness(0.82); }

/* --- sound gate --------------------------------------------------------- */

/* The way in, staged like a title card.

   Nothing loads past this point until the visitor answers, so this is the whole
   frame for a few seconds and it is built to be looked at. The line resolves
   OUT of the black rather than arriving from somewhere: tracking contracts,
   focus pulls in, a pool of light comes up under it. Then a hairline draws, the
   instruction settles, and the whole plate keeps drifting in a slow push that
   never quite arrives — a held shot rather than a finished animation. */
.gate {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--black);
  cursor: pointer;
  transition: opacity 0.9s var(--ease), visibility 0.9s var(--ease);
}

.gate.is-gone {
  opacity: 0;
  visibility: hidden;
}

/* A spill of light the type sits inside. Barely there — on a black screen a
   few percent of white is the difference between a void and a room. */
.gate__glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    72% 56% at 50% 47%,
    rgba(255, 255, 255, 0.055),
    rgba(255, 255, 255, 0.022) 38%,
    rgba(255, 255, 255, 0) 78%
  );
  animation: gate-glow 3s var(--ease) both;
}

/* A gradient this faint crosses only a handful of 8-bit levels, and the steps
   between them show up as concentric rings. A film grain of noise over the top
   breaks the banding the way dithering always has — and on a cinematographer's
   site, grain is hardly out of place. */
.gate__glow::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Normal blending at a whisper: enough variation to break the rings, far
     too little to lift the black. An overlay blend here greys the whole plate. */
  opacity: 0.028;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

.gate__prompt {
  margin: 0;
  padding: 0 var(--pad-x);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.5em;
  text-align: center;
  text-transform: uppercase;
  pointer-events: none;
  /* The push-in. 20s, alternating, never resolving. */
  animation: gate-breathe 20s ease-in-out 2.8s infinite alternate;
}

/* Tracking is added after the last glyph too, so a centred line needs a
   matching indent to sit on its axis — animated alongside the tracking so it
   stays centred the whole way in. */
.gate__lead {
  font-size: clamp(17px, 2.6vw, 30px);
  line-height: 1;
  letter-spacing: 0.34em;
  text-indent: 0.34em;
  color: var(--white);
  animation: gate-title 2.1s cubic-bezier(0.16, 1, 0.3, 1) 0.3s both;
}

/* Drawn from the centre out, like a light coming up on a stage. */
.gate__rule {
  display: block;
  width: min(34vw, 300px);
  height: 1px;
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0),
    rgba(255, 255, 255, 0.55),
    rgba(255, 255, 255, 0)
  );
  animation: gate-rule 1.4s var(--ease) 1.7s both;
}

.gate__sub {
  font-size: clamp(10px, 1.05vw, 12px);
  line-height: 1;
  letter-spacing: 0.4em;
  text-indent: 0.4em;
  color: var(--gray-7);
  animation: gate-settle 1.3s var(--ease) 2.15s both;
}

.gate__silent {
  position: absolute;
  left: 50%;
  bottom: calc(var(--pad-y) * 1.6);
  transform: translateX(-50%);
  padding: 0.6em 0.2em;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: clamp(10px, 1.05vw, 12px);
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: uppercase;
  color: var(--gray-7);
  text-decoration: underline;
  text-underline-offset: 0.55em;
  text-decoration-thickness: 1px;
  cursor: pointer;
  white-space: nowrap;
  /* Its own keyframes: the shared one would drop the -50% that centres it. */
  animation: gate-settle-centred 1.3s var(--ease) 2.5s both;
  transition: color 0.4s var(--ease);
}

.gate__silent:hover,
.gate__silent:focus-visible { color: var(--white); }

@keyframes gate-glow {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The signature move: the line comes together out of nothing. */
@keyframes gate-title {
  from {
    opacity: 0;
    letter-spacing: 0.95em;
    text-indent: 0.95em;
    filter: blur(13px);
    transform: scale(1.05);
  }
  55% { opacity: 1; }
  to {
    opacity: 1;
    letter-spacing: 0.34em;
    text-indent: 0.34em;
    filter: blur(0);
    transform: scale(1);
  }
}

@keyframes gate-rule {
  from { opacity: 0; transform: scaleX(0); }
  to   { opacity: 1; transform: scaleX(1); }
}

@keyframes gate-settle {
  from { opacity: 0; transform: translateY(0.5em); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes gate-settle-centred {
  from { opacity: 0; transform: translate(-50%, 0.5em); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

@keyframes gate-breathe {
  from { transform: scale(1); }
  to   { transform: scale(1.028); }
}

@media (prefers-reduced-motion: reduce) {
  .gate { transition-duration: 0.01ms; }
  .gate__glow,
  .gate__prompt,
  .gate__lead,
  .gate__rule,
  .gate__sub,
  .gate__silent { animation: none; }
}

/* --- hint --------------------------------------------------------------- */

.hint {
  position: fixed;
  left: 50%;
  top: 50%;
  z-index: 40;
  margin: 0;
  transform: translate(-50%, -50%);
  font-size: 11px;
  letter-spacing: 0.35em;
  text-transform: uppercase;
  color: var(--gray-6);
  pointer-events: none;
  transition: opacity 0.8s var(--ease);
}

.hint.is-hidden { opacity: 0; }

@media (max-width: 640px) {
  .hint { font-size: 10px; letter-spacing: 0.22em; white-space: nowrap; }
}

/* --- sound toggle ------------------------------------------------------- */

.sound-toggle {
  position: fixed;
  right: var(--pad-x);
  bottom: var(--pad-y);
  z-index: 55;
  display: inline-flex;
  align-items: center;
  gap: 0.8em;
  padding: 0;
  border: 0;
  background: none;
  color: var(--gray-6);
  font-family: inherit;
  font-size: 11px;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.4s var(--ease);
}

.sound-toggle:hover { color: var(--white); }

.sound-toggle__bars {
  display: inline-flex;
  align-items: flex-end;
  gap: 2px;
  height: 12px;
}

.sound-toggle__bars i {
  display: block;
  width: 2px;
  height: 3px;
  background: currentColor;
  transform-origin: bottom;
}

/* Bars only dance while sound is actually on and something can be heard. */
.sound-toggle.is-on .sound-toggle__bars i {
  animation: bar 1s ease-in-out infinite;
}

.sound-toggle.is-on .sound-toggle__bars i:nth-child(2) { animation-delay: 0.15s; }
.sound-toggle.is-on .sound-toggle__bars i:nth-child(3) { animation-delay: 0.3s; }
.sound-toggle.is-on .sound-toggle__bars i:nth-child(4) { animation-delay: 0.45s; }

@keyframes bar {
  0%, 100% { height: 3px; }
  50% { height: 12px; }
}

/* Waiting on a first click before the browser will allow sound. */
.sound-toggle.is-pending { color: var(--white); }
.sound-toggle.is-pending .sound-toggle__bars i { height: 3px; animation: none; }

@media (prefers-reduced-motion: reduce) {
  .sound-toggle.is-on .sound-toggle__bars i { animation: none; height: 7px; }
}

@media (max-width: 640px) {
  .sound-toggle {
    bottom: auto;
    top: var(--pad-y);
    right: var(--pad-x);
    font-size: 10px;
    letter-spacing: 0.2em;
  }
  .sound-toggle__label { display: none; }
}

/* --- bottom menu -------------------------------------------------------- */

.app-menu {
  position: fixed;
  left: 50%;
  bottom: calc(var(--pad-y) * 0.6);
  z-index: 50;
  transform: translateX(-50%);
  text-align: center;
  text-transform: uppercase;
  mix-blend-mode: difference;
}

.app-menu__line + .app-menu__line { margin-top: 0.5em; }

.app-menu .word,
.app-menu .link {
  display: inline;
  margin: 0 0.35em;
  color: rgba(255, 255, 255, 0.6);
  transition: color 0.5s var(--ease);
}

.app-menu .word { font-size: 13px; }
.app-menu .link { font-size: 22px; }
.app-menu .link:hover { color: rgba(255, 255, 255, 0.9); }

/* Underline that wipes in from the centre — matches the reference's .link. */
.link { position: relative; }

.link::after {
  content: "";
  position: absolute;
  left: 0;
  top: 100%;
  width: calc(100% - 0.3em);
  height: 1.5px;
  background-color: currentColor;
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.4s var(--ease);
}

.link:hover::after { transform: scaleX(1); }

@media (max-width: 640px) {
  .app-menu .link { font-size: 18px; }
  .app-menu .word { font-size: 11px; }
}

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

/* The header and menu use mix-blend-mode, which composites against the page
   regardless of z-index — so they must be hidden outright, not stacked under. */
body.is-lightbox-open .app-header,
body.is-lightbox-open .app-menu,
body.is-lightbox-open .sound-toggle,
body.is-lightbox-open .hint {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s var(--ease);
}

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(var(--pad-y) * 2) var(--pad-x);
  background: rgba(0, 0, 0, 0.94);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.6s var(--ease), visibility 0.6s var(--ease);
}

.lightbox.is-open { opacity: 1; visibility: visible; }

/* The backdrop leads on the way in and lingers on the way out, so the picture
   is never travelling against a transparent page. */
.lightbox { transition: opacity 0.45s var(--ease), visibility 0.45s var(--ease); }

.lightbox__stage {
  position: relative;
  max-width: min(1200px, 100%);
  max-height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Scaled from the tile's rectangle on open and back to it on close — see
     zoom() in grid.js. Transform-origin must stay centred: the inverse is
     computed from centre deltas. */
  transform-origin: 50% 50%;
  transition: transform 0.58s var(--ease);
  will-change: transform;
}

/* The master fades in over the still-playing hover loop once it can play, so
   the swap to full resolution is not a visible cut. */
.lightbox__upgrade {
  position: absolute;
  inset: 0;
  margin: auto;
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}

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

.lightbox__stage img,
.lightbox__stage video {
  max-width: 100%;
  max-height: 78vh;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
}

.lightbox__close {
  position: absolute;
  top: var(--pad-y);
  right: var(--pad-x);
  z-index: 2;
  padding: 0.5em 0;
  border: 0;
  background: none;
  color: var(--gray-6);
  font-family: inherit;
  font-size: 12px;
  letter-spacing: 0.35em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.4s var(--ease);
}

.lightbox__close:hover { color: var(--white); }

.lightbox__meta {
  position: absolute;
  left: var(--pad-x);
  bottom: var(--pad-y);
  text-transform: uppercase;
}

.lightbox__title {
  margin: 0;
  font-size: 14px;
  letter-spacing: 0.28em;
  color: var(--white);
}

.lightbox__caption {
  margin: 0.5em 0 0;
  font-size: 11px;
  letter-spacing: 0.28em;
  color: var(--gray-7);
}

@media (prefers-reduced-motion: reduce) {
  .stage { transition-duration: 0.01ms; }
  .tile__media,
  .tile__label,
  .lockup__ch,
  .hint,
  .lightbox,
  .lightbox__stage,
  .lightbox__upgrade,
  .link::after { transition-duration: 0.01ms; }
}
