/* ---------------------------------------------------------------------------
   Nightvision Artists — V1
   Concept: the brand name taken literally. An image-intensifier picture —
   phosphor green on near-black — seen through a viewfinder. One hot-red
   accent (the REC light, the targeting dot), everything else green-white.
--------------------------------------------------------------------------- */

@font-face {
  font-family: 'Google Sans';
  src: url('fonts/GoogleSans-subset.woff') format('woff2-variations');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --bg: #040605;
  --ink: #f3f7f1; /* paper, barely tinted */
  --phos: #b8d8bd; /* the bright tone of the field: sage, not neon */
  --mute: rgba(206, 226, 208, 0.78);
  --rule: rgba(184, 216, 189, 0.5);
  --red: #ff3b30; /* the only accent */

  --pad: clamp(1.4rem, 3vw, 3rem); /* sits just inside the viewfinder frame */

  /* Entrance choreography. Desktop gets the slow, staged reveal; phones
     compress it (see the touch/narrow block below) so words are never kept
     waiting behind it. Each element's own delay (--d) is scaled by --stagger. */
  --stagger: 1;
  --rise-dur: 0.9s;
  --wipe-dur: 1.1s;
  --frame-delay: 1.2s;
  --ease: cubic-bezier(0.2, 0.7, 0, 1);
}

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

html {
  background: var(--bg);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  /* Clicking fast double/triple-clicks the page text; a selection then turns the next
     click into a text drag and the map stops responding. Nothing here needs selecting
     except the links below. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  /* quick repeated taps on a phone shouldn't trigger the browser's double-tap zoom
     (pinch-zoom still works) */
  touch-action: manipulation;
  min-height: 100svh;
  overflow-x: hidden;
  background: var(--bg);
  color: var(--ink);
  font-family: 'Google Sans', 'Helvetica Neue', sans-serif;
  font-size: 1rem;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

::selection {
  background: var(--phos);
  color: var(--bg);
}

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

.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --------------------------------------------------------------- the picture */

#field {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 0;
  background: var(--bg);
  /* rendered at dither-cell resolution, so upscale without smoothing */
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

/* no WebGL2: a still, soft phosphor wash rather than an empty black page */
.no-gl body {
  background:
    radial-gradient(60% 50% at 30% 35%, rgba(70, 105, 86, 0.32), transparent 70%),
    radial-gradient(50% 40% at 75% 60%, rgba(184, 216, 189, 0.1), transparent 70%),
    var(--bg);
}
.no-gl #field {
  display: none;
}

/* scanlines + falloff, under the type so the copy stays crisp */
.optics {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(to top, rgba(4, 6, 5, 0.5), transparent 40%),
    radial-gradient(ellipse at 50% 45%, transparent 52%, rgba(0, 0, 0, 0.6) 100%),
    repeating-linear-gradient(
      to bottom,
      transparent 0,
      transparent 2px,
      rgba(0, 0, 0, 0.18) 3px
    );
  animation: flicker 6s steps(1, end) infinite;
}

@keyframes flicker {
  0%,
  100% {
    opacity: 1;
  }
  47% {
    opacity: 1;
  }
  48% {
    opacity: 0.86;
  }
  49% {
    opacity: 1;
  }
  83% {
    opacity: 0.93;
  }
  84% {
    opacity: 1;
  }
}

/* ---------------------------------------------------------------- viewfinder */

.frame {
  position: fixed;
  inset: clamp(0.7rem, 1.4vw, 1.4rem);
  z-index: 3;
  pointer-events: none;
}
.frame i {
  position: absolute;
  width: clamp(1.1rem, 2vw, 1.8rem);
  height: clamp(1.1rem, 2vw, 1.8rem);
  border: 0 solid var(--rule);
  animation: frame-in 0.6s var(--ease) var(--frame-delay) both;
}
.frame i:nth-child(1) {
  top: 0;
  left: 0;
  border-width: 1px 0 0 1px;
}
.frame i:nth-child(2) {
  top: 0;
  right: 0;
  border-width: 1px 1px 0 0;
}
.frame i:nth-child(3) {
  bottom: 0;
  left: 0;
  border-width: 0 0 1px 1px;
}
.frame i:nth-child(4) {
  bottom: 0;
  right: 0;
  border-width: 0 1px 1px 0;
}

@keyframes frame-in {
  from {
    opacity: 0;
    transform: scale(1.6);
  }
}

/* -------------------------------------------------------------------- layout */

.stage {
  position: relative;
  z-index: 2;
  min-height: 100svh;
  padding: var(--pad);
  padding-block: calc(var(--pad) + 0.4rem);
  display: grid;
  grid-template-rows: auto 1fr;
}

.hud {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
}

.hud__item {
  margin: 0;
  font-size: clamp(0.68rem, 0.84vw, 0.8rem);
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mute);
  text-shadow:
    0 0 10px var(--bg),
    0 0 3px var(--bg);
}

.hero {
  align-self: end;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: clamp(1.5rem, 4vw, 4rem);
}

.hero__copy {
  display: grid;
  gap: clamp(0.8rem, 1.6vw, 1.4rem);
  justify-items: start;
}

.lede {
  margin: 0;
  max-width: 24ch;
  font-size: clamp(1.1rem, 1.85vw, 1.7rem);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.012em;
  color: var(--ink);
  text-wrap: balance;
  text-shadow:
    0 0 16px var(--bg),
    0 0 4px var(--bg);
}

/* The real wordmark, used as a mask so the ink colour is ours to set. */
.wordmark {
  margin: 0;
  width: clamp(16rem, 62vw, 72rem);
  filter: drop-shadow(0 0 20px rgba(184, 216, 189, 0.16));
  clip-path: inset(-48px 100% -48px -48px); /* wipe from the left; the box reaches past the logo so its glow is never sliced */
  animation: wipe var(--wipe-dur) steps(16, end) calc(var(--d, 0s) * var(--stagger)) forwards;
}
.wordmark__ink {
  display: block;
  /* The artwork has 40px of transparent padding on every side (of 2800), so
     nothing sits on the edge of the mask and rounding can't shave a letter.
     The negative margins cancel it out: the logo keeps its size and place. */
  --pad: calc(40 / 2800 * 100%);
  margin: calc(-1 * var(--pad));
  aspect-ratio: 2880 / 572;
  background: var(--ink);
  -webkit-mask: url('img/wordmark.png') 0 0 / 100% 100% no-repeat;
  mask: url('img/wordmark.png') 0 0 / 100% 100% no-repeat;
}
@keyframes wipe {
  to {
    clip-path: inset(-48px -48px -48px -48px);
  }
}

.origin {
  max-width: 62ch;
  line-height: 1.7;
}

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

.contact {
  display: grid;
  gap: clamp(0.8rem, 1.5vw, 1.2rem);
  font-style: normal;
  text-align: right;
  justify-items: end;
}
.contact p {
  margin: 0;
  display: grid;
  gap: 0.25rem;
}
.contact span {
  font-size: clamp(0.66rem, 0.78vw, 0.76rem);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--mute);
  text-shadow: 0 0 8px var(--bg);
}
.contact a {
  position: relative;
  font-size: clamp(0.92rem, 1.2vw, 1.1rem);
  font-weight: 500;
  padding-block: 0.1em;
  will-change: transform;
  background:
    linear-gradient(var(--phos), var(--phos)) right bottom / 0% 1px no-repeat;
  transition:
    color 0.25s var(--ease),
    background-size 0.45s var(--ease),
    transform 0.5s var(--ease);
}
.contact a:focus-visible {
  color: var(--phos);
  background-size: 100% 1px;
}
/* Hover effects are for real pointers only. On a touch screen a tap would
   "stick" the hover state, and phones then swallow the tap as the hover — the
   link needs a second tap to actually open. */
@media (hover: hover) and (pointer: fine) {
  .contact a:hover {
    color: var(--phos);
    background-size: 100% 1px;
  }
}
a:focus-visible {
  outline: 1px solid var(--red);
  outline-offset: 6px;
}

/* ------------------------------------------------------------------- reveals */

.rise {
  animation: rise var(--rise-dur) var(--ease) calc(var(--d, 0s) * var(--stagger)) both;
}
@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(0.7rem);
    filter: blur(5px);
  }
}

/* -------------------------------------------------------------- map signals */

/* A lock-on dropped over a city when a ping or route lands. The bracket
   contracts onto the target, holds, then dissolves — all driven by field.js. */
.ping {
  position: fixed;
  z-index: 4;
  pointer-events: none;
  animation: ping-life 3.8s linear both;
}
.ping__lock {
  position: absolute;
  top: 0;
  left: 0;
  width: 2.1rem;
  height: 2.1rem;
  margin: -1.05rem 0 0 -1.05rem;
  --c: var(--red);
  background:
    linear-gradient(var(--c), var(--c)) 0 0 / 0.55rem 1px no-repeat,
    linear-gradient(var(--c), var(--c)) 0 0 / 1px 0.55rem no-repeat,
    linear-gradient(var(--c), var(--c)) 100% 0 / 0.55rem 1px no-repeat,
    linear-gradient(var(--c), var(--c)) 100% 0 / 1px 0.55rem no-repeat,
    linear-gradient(var(--c), var(--c)) 0 100% / 0.55rem 1px no-repeat,
    linear-gradient(var(--c), var(--c)) 0 100% / 1px 0.55rem no-repeat,
    linear-gradient(var(--c), var(--c)) 100% 100% / 0.55rem 1px no-repeat,
    linear-gradient(var(--c), var(--c)) 100% 100% / 1px 0.55rem no-repeat;
  animation: lock-on 0.75s var(--ease) both;
}
@keyframes lock-on {
  from {
    transform: scale(3.2);
    opacity: 0;
  }
  45% {
    opacity: 1;
  }
}
.ping__label {
  position: absolute;
  top: -1.05rem;
  left: 1.7rem;
  display: grid;
  gap: 0.15rem;
  white-space: nowrap;
  font-size: clamp(0.64rem, 0.78vw, 0.74rem);
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  text-shadow:
    0 0 10px var(--bg),
    0 0 3px var(--bg);
  animation: label-in 0.5s steps(8, end) 0.35s both;
}
.ping__label strong {
  font-weight: 700;
  color: var(--ink);
}
.ping__label em {
  font-style: normal;
  color: var(--mute);
}
/* near the right edge the label flips to the other side of the bracket */
.ping--left .ping__label {
  left: auto;
  right: 1.7rem;
  text-align: right;
}
@keyframes label-in {
  from {
    opacity: 0;
    clip-path: inset(0 100% 0 0);
  }
}
@keyframes ping-life {
  0%,
  78% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/* ------------------------------------------------------------------- reticle */

.reticle {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 50;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s;
}
.reticle.is-on {
  opacity: 1;
}

.reticle__ring {
  position: absolute;
  top: 0;
  left: 0;
  width: 2.9rem;
  height: 2.9rem;
  border: 1px solid var(--phos);
  border-radius: 50%;
  /* size, position and radius are driven by springs in hud.js */
  transition:
    border-color 0.25s,
    background-color 0.25s;
}
/* four tick marks, to read as a targeting reticle rather than a plain ring */
.reticle__ring::before,
.reticle__ring::after {
  content: '';
  position: absolute;
  background: var(--phos);
  transition: opacity 0.2s;
}
.reticle__ring::before {
  left: 50%;
  top: -0.55rem;
  bottom: -0.55rem;
  width: 1px;
  mask: linear-gradient(
    to bottom,
    #000 0 0.4rem,
    transparent 0.4rem calc(100% - 0.4rem),
    #000 calc(100% - 0.4rem)
  );
  -webkit-mask: linear-gradient(
    to bottom,
    #000 0 0.4rem,
    transparent 0.4rem calc(100% - 0.4rem),
    #000 calc(100% - 0.4rem)
  );
}
.reticle__ring::after {
  top: 50%;
  left: -0.55rem;
  right: -0.55rem;
  height: 1px;
  mask: linear-gradient(
    to right,
    #000 0 0.4rem,
    transparent 0.4rem calc(100% - 0.4rem),
    #000 calc(100% - 0.4rem)
  );
  -webkit-mask: linear-gradient(
    to right,
    #000 0 0.4rem,
    transparent 0.4rem calc(100% - 0.4rem),
    #000 calc(100% - 0.4rem)
  );
}

.reticle__dot {
  position: absolute;
  top: 0;
  left: 0;
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  background: var(--red);
  box-shadow: 0 0 8px var(--red);
}

.reticle__readout {
  position: absolute;
  top: 0;
  left: 0;
  margin: 1.9rem 0 0 1.9rem;
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  white-space: pre;
  color: var(--phos);
  text-shadow: 0 0 6px var(--bg), 0 0 2px var(--bg);
}

/* over a link: the ring (resized by hud.js) turns red and drops its ticks,
   and the coordinate readout steps aside so it can't collide with the labels */
.reticle__readout {
  transition: opacity 0.2s;
}
.reticle.is-link .reticle__readout {
  opacity: 0;
}
.reticle.is-link .reticle__ring {
  border-color: var(--red);
  background-color: rgba(255, 59, 48, 0.06);
}
.reticle.is-link .reticle__ring::before,
.reticle.is-link .reticle__ring::after {
  opacity: 0;
}

.has-reticle,
.has-reticle a {
  cursor: none;
}

/* ----------------------------------------------- phones: don't make them wait */

/* On a phone the copy is the first thing a person wants, and a 2-second
   staged entrance reads as the page being slow, not as design. So: the whole
   sequence is compressed to ~12% of its length (everything is readable inside
   a second), the fades are shorter, and the blur is dropped — blurring large
   text while it animates is also costly on a phone GPU. */
@media (max-width: 56rem), (hover: none) and (pointer: coarse) {
  :root {
    --stagger: 0.12;
    --rise-dur: 0.45s;
    --wipe-dur: 0.5s;
    --frame-delay: 0.1s;
  }
  @keyframes rise {
    from {
      opacity: 0;
      transform: translateY(0.5rem);
    }
  }
}

/* ---------------------------------------------------------------- responsive */

@media (max-width: 56rem) {
  .hero {
    grid-template-columns: 1fr;
    gap: 1.75rem;
  }
  .contact {
    text-align: left;
    justify-items: start;
    grid-template-columns: 1fr;
  }
  .wordmark {
    width: min(100%, 36rem);
  }
  .lede {
    font-size: clamp(0.92rem, 4.15vw, 1.4rem);
    max-width: none; /* two lines: "…booking & management / for electronic music." */
  }
  .origin {
    display: none;
  }
}

@media (max-width: 30rem) {
  .hud--top p:last-child {
    display: none;
  }
}

/* short landscape screens: keep everything on one frame */
@media (max-height: 34rem) and (min-width: 40rem) {
  .origin {
    display: none;
  }
  .lede {
    max-width: 40ch;
    font-size: 1rem;
  }
}

/* ------------------------------------------------------------- reduced motion */

@media (prefers-reduced-motion: reduce) {
  .optics {
    animation: none;
  }
  .rise,
  .frame i {
    animation: none;
    opacity: 1;
  }
  .wordmark {
    animation: none;
    clip-path: none;
  }
}

/* ------------------------------------------------------------ views + roster */

/* Home, the roster and an artist all live in the same slot; one is shown at a time. */
.view {
  grid-row: 2;
  grid-column: 1;
  align-self: end;
  opacity: 0;
  visibility: hidden;
  transform: translateY(0.8rem);
  transition:
    opacity 0.4s var(--ease),
    transform 0.4s var(--ease),
    visibility 0s linear 0.4s;
}
body[data-view='home'] .hero,
body[data-view='roster'] .roster,
body[data-view='artist'] .artist {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition:
    opacity 0.55s var(--ease) 0.2s,
    transform 0.55s var(--ease) 0.2s,
    visibility 0s;
}

.nav {
  align-self: flex-start;
  padding: 0.7rem 1.15rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-size: clamp(0.9rem, 1.15vw, 1.1rem);
  color: var(--ink);
  text-decoration: none;
  text-shadow: none;
  background: rgba(4, 6, 5, 0.45);
  transition:
    color 0.25s,
    border-color 0.25s,
    background 0.25s;
}
.nav:focus-visible {
  color: var(--bg);
  background: var(--ink);
  border-color: var(--ink);
}
@media (hover: hover) and (pointer: fine) {
  .nav:hover {
    color: var(--bg);
    background: var(--ink);
    border-color: var(--ink);
  }
}

.roster__list {
  list-style: none;
  margin: 0;
  padding: 0;
  width: min(100%, 50rem);
  display: grid;
  grid-template-columns: 1fr 1fr; /* two columns: eighteen names would otherwise run off the page */
  grid-auto-flow: column;
  grid-template-rows: repeat(9, auto);
  column-gap: clamp(1.5rem, 3vw, 3rem);
}
.roster__item {
  opacity: 0;
  transform: translateY(0.6rem);
  transition:
    opacity 0.5s var(--ease),
    transform 0.5s var(--ease);
}
body[data-view='roster'] .roster__item {
  opacity: 1;
  transform: none;
  transition-delay: calc(0.3s + var(--i, 0) * 0.055s);
}
.roster__item a {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 1rem;
  padding: 0.5rem 0 0.55rem;
  border-top: 1px solid var(--rule);
  color: var(--ink);
  text-decoration: none;
  font-size: clamp(1.05rem, 1.65vw, 1.5rem);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.1;
  text-shadow:
    0 0 16px var(--bg),
    0 0 4px var(--bg);
  transition:
    color 0.25s,
    padding-left 0.35s var(--ease);
}
.roster__item a:focus-visible {
  color: var(--phos);
  padding-left: 0.6rem;
}
@media (hover: hover) and (pointer: fine) {
  .roster__item a:hover {
    color: var(--phos);
    padding-left: 0.6rem;
  }
}
.roster__meta {
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mute);
  text-align: right;
}

.artist {
  display: grid;
  gap: 1.1rem;
  justify-items: start;
  max-width: 34rem;
}
.artist__eyebrow {
  margin: 0;
  font-size: 0.74rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--phos);
  text-shadow:
    0 0 10px var(--bg),
    0 0 3px var(--bg);
}
.artist__name {
  margin: 0;
  font-size: clamp(2.4rem, 6.4vw, 6rem);
  font-weight: 500;
  letter-spacing: -0.035em;
  line-height: 0.98;
  text-shadow:
    0 0 24px var(--bg),
    0 0 6px var(--bg);
}
.artist__facts {
  margin: 0;
  display: grid;
  gap: 0.7rem;
  font-size: clamp(0.95rem, 1.2vw, 1.1rem);
}
.artist__facts div {
  display: grid;
  gap: 0.15rem;
}
.artist__facts dt {
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--mute);
}
.artist__facts dd {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1.2rem;
  text-shadow:
    0 0 12px var(--bg),
    0 0 3px var(--bg);
}
.artist__facts a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-underline-offset: 0.25em;
}

@media (max-width: 56rem) {
  /* phone: one column, and the list scrolls inside its own window so the map stays visible */
  .roster__list {
    width: 100%;
    grid-template-columns: 1fr;
    grid-template-rows: none;
    grid-auto-flow: row;
    max-height: 44svh;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
    border-bottom: 1px solid var(--rule);
    -webkit-mask: linear-gradient(to bottom, #000 calc(100% - 1.6rem), transparent);
    mask: linear-gradient(to bottom, #000 calc(100% - 1.6rem), transparent);
  }
  .roster__list::-webkit-scrollbar {
    display: none;
  }
  .roster__item a {
    padding: 0.45rem 0 0.5rem;
    font-size: clamp(1.05rem, 4.6vw, 1.35rem);
  }
  .artist {
    gap: 0.8rem;
  }
  .artist__name {
    font-size: clamp(2.2rem, 12vw, 3.4rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  .view,
  .roster__item {
    transition: none;
  }
}

/* …emails and handles stay selectable and copyable */
.contact a,
.artist__facts a {
  -webkit-user-select: text;
  user-select: text;
}
a,
img {
  -webkit-user-drag: none;
}
