*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  background: #000;
  color: #fff;
  font-family: system-ui, sans-serif;
  overflow: hidden;
}

#scene,
#fallback {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

#scene {
  touch-action: none;
}

#fallback {
  background: #000 url("../assets/bg-1280.webp") center / contain no-repeat;
  transition: transform 0.2s ease-out;
}

@media (orientation: portrait) {
  #fallback {
    background-image: url("../assets/bg-portrait-720.webp");
  }
}

#content {
  position: relative;
  pointer-events: none;
}

/* Name across the top, in the black above the hat */
#name {
  position: fixed;
  top: max(2vmin, env(safe-area-inset-top));
  left: 50%;
  width: 96vw;
  margin: 0;
  padding: 1vmin 0;
  transform: translateX(-50%);
  pointer-events: auto;
  user-select: none;
  -webkit-user-select: none;
}

/* The slow pulse lives on the image so it multiplies with the flicker on #name.
   Alternating with a sine ease makes the opacity follow a sine wave. name.js lifts
   the image to full opacity for a flicker and eases it back down to --pulse-high. */
#name img {
  --pulse-high: 0.7;
  --pulse-low: 0.4;
  --pulse-period: 4s;
  display: block;
  width: 100%;
  opacity: var(--pulse-high);
  animation: pulse calc(var(--pulse-period) / 2) cubic-bezier(0.37, 0, 0.63, 1) infinite alternate;
}

@keyframes pulse {
  to { opacity: var(--pulse-low); }
}

/* In the landscape scene the name runs the width of the photo, which never gets wider
   than 1728:963 (see minVisible in config.js), and the shader draws it behind the figure. This element only provides the
   layout, the hover target and the opacity. */
@media (orientation: landscape) {
  #name {
    width: min(96vw, 96vh * 1728 / 963);
  }
}

/* Hidden until name.js flickers it in once the scene is showing */
#name.intro {
  opacity: 0;
  pointer-events: none;
}

#name.masked img {
  visibility: hidden;
}

/* Without WebGL nothing can mask it, so it is sized to stay above the hat. --room is the
   gap above the hat: the fallback photo is fitted with "contain" and the hat top sits at
   16.5% of its height. */
@media (orientation: landscape) {
  .no-gl #name {
    --room: calc(50vh - 0.335 * min(100vh, 56.25vw));
    top: max(calc(var(--room) * 0.1), env(safe-area-inset-top));
    width: min(96vw, var(--room) * 7.5);
    padding: calc(var(--room) * 0.05) 0;
  }
}

@media (hover: hover) and (pointer: fine) {
  #name:hover {
    animation: flicker 0.3s;
  }
}

#name.flicker {
  animation: flicker 0.3s;
}

@media (prefers-reduced-motion: reduce) {
  #name,
  #name img {
    animation: none !important;
  }
}

@keyframes flicker {
  0% { opacity: 0.1; }
  9% { opacity: 0.5; }
  10% { opacity: 1; }
  19% { opacity: 1; }
  29% { opacity: 0.4; }
  39% { opacity: 1; }
  49% { opacity: 0.2; }
  59% { opacity: 1; }
  69% { opacity: 0.5; }
  79% { opacity: 0.8; }
  80% { opacity: 0.4; }
  89% { opacity: 0; }
  99% { opacity: 1; }
  100% { opacity: 0.2; }
}

/* Contact line near the bottom, over the figure's waist */
#contact {
  position: fixed;
  left: 50%;
  bottom: max(8vh, env(safe-area-inset-bottom));
  transform: translateX(-50%);
  font-family: "Oxanium", system-ui, sans-serif;
  font-weight: 300;
  font-size: 3.2vmin;
  letter-spacing: 0.04em;
  white-space: nowrap;
  color: #fff;
  /* Middle of the name's pulse range so the two read as the same grey */
  opacity: 0.85;
  text-decoration: none;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: auto;
  transition: opacity 0.2s;
}

#contact:hover,
#contact:focus-visible {
  opacity: 1;
}

/* Swap ◎ for ◉ on hover */
#contact .dot-on,
#contact:hover .dot-off,
#contact:focus-visible .dot-off {
  display: none;
}

#contact:hover .dot-on,
#contact:focus-visible .dot-on {
  display: inline;
}

/* No hover on touch screens, so flash between the two shapes instead. Both glyphs share one grid
   cell, so they sit on the same baseline and the link's width doesn't change. */
@media (hover: none) {
  #contact .dot {
    display: inline-grid;
  }

  #contact .dot-off,
  #contact .dot-on {
    display: block;
    grid-area: 1 / 1;
    animation: dot-flash 1.2s steps(1, end) infinite;
  }

  #contact .dot-on {
    animation-name: dot-flash-on;
  }
}

@keyframes dot-flash {
  0% { opacity: 1; }
  50% { opacity: 0; }
}

@keyframes dot-flash-on {
  0% { opacity: 0; }
  50% { opacity: 1; }
}

/* Hidden until name.js shows it on the first pointerup after the intro. Visibility rather than
   opacity, so the hover transition doesn't ease it in. */
#contact.intro {
  visibility: hidden;
}

@media (orientation: portrait) {
  #contact {
    font-size: 5.2vmin;
  }
}

/* Loading screen: covers everything but the scan lines until js/loader.js removes it */
#loader {
  position: fixed;
  inset: 0;
  z-index: 5;
  display: grid;
  place-items: center;
  background: #000;
  color: #fff;
  transition: opacity var(--out, 0.4s); /* --out is set by loader.js (OUT_MS) */
}

#loader.out {
  opacity: 0;
  pointer-events: none;
}

/* Split dismissal: two black panels, each showing its half of the frozen logo,
   slide apart to reveal the scene */
#loader.split.out {
  opacity: 1;
}

#loader.split {
  background: none;
}

#loader .half {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: #000;
  transition: transform var(--out, 0.4s) cubic-bezier(0.5, 0, 0.75, 0);
}

#loader .half.l { clip-path: inset(0 50% 0 0); }
#loader .half.r { clip-path: inset(0 0 0 50%); }

#loader.out .half.l { transform: translateX(-50%); }
#loader.out .half.r { transform: translateX(50%); }

#loader svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* One-pixel stroke so pieces drawn separately overlap instead of leaving a seam */
#loader path {
  fill: currentColor;
  stroke: currentColor;
  stroke-width: 1px;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}

#loader.orbit svg,
#loader .scene {
  width: 100vw;
  aspect-ratio: 1000 / var(--vb-h, 194); /* viewBox height, set by loader.js */
  height: auto;
}

#loader .scene {
  position: relative;
  /* Relative to the width so the fold foreshortens the same at any viewport size;
     a fixed px value flattens the flip on narrow screens. Set by loader.js. */
  perspective: var(--persp, 200vw);
}

#loader .card {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

#loader .card svg {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  /* Safari only honours backface-visibility on elements with a 3D transform of their
     own; without this the front face stays visible past 90° and the flip looks like
     the whole plate revolving */
  transform: rotateY(0deg);
}

/* Second svg in each card is the FDORAG side */
#loader .card svg + svg {
  transform: rotateY(180deg);
}

/* CRT scan lines drifting up over everything. The layer is one loop taller than the
   screen and slides by transform, so the lines are painted once and only composited. */
#scanlines {
  --line: 4.8px;
  --loop: calc(var(--line) * 5);
  position: fixed;
  inset: 0 0 calc(var(--loop) * -1);
  z-index: 10;
  pointer-events: none;
  opacity: 0.3;
  background: linear-gradient(to top, #000 10%, transparent 10%, transparent 50%, #2a0d11 50%, #000 60%, transparent 60%) 0 0 / 100% var(--line);
  animation: scan 1.5s linear infinite;
  will-change: transform;
}

@keyframes scan {
  to { transform: translateY(calc(var(--loop) * -1)); }
}

@media (prefers-reduced-motion: reduce) {
  #scanlines {
    animation: none;
  }
}
