/* ==========================================================================
   QVO.TECH / scene composition
   One continuous scene. There are no stacked sections: a single fixed stage
   is driven by a scroll track, and scroll changes the Core, the camera, the
   typography and the contextual read-out.
   ========================================================================== */

/* ---------------------------------------------------------------- track ---
   The only thing that actually scrolls. It contributes distance, nothing
   visual. One viewport per chapter plus a settle at each end. */

.track {
  position: relative;
  width: 100%;
  height: 560vh;
  pointer-events: none;
}

/* ---------------------------------------------------------------- stage ---
   Everything visible lives here, pinned for the whole scroll. */

.stage {
  position: fixed;
  inset: 0;
  overflow: hidden;
  /* Children opt back in. Keeps the scroll surface untouched. */
  pointer-events: none;
}

/* ---------------------------------------------------------------- frame ---
   Hairline armature. Inherited from the reference's vertical rules, but here
   it closes into a full frame and carries real content (nav, rail, read-out)
   on its edges. Structural, not decorative. */

.frame {
  position: absolute;
  inset: var(--frame);
  z-index: var(--z-frame);
  border: var(--hairline) solid var(--rule);
  pointer-events: none;
}

/* Two interior verticals that set the composition's columns. Desktop only:
   below 768px there is not enough width for them to organise anything. */
.frame__col {
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--hairline);
  background: var(--rule);
  display: none;
}

/* Positioned to divide the three fields, not to decorate. The left rule closes
   the copy column and the right one opens the terms panel, so neither ever
   crosses text. At 22% the left rule ran straight through the headline. */
.frame__col--a { left: 46%; }
.frame__col--b { left: 78%; }

@media (min-width: 768px) {
  .frame__col { display: block; }
}

/* ----------------------------------------------------------------- core ---
   The QVO Core. One persistent object for the entire experience. */

.core {
  position: absolute;
  inset: 0;
  z-index: var(--z-core);
  display: block;
  width: 100%;
  height: 100%;
}

/* Video renderer.
   Full bleed, because the master is a lit scene rather than an isolated object:
   it has an architectural interior, a floor and a cast shadow. Fitting a scene
   into a box in the right-hand field would read as a photograph pasted on the
   page. A scene has to behave like a backdrop. */
.core-video {
  position: absolute;
  inset: 0;
  z-index: var(--z-core);
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Bias the crop so the object clears the headline instead of touching it. */
  object-position: 34% center;
  pointer-events: none;
  /* The desaturation that settles the render's warm cream into this cool palette
     is baked into the encode, not applied here. As a CSS filter it re-processed
     every pixel of the surface on every frame, for a result that never changes.
     See prepare-core-video.mjs. */
}

/* Veil.
   Asymmetric on purpose. A global scrim would dull the one thing worth looking
   at; this guarantees a field for the copy column on the left and leaves the
   right side, where the object lives, completely untouched. */
.veil {
  position: absolute;
  inset: 0;
  z-index: var(--z-veil);
  pointer-events: none;
  background:
    /* Right edge: a much weaker lift, just enough to seat the terms panel.
       It sits over the brightest wall in the scene and had nothing at all. */
    linear-gradient(
      270deg,
      rgb(var(--paper-rgb) / 0.62) 0%,
      rgb(var(--paper-rgb) / 0.3) 10%,
      rgb(var(--paper-rgb) / 0) 20%
    ),
    /* Left column: the copy field. */
    linear-gradient(
      100deg,
      rgb(var(--paper-rgb) / 0.95) 0%,
      rgb(var(--paper-rgb) / 0.78) 26%,
      rgb(var(--paper-rgb) / 0) 58%
    );
}

/* Below 768px the copy sits under the object rather than beside it, so the veil
   runs bottom-up instead of left-in. */
@media (max-width: 767px) {
  .veil {
    background: linear-gradient(
      to top,
      rgb(var(--paper-rgb) / 0.96) 0%,
      rgb(var(--paper-rgb) / 0.84) 34%,
      rgb(var(--paper-rgb) / 0) 62%
    );
  }

  .core-video {
    /* A portrait viewport against a landscape master is the one case where full
       bleed fails. Cover crops so hard the object stops being an object and
       becomes abstract glass texture; contain would letterbox the render's cream
       field against the page and show a seam. So: a landscape band in the upper
       region, cover-cropped within it, faded out top and bottom so it dissolves
       into the paper instead of ending on an edge. */
    inset: 7% 0 auto 0;
    height: 44vh;
    object-position: center center;
    -webkit-mask-image: linear-gradient(
      to bottom,
      transparent 0%,
      #000 16%,
      #000 64%,
      transparent 100%
    );
    mask-image: linear-gradient(
      to bottom,
      transparent 0%,
      #000 16%,
      #000 64%,
      transparent 100%
    );
  }
}

/* ------------------------------------------------------------------ nav ---
   Single line, capped height. */

/* Starts at the frame edge rather than the viewport edge, so the CTA sits
   inside the frame instead of breaking out through its top and right rules. */
.nav {
  position: absolute;
  top: var(--frame);
  left: var(--frame);
  right: var(--frame);
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 64px;
  padding: 0 var(--s-6);
}

.nav__mark {
  display: inline-flex;
  align-items: baseline;
  gap: 0.1em;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  font-weight: var(--w-medium);
  letter-spacing: 0.06em;
  pointer-events: auto;
}

.nav__mark span {
  color: var(--ink-35);
}

.nav__cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  /* Solid field, not a ghost button. The Core draws behind it, and a
     transparent CTA over a point cloud cannot guarantee label contrast. */
  background: var(--paper);
  border: var(--hairline) solid var(--rule-strong);
  border-radius: var(--radius);
  pointer-events: auto;
  white-space: nowrap;
  transition: border-color var(--d-fast) var(--ease),
              background-color var(--d-fast) var(--ease);
}

.nav__cta:hover {
  border-color: var(--ink);
  background: var(--paper-raised);
}

.nav__cta:active {
  transform: translateY(1px);
}

/* ----------------------------------------------------------------- rail ---
   Functional chapter navigation and progress read-out. This is the only
   place the signal colour appears in the DOM. */

.rail {
  position: absolute;
  left: calc(var(--frame) + var(--s-6));
  bottom: calc(var(--frame) + var(--s-6));
  z-index: var(--z-rail);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  pointer-events: auto;
}

.rail__item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  color: var(--ink-35);
  transition: color var(--d-base) var(--ease);
}

.rail__item:hover {
  color: var(--ink-70);
}

.rail__item[aria-current='true'] {
  color: var(--ink);
}

/* The marker is a tick that extends when active. Length encodes state, so no
   coloured dot is needed. */
.rail__tick {
  width: 14px;
  height: var(--hairline);
  background: currentColor;
  transition: width var(--d-base) var(--ease),
              height var(--d-base) var(--ease),
              background-color var(--d-base) var(--ease);
}

/* Two pixels when active, not one. The accent is the only colour in the system
   and at a single hairline it read as dark grey on a standard display: the hue
   only survived at 2x. Doubling the height is what lets it actually register. */
.rail__item[aria-current='true'] .rail__tick {
  width: 40px;
  height: 2px;
  background: var(--signal);
}

/* ------------------------------------------------------------- chapters ---
   All chapters occupy the same box. Scroll crossfades between them, so the
   composition never reflows and the Core stays the constant. */

.chapters {
  position: absolute;
  inset: 0;
  z-index: var(--z-type);
  display: grid;
  place-items: end start;
  /* Bottom padding clears the rail, which is four rows tall plus its own
     offset. At 8rem the lead paragraph was landing on the first rail item. */
  padding: 0 calc(var(--frame) + var(--s-6)) calc(var(--frame) + 11.5rem);
}

@media (min-width: 768px) {
  .chapters {
    place-items: center start;
    padding: 0 calc(var(--frame) + var(--s-6));
  }
}

.chapter {
  grid-area: 1 / 1;
  max-width: 30rem;
  /* Driven from scene.js via a single custom property per chapter, so the
     browser only ever composites opacity and transform. */
  opacity: var(--reveal, 0);
  transform: translateY(calc((1 - var(--reveal, 0)) * 18px));
  transition: opacity var(--d-base) linear, transform var(--d-base) var(--ease);
  will-change: opacity, transform;
}

/* Deliberately NOT visibility: hidden on the faded chapters.
 *
 * visibility: hidden removes an element from the accessibility tree, so with it
 * a screen reader reached exactly one chapter out of five and the other four
 * simply did not exist. Opacity is purely visual and leaves everything in the
 * tree, so all five chapters are readable in DOM order as an ordinary document
 * while only one is ever legible on screen. The chapters hold no focusable
 * elements, so there is nothing to trap; pointer-events alone is enough to stop
 * faded copy intercepting selection. */
.chapter[data-active='false'] {
  pointer-events: none;
}

/* Visually hidden, still announced. Standard clip pattern rather than
   display:none, which would defeat the point. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.chapter__title {
  font-size: var(--t-display);
  font-weight: var(--w-light);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-display);
  text-wrap: balance;
}

.chapter__lead {
  margin-top: var(--s-6);
  max-width: 26rem;
  font-size: var(--t-lead);
  line-height: var(--lh-lead);
  color: var(--ink-70);
}

/* Opening chapter carries the wordmark scale; the rest are one step down.
   Its measure widens with the scale so the headline still lands in two lines. */
/* Measure stays at the shared 30rem. The wider 36rem measure pushed the copy
   into the Core's field once the Core was scaled up properly. */
.chapter--open {
  /* Visible without JavaScript. The scene is progressive: if the module never
     runs, the opening statement and the CTA are still readable rather than
     leaving a frame full of invisible copy. */
  --reveal: 1;
}

.chapter--open .chapter__title {
  font-size: var(--t-display-open);
}

/* -------------------------------------------------------------- context ---
   Contextual detail for the active capability. Sits on the frame's right
   column at desktop, hidden on mobile where there is no room for a second
   reading order. */

.context {
  position: absolute;
  top: 50%;
  right: calc(var(--frame) + var(--s-6));
  z-index: var(--z-type);
  display: none;
  width: 15rem;
  transform: translateY(-50%);
}

/* 1280 rather than 1024: at 1024 the panel and the Core were competing for the
   same band of the right-hand field. */
@media (min-width: 1280px) {
  .context { display: block; }
}

.context__list {
  display: grid;
  gap: var(--s-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.context__item {
  grid-area: 1 / 1;
  opacity: var(--reveal, 0);
  transition: opacity var(--d-base) linear;
}

.context__row {
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-3) 0;
  border-top: var(--hairline) solid var(--rule);
  color: var(--ink-50);
}

.context__row span:last-child {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------- grain ---
   Fixed, pointer-events none, never on a scrolling container. */

.grain {
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: 0.22;
  mix-blend-mode: multiply;
  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.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}

@media (prefers-color-scheme: dark) {
  :root:not(.has-core-video) .grain {
    mix-blend-mode: screen;
    opacity: 0.14;
  }
}

/* Grain is dropped entirely on the video path. A full-viewport blend-mode layer
   forces the compositor to re-blend the whole screen against the video surface
   on every seeked frame, and the render already carries its own film grain, so
   it was paying a per-frame cost for something nobody could see. */
:root.has-core-video .grain {
  display: none;
}

/* Grain is texture, not information. Drop it when transparency is reduced. */
@media (prefers-reduced-transparency: reduce) {
  .grain { display: none; }
}
