/* The House — a scroll walkthrough.
   Self-contained: this folder owns its own CSS and JS, and shares only the
   two font files with the rest of the workbench. */

@font-face {
  font-family: 'Overused-Grotesk';
  src: url('/assets/fonts/overusedgrotesk-medium.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Overused-Grotesk';
  src: url('/assets/fonts/overusedgrotesk-semibold.woff2') format('woff2');
  font-weight: 600; font-style: normal; font-display: swap;
}

:root {
  color-scheme: dark;

  --ink:    #ffffff;
  --ground: #000000;
  --quiet:  #8a8a8a;
  --faint:  #3a3a3a;
  --line:   #03f6ff;              /* the studio cyan, used only for indicators */

  --font: 'Overused-Grotesk', -apple-system, BlinkMacSystemFont,
          'Helvetica Neue', Arial, sans-serif;

  --gutter: clamp(1.25rem, 4vw, 4rem);
  --lead-len: clamp(2.5rem, 7vw, 7rem);   /* leader line length */

  /* How far you scroll per scene. One viewport of that is the pinned frame,
     the rest is the travel that drives the reveal. */
  --scene-travel: 200vh;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-weight: 500;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

/* ── Fixed chrome ────────────────────────────────────────────────────────── */

.bar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: clamp(1rem, 2.4vh, 1.75rem) var(--gutter);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.bar-title { color: var(--ink); }
.bar-state { color: var(--quiet); font-variant-numeric: tabular-nums; }
.bar-state b { color: var(--line); font-weight: 600; }

.back {
  color: var(--quiet);
  text-decoration: none;
  transition: color 180ms ease;
}
.back:hover { color: var(--ink); }

/* The scene index. Ticks grow and light up as you reach them. */
.rail {
  position: fixed;
  right: var(--gutter);
  top: 50%;
  transform: translateY(-50%);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  align-items: flex-end;
}

.rail button {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--faint);
  font: inherit;
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  transition: color 260ms ease;
}

.rail button::after {
  content: "";
  display: block;
  width: 18px;
  height: 1px;
  background: currentColor;
  transition: width 260ms ease, background 260ms ease;
}

.rail button span {
  opacity: 0;
  transform: translateX(6px);
  transition: opacity 260ms ease, transform 260ms ease;
  white-space: nowrap;
}

.rail button:hover { color: var(--quiet); }
.rail button:hover span { opacity: 1; transform: none; }

.rail button[aria-current="true"] { color: var(--line); }
.rail button[aria-current="true"]::after { width: 40px; }
.rail button[aria-current="true"] span { opacity: 1; transform: none; }

.veil-top {
  position: fixed;
  inset: 0 0 auto 0;
  height: 22vh;
  z-index: 55;
  pointer-events: none;
  background: linear-gradient(to bottom,
              rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.45) 45%, transparent 100%);
}

/* ── Scenes ──────────────────────────────────────────────────────────────── */

/* Each scene is its own stacking context, and because they are positioned and
   in document order, a later scene always paints over an earlier one. That is
   what lets the incoming frame cover the outgoing one as it rises. */
.scene {
  position: relative;
  height: var(--scene-travel);
  isolation: isolate;
}

/* --p is the scene's own scroll progress, 0 to 1, written by house.js. */
.frame {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;

  /* Exit. A pinned frame holds at full strength until the very end of its
     travel and then drops away over the last 15%, so its caption dissolves
     rather than sliding up under the bar while the next scene arrives. */
  opacity: clamp(0, calc((1 - var(--p, 0)) * 6.67), 1);
}

/* Layer order inside a pinned frame:
     1  .plate     the photograph, or the schematic standing in for it
     2  .shade     a permanent bottom gradient, so a caption stays readable
     3  .annos     element indicators
     4  .caption   scene number, title, note
     5  .enter     black that burns off as the frame settles into place

   The gradient that hides what is coming has to live in here rather than as
   one fixed overlay on the viewport. A sticky element creates its own stacking
   context, so nothing inside .frame can raise itself above a fixed layer no
   matter what z-index it is given: a single fixed veil would darken the
   captions along with the plate. Per-frame layers keep the two separable. */
.plate {
  position: absolute;
  inset: 0;
  z-index: 1;
  /* Settles from slightly over-scaled to true size across the scene's travel,
     which is what makes the arrival feel like a move rather than a cut. */
  transform: scale(calc(1.16 - 0.16 * var(--p, 0)));
  transform-origin: 50% 45%;
  will-change: transform;
}

/* With no script --p is never written, and the plate would sit permanently at
   the start of its settle, cropped by 16%. */
.no-js .plate { transform: none; }

.shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 700ms ease;
}

/* Set by house.js once the file actually loads. Until then the scene shows the
   schematic, so a missing plate is a design state rather than a broken image. */
.shot.is-loaded { opacity: 1; }

/* BOTH dimensions have to be stated outright. An <svg> is a replaced element,
   so an `auto` width or height is taken from the viewBox ratio and the opposite
   inset is then dropped as over-constrained: sizing by left/right left it
   anchored to the top of the frame, and sizing by top/bottom gave it an 837px
   width inside a 390px phone. Neither shows up on a desktop viewport, where
   84vw / 1.6 lands near 84vh by coincidence. Set width and height, position
   with left and top, and let preserveAspectRatio center the drawing. */
.schematic {
  position: absolute;
  display: block;
  left: 8vw;
  top: 8vh;
  width: 80vw;      /* stops short of the rail on the right */
  height: 84vh;
  max-width: none;
  transition: opacity 700ms ease;
}

.has-shot .schematic { opacity: 0; }

.schematic .ln {
  fill: none;
  stroke: #cfd6d8;
  stroke-width: 1.1;
  vector-effect: non-scaling-stroke;
  stroke-linecap: square;
}

.schematic .hair {
  stroke: var(--line);
  stroke-width: 0.8;
  opacity: 0.55;
}

/* Every stroke draws itself in when the scene becomes active. The dash values
   are set per path by house.js from its own measured length. */
.schematic path,
.schematic rect {
  stroke-dashoffset: var(--dash, 0);
  transition: stroke-dashoffset 1100ms cubic-bezier(0.22, 1, 0.36, 1);
}

.is-active .schematic path,
.is-active .schematic rect { stroke-dashoffset: 0; }

/* No script, no dash array, so the drawing is simply there. */
.no-js .enter { display: none; }

/* Photographs are pulled toward the page rather than dropped into it: held
   well back from full brightness, desaturated most of the way, and given a
   cool cast so a set of references shot by different people in different light
   still reads as one walkthrough. The labels stay legible over any of them
   because the whole set sits in the same narrow value range. */
.shot {
  object-position: var(--pos, 50% 50%);
  filter: saturate(0.4) contrast(1.16) brightness(0.62);
}

.plate.has-shot::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(155deg,
              rgba(3, 246, 255, 0.16) 0%, rgba(0, 0, 0, 0) 46%,
              rgba(3, 246, 255, 0.08) 100%);
  mix-blend-mode: soft-light;
}

/* ── Element indicators ──────────────────────────────────────────────────── */

/* Permanent scrim. Its job is legibility, not concealment: two gradients, one
   up from the bottom and one in from the left, so the caption always sits on
   something close to black whether the plate underneath is a line drawing or a
   bright photograph. */
.shade {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    linear-gradient(to top,
      rgba(0,0,0,0.97) 0%, rgba(0,0,0,0.88) 16%, rgba(0,0,0,0.55) 38%,
      rgba(0,0,0,0.16) 62%, transparent 82%),
    linear-gradient(to right,
      rgba(0,0,0,0.86) 0%, rgba(0,0,0,0.5) 22%, rgba(0,0,0,0.12) 40%, transparent 56%);
}

/* The reveal. --enter runs 1 to 0 as a frame rises from the bottom of the
   viewport into its pinned position, so a scene arrives out of black with a
   soft edge instead of sliding in fully formed. */
.enter {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  opacity: var(--enter, 0);
  background: linear-gradient(to bottom,
              #000 0%, #000 42%, rgba(0,0,0,0.72) 68%, transparent 100%);
}

.annos { position: absolute; inset: 0; z-index: 3; pointer-events: none; }

.anno {
  position: absolute;
  left: var(--x);
  top: var(--y);
  display: flex;
  align-items: center;
  color: var(--line);
}

/* Everything below hides a thing until its scene is active, and every one of
   those rules is scoped to .js on purpose. The reveal has to be opt-in: if the
   script never runs, or its callbacks are dropped by a throttled context, the
   page must still be a legible walkthrough rather than a black screen. */
.js .anno {
  opacity: 0;
  transition: opacity 520ms ease;
  transition-delay: calc(var(--i, 0) * 130ms + 320ms);
}

.anno[data-dir="l"] {
  flex-direction: row-reverse;
  transform: translateX(-100%);
}

.js .is-active .anno { opacity: 1; }

.dot {
  flex: none;
  box-shadow: 0 0 10px rgba(0,0,0,0.8);
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: 1px solid currentColor;
  background: rgba(0, 0, 0, 0.4);
}

.lead {
  flex: none;
  width: var(--lead-len);
  height: 1px;
  background: currentColor;
  transform-origin: left center;
}

.js .lead {
  transform: scaleX(0);
  transition: transform 620ms cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: calc(var(--i, 0) * 130ms + 380ms);
}

.anno[data-dir="l"] .lead { transform-origin: right center; }
.js .is-active .lead { transform: scaleX(1); }

.tag {
  display: block;
  padding-left: 0.6rem;
  white-space: nowrap;
}

.anno[data-dir="l"] .tag { padding-left: 0; padding-right: 0.6rem; text-align: right; }

.tag b,
.tag i { text-shadow: 0 0 12px rgba(0,0,0,0.95), 0 0 4px rgba(0,0,0,0.9); }

.tag b {
  display: block;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
}

.tag i {
  display: block;
  margin-top: 0.15rem;
  font-style: normal;
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--line);
}

/* ── Captions ────────────────────────────────────────────────────────────── */

.caption {
  position: absolute;
  left: var(--gutter);
  bottom: clamp(3rem, 9vh, 6rem);
  z-index: 4;
  max-width: 30rem;
}

.js .caption {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 700ms ease, transform 700ms cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: 140ms;
}

.js .is-active .caption { opacity: 1; transform: none; }

.idx {
  display: block;
  margin-bottom: 0.9rem;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  color: var(--line);
  font-variant-numeric: tabular-nums;
}

.caption h2 {
  margin: 0;
  font-size: clamp(2.1rem, 6vw, 4.5rem);
  line-height: 1.02;
  font-weight: 600;
  letter-spacing: -0.035em;
  text-transform: uppercase;
}

.caption p {
  margin: 1rem 0 0;
  max-width: 34ch;
  font-size: clamp(0.82rem, 1.1vw, 0.95rem);
  line-height: 1.65;
  color: var(--quiet);
}

/* ── Card scenes ─────────────────────────────────────────────────────────────
   Not every plate wants to be full bleed. A technical sheet is a drawing on a
   light ground, and cropping it to fill a black screen would throw away the
   thing that makes it worth showing. It sits as an object on the page instead,
   pushed right of the caption and desaturated to a printed grey. */

.scene--card .shot {
  position: absolute;
  left: 62%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: auto;
  height: auto;
  max-width: min(30vw, 400px);
  max-height: 72vh;
  object-fit: contain;
  filter: grayscale(1) contrast(1.04) brightness(0.88);
  border: 1px solid #262626;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.9);
}

.scene--card .plate.has-shot::after { display: none; }

/* ── Hero ────────────────────────────────────────────────────────────────── */

.scene--hero { height: 190vh; }

/* Pushed right so the massing does not sit behind the title. */
.scene--hero .schematic { left: 28vw; top: 6vh; width: 57vw; height: 70vh; }

.hero-copy {
  position: absolute;
  left: var(--gutter);
  bottom: clamp(6rem, 18vh, 11rem);
  z-index: 4;
  max-width: 36rem;
  transform: translateY(calc(var(--p, 0) * -60px));
}

.eyebrow {
  margin: 0 0 1.1rem;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--line);
}

.hero-copy h1 {
  margin: 0;
  font-size: clamp(3rem, 13vw, 11rem);
  line-height: 0.9;
  font-weight: 600;
  letter-spacing: -0.045em;
  text-transform: uppercase;
}

.lede {
  margin: 1.4rem 0 0;
  max-width: 38ch;
  font-size: clamp(0.85rem, 1.15vw, 1rem);
  line-height: 1.7;
  color: var(--quiet);
}

.cue {
  position: absolute;
  right: var(--gutter);
  bottom: clamp(2rem, 6vh, 3.5rem);
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--quiet);
  opacity: calc(1 - var(--p, 0) * 2.4);
}

.cue-line {
  display: block;
  width: 44px;
  height: 1px;
  background: var(--line);
  transform-origin: left center;
  animation: cue 2.4s ease-in-out infinite;
}

@keyframes cue {
  0%, 100% { transform: scaleX(0.3); opacity: 0.4; }
  50%      { transform: scaleX(1);   opacity: 1; }
}

/* ── Outro ───────────────────────────────────────────────────────────────── */

.outro {
  position: relative;
  z-index: 50;
  min-height: 80vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 6rem var(--gutter);
  border-top: 1px solid #161616;
}

.outro h2 {
  margin: 0;
  max-width: 20ch;
  font-size: clamp(1.7rem, 4.5vw, 3.2rem);
  line-height: 1.05;
  font-weight: 600;
  letter-spacing: -0.03em;
  text-transform: uppercase;
}

.outro code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.85em;
  color: var(--ink);
}

.back--big {
  margin-top: 2.5rem;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  align-self: flex-start;
}

/* ── Narrow screens ──────────────────────────────────────────────────────── */

@media (max-width: 60rem) {
  .rail { display: none; }
  .schematic,
  .scene--hero .schematic { left: 5vw; top: 14vh; width: 90vw; height: 62vh; }
}

@media (max-width: 40rem) {
  :root { --lead-len: 1.75rem; }

  .scene--card .shot {
    left: 50%;
    top: 42%;
    max-width: 62vw;
    max-height: 46vh;
  }

  .bar-title { display: none; }

  /* Labels are pinned to two columns rather than floating over the plate, so
     they cannot collide with each other or run off the edge on a phone. */
  .annos {
    inset: auto 0 auto 0;
    top: 16vh;
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    padding: 0 var(--gutter);
  }

  .anno {
    position: static;
    transform: none;
    flex-direction: row;
  }

  .anno[data-dir="l"] { flex-direction: row; transform: none; }
  .anno[data-dir="l"] .lead { transform-origin: left center; }
  .anno[data-dir="l"] .tag { padding-left: 0.6rem; padding-right: 0; text-align: left; }

  .tag b { font-size: 0.66rem; }
  .tag i { font-size: 0.58rem; }

  .caption { bottom: 2.5rem; right: var(--gutter); }
  .caption p { display: none; }
}

/* ── Motion ──────────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  /* Everything still reveals, it just arrives rather than travels. */
  .plate { transform: none; }
  .hero-copy { transform: none; }
  .cue-line { animation: none; transform: none; }
  .caption { transform: none; }
  .lead { transition-duration: 1ms; }
  .schematic path,
  .schematic rect { stroke-dashoffset: 0; transition: none; }
  * { transition-duration: 220ms !important; transition-delay: 0ms !important; }
}
