@import url("https://fonts.cdnfonts.com/css/pp-neue-montreal");
@import url("shell.css");

/* ===================================================================
   HORIZON - front page

   A single rotating ring of work seen in perspective, with a heading
   compass along the bottom. Only the ring wrapper transforms per frame;
   every card's own transform is set once and never touched again, so the
   whole page is one composited layer plus 16 opacity writes.

   Loaded ONLY by index.html. Project pages are untouched.
   =================================================================== */

:root {
  --hz-white: #ffffff;
  --hz-dim: rgba(255, 255, 255, 0.42);
  --hz-faint: rgba(255, 255, 255, 0.16);
  --hz-rule: rgba(255, 255, 255, 0.12);

  --hz-card-w: 560px;
  --hz-card-h: calc(var(--hz-card-w) / 1.6);
  --hz-perspective: 1900px;
  --hz-gap: 30px;
  --hz-ring-top: 43%;

  --hz-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* base.css lets the index scroll on mobile. The ring is a fixed stage,
   so that is reclaimed here - these are the counterparts to those rules. */
html {
  overflow: hidden !important;
  height: 100% !important;
  scroll-behavior: auto;
}

body.horizon {
  overflow: hidden !important;
  height: 100vh;
  height: 100dvh;
  width: 100%;
  position: relative;
  background: #000;
  color: var(--hz-white);
  font-family: var(--font-primary);
  overscroll-behavior: none;
  -webkit-font-smoothing: antialiased;
}

@media (max-width: 768px) {
  body.horizon {
    position: fixed !important;
    overflow: hidden !important;
    height: 100dvh !important;
    inset: 0;
  }
}

/* Film grain.
   A single-octave, stitched noise tile that jumps between ten positions at
   ~22fps - real grain resamples every frame, it does not drift smoothly.
   Only `transform` animates, so the texture uploads once and the motion is
   pure compositor work: no repaint, no per-frame rasterisation.

   The tile halves on 2x displays so one noise pixel lands on one device
   pixel. That is what keeps it sharp rather than soft and smeared. */
body.horizon::after {
  content: "";
  position: fixed;
  top: -10%;
  left: -10%;
  width: 120%;
  height: 120%;
  z-index: 60;
  pointer-events: none;
  opacity: 0.075;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 128px 128px;
  background-repeat: repeat;
  animation: hz-grain 0.45s steps(1) infinite;
  will-change: transform;
}

@media (min-resolution: 2dppx) {
  body.horizon::after {
    background-size: 64px 64px;
    opacity: 0.085;
  }
}

@keyframes hz-grain {
  0%   { transform: translate3d(0, 0, 0); }
  10%  { transform: translate3d(-3%, -2%, 0); }
  20%  { transform: translate3d(2%, -4%, 0); }
  30%  { transform: translate3d(-4%, 3%, 0); }
  40%  { transform: translate3d(3%, 2%, 0); }
  50%  { transform: translate3d(-2%, -3%, 0); }
  60%  { transform: translate3d(4%, 1%, 0); }
  70%  { transform: translate3d(-1%, 4%, 0); }
  80%  { transform: translate3d(2%, -1%, 0); }
  90%  { transform: translate3d(-3%, 2%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* Ambient falloff - the ring sits in a pool of light, edges fall to black */
body.horizon::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(ellipse 90% 70% at 50% 56%, rgba(255, 255, 255, 0.05), transparent 60%);
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Header and logo live in site-header.css now, shared with the project
   pages, so there is one definition instead of one per page type. The mono
   type scale they used is still needed by the rest of this page. */

.hz-sig,
.hz-est,
.hint,
.compass-read,
.compass-arc-label,
.intro-label,
.title-meta {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* ============ Stage ============ */

.stage {
  position: fixed;
  inset: 0;
  z-index: 10;
  perspective: var(--hz-perspective);
  perspective-origin: 50% 42%;
  touch-action: none;
  cursor: grab;
  overscroll-behavior: none;
}

.stage.is-dragging { cursor: grabbing; }

.ring {
  position: absolute;
  left: 50%;
  top: var(--hz-ring-top);
  width: 0;
  height: 0;
  transform-style: preserve-3d;
  will-change: transform;
}

.card {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--hz-card-w);
  height: var(--hz-card-h);
  margin-left: calc(var(--hz-card-w) / -2);
  margin-top: calc(var(--hz-card-h) / -2);
  display: block;
  background: transparent;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  text-decoration: none;
  cursor: pointer;
  opacity: 0;
  will-change: opacity;
  contain: layout style;
  --halo-rgb: 255, 255, 255;   /* replaced per-card from the artwork */
  transition: transform 0.55s var(--hz-ease);   /* NOT paint - the glow has to spill outside */
}

.card::after { display: none; }              /* cancel the global link wipe */

/* The image, clipped. Kept separate from .card so the halo can bleed out. */
.card-media {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  background: #050505;
}

.card-media::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.55);
}

.card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
  transition: transform 0.9s var(--hz-ease), filter 0.5s var(--hz-ease);
}

/* ---- Wavy border glow ----------------------------------------------
   Two counter-rotating conic gradients under a blur. Because they run at
   different speeds and never realign, the light travelling around the rim
   never repeats - it undulates. Both spinners animate `transform` only,
   so the motion itself stays on the compositor.

   The trending version of this is a rainbow @property angle sweep. Rainbow
   would break the monochrome, so the wedges are white at varying strength
   and the colour comes from the artwork underneath.
   -------------------------------------------------------------------- */
.card-halo {
  position: absolute;
  inset: -16px;
  z-index: 0;
  overflow: hidden;
  opacity: 0;
  filter: blur(30px);
  transition: opacity 0.7s var(--hz-ease);
  pointer-events: none;
}

.card-halo i {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 165%;
  padding-bottom: 165%;
  height: 0;
  margin: -82.5% 0 0 -82.5%;
  transform-origin: 50% 50%;
  will-change: transform;
}

.card-halo i:nth-child(1) {
  background: conic-gradient(
    from 0deg,
    transparent 0deg,
    rgba(var(--halo-rgb), 0.95) 34deg,
    transparent 92deg,
    rgba(var(--halo-rgb), 0.6) 168deg,
    transparent 226deg,
    rgba(var(--halo-rgb), 0.85) 300deg,
    transparent 360deg
  );
  animation: halo-spin 9s linear infinite;
}

.card-halo i:nth-child(2) {
  background: conic-gradient(
    from 0deg,
    transparent 0deg,
    rgba(var(--halo-rgb), 0.75) 74deg,
    transparent 150deg,
    rgba(var(--halo-rgb), 0.9) 250deg,
    transparent 340deg
  );
  animation: halo-spin 13s linear infinite reverse;
  opacity: 0.65;
}

@keyframes halo-spin {
  to { transform: rotate(360deg); }
}

/* Focused card: a hairline, nothing louder */
.card-edge {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  border: 1px solid rgba(var(--halo-rgb), 0.5);
  opacity: 0;
  transition: opacity 0.6s var(--hz-ease), border-color 0.6s var(--hz-ease);
}

/* The selected card sits lit at rest - ambient, not a highlight */
.card.is-focus .card-edge { opacity: 0.3; }
.card.is-focus .card-halo { opacity: 0.32; }

/* Hover reward, focused card only: the rim lights up and breathes */
@media (hover: hover) {
  .card.is-focus:hover .card-halo { opacity: 0.62; }
  .card.is-focus:hover .card-edge { opacity: 0.62; }
  .card.is-focus:hover img { transform: scale(1.045); }
  .card:not(.is-focus):hover img { filter: brightness(1.3); }
}

/* No hover to reward on touch - give the focused card a resting glow */
@media (hover: none) {
  .card.is-focus .card-halo { opacity: 0.42; }
}

.card:focus-visible .card-halo { opacity: 0.62; }

/* The card being flown into carries the blur itself, so its rim, frame and
   hover scale all get out of the way. */
.card.is-zooming {
  z-index: 5;
  transition: none;      /* the flight writes transform every frame */
}

.card.is-zooming .card-halo,
.card.is-zooming .card-edge {
  opacity: 0 !important;
  transition: opacity 0.25s linear;
}

.card.is-zooming img {
  transform: none !important;
  transition: none;
}

.card:focus-visible {
  outline: none;
}

.card:focus-visible .card-edge {
  opacity: 1;
  border-width: 2px;
}

/* Falloff at the mouth of the cylinder. Plain gradients over pure black
   rather than a mask on .stage - masking the perspective root would force
   the whole 3D scene to re-rasterise every frame. */
.edge-fade {
  position: fixed;
  top: 0;
  bottom: 0;
  width: clamp(140px, 24vw, 460px);
  z-index: 15;
  pointer-events: none;
}

.edge-fade-l {
  left: 0;
  background: linear-gradient(
    to right,
    #000 0%,
    rgba(0, 0, 0, 0.98) 16%,
    rgba(0, 0, 0, 0.86) 36%,
    rgba(0, 0, 0, 0.52) 62%,
    rgba(0, 0, 0, 0.18) 82%,
    transparent 100%
  );
}

.edge-fade-r {
  right: 0;
  background: linear-gradient(
    to left,
    #000 0%,
    rgba(0, 0, 0, 0.98) 16%,
    rgba(0, 0, 0, 0.86) 36%,
    rgba(0, 0, 0, 0.52) 62%,
    rgba(0, 0, 0, 0.18) 82%,
    transparent 100%
  );
}

/* ============ Titles ============ */

.titles {
  position: fixed;
  left: 0;
  right: 0;
  /* Directly under the card: ring centre + a little over half the card's
     height (it sits lifted, so slightly taller than at rest) + a gap. Ties
     itself to --hz-card-h, so every breakpoint stays composed. */
  top: calc(var(--hz-ring-top) + var(--hz-card-h) * 0.62 + 4.5rem);
  bottom: auto;
  z-index: 20;
  text-align: center;
  pointer-events: none;
  padding: 0 1.5rem;
}

.title-main {
  font-family: var(--font-primary);
  font-size: clamp(30px, 5vw, 76px);
  font-weight: 600;
  line-height: 0.94;
  letter-spacing: -0.045em;
  text-transform: uppercase;
  margin: 0 0 0.75rem;
  color: var(--hz-white);
}

.title-meta {
  color: var(--hz-dim);
  margin: 0;
  font-size: clamp(13px, 1.05vw, 17px);
  letter-spacing: 0.22em;
}

.hint {
  position: fixed;
  left: 0;
  right: 0;
  top: 13%;
  bottom: auto;
  z-index: 20;
  text-align: center;
  color: var(--hz-faint);
  pointer-events: none;
  transition: opacity 0.8s var(--hz-ease);
  margin: 0;
}

.hint.is-gone { opacity: 0; }

/* ============ Footer / compass ============ */

.hz-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1.75rem 2rem;
  pointer-events: none;
}

.hz-footer > * { pointer-events: auto; }

.hz-sig,
.hz-est {
  color: var(--hz-dim);
  margin: 0;
  flex: 1 1 0;
  white-space: nowrap;
}

.hz-est { text-align: right; }

.compass {
  position: relative;
  width: min(440px, 46vw);
  flex: 0 0 auto;
  padding-bottom: 0.15rem;
}

.compass-window {
  position: relative;
  height: 26px;
  cursor: pointer;
}

/* The full extent of the ring */
.compass-window::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--hz-faint);
}

/* Degree ticks. One repeating gradient, shifted by background-position -
   no per-tick DOM, no layout, and it scrolls forever. */
/* Retired: the ruler is now drawn inside each project slice, because the
   track is no longer linear in degrees. */
.compass-ticks {
  display: none;
}

.compass-arcs {
  position: absolute;
  inset: 0;
}

/* Each project owns a contiguous arc of the ring, drawn to scale */
.compass-arc {
  position: absolute;
  left: 0;
  bottom: 11px;
  height: 1px;
  background: rgba(var(--halo-rgb, 255, 255, 255), 0.32);
  transition: background-color 0.4s var(--hz-ease);
  will-change: transform;
}

/* One tick per image, spread across that project's slice - so the slices
   stay equal while still showing which project holds the most work. */
.compass-arc-ticks {
  position: absolute;
  left: 0;
  top: 5px;
  width: 100%;
  height: 6px;
  background-image: repeating-linear-gradient(
    to right,
    var(--hz-faint) 0,
    var(--hz-faint) 1px,
    transparent 1px,
    transparent var(--gap, 20px)
  );
}

.compass-arc.is-near {
  background: rgba(var(--halo-rgb, 255, 255, 255), 0.95);
}

.compass-arc-label {
  position: absolute;
  bottom: 15px;
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: 9.5px;
  letter-spacing: 0.16em;
  color: var(--hz-dim);
}

.compass-arc.is-near .compass-arc-label { color: var(--hz-white); }

/* How far through the ring you are */
.compass-progress {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: var(--hz-white);
  transform-origin: left center;
  transform: scaleX(0);
  will-change: transform;
  pointer-events: none;
}

.compass-needle {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 15px;
  background: var(--hz-white);
  will-change: transform;
  pointer-events: none;
}

.compass-read {
  display: flex;
  justify-content: space-between;
  margin-top: 0.65rem;
  color: var(--hz-dim);
  font-size: 10px;
}

/* ============ Intro ============ */

.intro {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
}

.intro.is-done {
  opacity: 0;
  pointer-events: none;
}

.intro-inner {
  width: min(88vw, 1400px);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: clamp(1rem, 2.5vw, 2rem);
}

.intro-count {
  font-family: var(--font-primary);
  font-size: clamp(56px, 15vw, 170px);
  font-weight: 600;
  line-height: 0.85;
  letter-spacing: -0.05em;
  color: var(--hz-white);
  font-variant-numeric: tabular-nums;
  text-align: left;
}

.intro-count::after {
  content: "%";
  font-size: 0.3em;
  letter-spacing: 0;
  vertical-align: top;
  margin-left: 0.15em;
  opacity: 0.45;
}

.intro-bar {
  position: relative;
  width: 100%;
  height: 2px;
  background: var(--hz-faint);
  overflow: hidden;
}

.intro-bar i {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  background: var(--hz-white);
  transition: width 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.intro-label {
  color: var(--hz-dim);
  text-align: right;
}

/* ============ Responsive ============ */

@media (min-width: 1800px) {
  :root {
    --hz-card-w: 640px;
    --hz-perspective: 2100px;
  }
}

@media (max-width: 1440px) {
  :root {
    --hz-card-w: 480px;
    --hz-perspective: 1650px;
  }
}

@media (max-width: 1100px) {
  :root {
    --hz-card-w: 400px;
    --hz-perspective: 1400px;
  }
}

@media (max-width: 768px) {
  :root {
    /* 82vw at rest so the selected card, which rides lifted toward the
       camera, lands at roughly 92vw instead of overflowing the screen. */
    --hz-card-w: min(82vw, 410px);
    --hz-perspective: 1150px;
    --hz-ring-top: 44%;
    /* Neighbours sit just off screen at rest across 360-430px viewports,
       so the sides read as clean black instead of sliced card edges. */
    --hz-gap: 60px;
  }


  .title-main { letter-spacing: -0.03em; }

  .stage { perspective-origin: 50% 46%; }
   /* centred in the band between title and compass */

  .hint { bottom: 7rem; }

  .hz-footer {
    padding: 1.25rem;
    flex-direction: column;
    align-items: center;
    gap: 0.9rem;
  }

  .hz-sig, .hz-est { display: none; }

  .compass { width: min(320px, 84vw); }

  /* The card now runs nearly edge to edge, so the falloff must stop where
     the card begins or it would dim the artwork's own edges. */
  .edge-fade { width: 4vw; }

  /* blur(30px) at inset -16px reaches past a phone's screen edge and reads
     as a hard band rather than a bloom. Scaled down to fit. */
  .card-halo {
    inset: -6px;
    filter: blur(18px);
  }

  .card.is-focus .card-halo { opacity: 0.26; }
}

/* Short windows: laptop browsers with big toolbars, phones in landscape.
   With the title sitting under the card, the whole stack has to shrink or it
   runs into the compass - tie the card to viewport height and pull it up. */
@media (max-height: 620px) {
  :root {
    --hz-card-w: min(34vw, 52vh);
    --hz-ring-top: 33%;
  }

  .titles {
    top: calc(var(--hz-ring-top) + var(--hz-card-h) * 0.62 + 2.2rem);
  }

  .title-main {
    font-size: clamp(20px, 3vw, 40px);
    margin-bottom: 0.4rem;
  }

  .title-meta { font-size: 11px; }

  .hz-footer { padding: 1rem 1.5rem; }

  .hint { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .card, .intro, .veil { transition: none !important; }
  .card-halo i { animation: none !important; }
  body.horizon::after { animation: none !important; }
}
