/* Adriano Jewelry: the landing journey.
   Loaded by index.html alone; site.css stays the shared shell for every page.

   The landing page is now one continuous scroll story run by home.js: a black
   room holding the ring box, a sticky viewport the story plays in, text beats
   that enter and leave with scroll, a white passage through the inside of the
   diamond, and a finale on black. This sheet is the costume for all of it.

   THE GROUND IS THE SAME GROUND THE WHOLE SITE STANDS ON, which it was not
   until v0.3.7. This sheet carried the dark half of the ramp alone while the
   rest of the site was paper; site.css is black silk over the same #050506
   now, so the tokens below are the shared ones restated rather than a second
   palette, and the film simply opens on the ground the reader was already on.
   The rule from site.css still holds: R, G and B equal in every token, no hue
   anywhere, so the gold of the marque, the fire in the stone and the
   photographed pieces are the only colour on the page.

   THE SILK IS BEHIND THE FILM, not beside it. home.js drives the .velvet
   layer's opacity from the same scroll progress everything else on this page
   comes from: dimmed to a fraction while the dark room plays, so it reads as
   the wall the box stands against rather than as wallpaper competing with it;
   zero from the moment the reader crosses the table until the crystal room
   collapses, because in there the ground is white and belongs to the shader;
   and full strength by the time the film parks and the finale scrolls up. The
   canvas is transparent over it, which is why the room's own dome is gone.

   CONTRAST FLOOR, DARK GROUND (WCAG 2.1 relative luminance, measured):
   on #050506: --pearl #ececec 17.3:1, --silver #c9c9c9 12.3:1,
   --fog #9a9a9a 7.3:1, and --hairline (rgba(236,236,236,0.42) composited)
   3.55:1. Text needs 4.5:1 and UI hairlines 3:1, so every painted value
   clears with margin. The 0.42 alpha is not taste: at site.css's 0.34 a
   light hairline composites to 2.68:1 on this ground and fails the UI
   floor. A translucent light line on ink needs MORE alpha than the light
   ground's dark lines carry, the mirror of the lesson recorded in site.css.
   The same tokens restyle the footer at the foot of this page; on its
   slightly lifted #0a0a0b ground the tightest value is --fog at 7.0:1.

   INSIDE THE DIAMOND the ground is white and the ink beats carry their own
   two values: --crystal-fg 15:1 and --crystal-muted at worst 5.2:1 against the darkest
   patch the crystal shader is allowed to put behind text. The shader's
   shading terms sum to a floor of 0.52 before tonemapping, which lands at
   #ea under the film's inside exposure, still clear of the #e6 those
   ratios were measured against; the ink beats also carry their own white
   radial clearing over the showcase. The coda after the collapse returns
   to the ink ground above.

   Every measurement from a 320px phone to a wide monitor is clamp()ed, the
   outer paddings take env(safe-area-inset-*), and the portrait composition
   (text above the prop, prop in the lower field) switches on the same
   aspect threshold home.js uses for its camera: width/height under 0.9. */

:root {
  --ink: #050506;
  --ink-lifted: #0a0a0b;
  --pearl: #ececec;
  --silver: #c9c9c9;
  --fog: #9a9a9a;
  --hairline: rgba(236, 236, 236, 0.42);
  --hairline-faint: rgba(236, 236, 236, 0.16);
  --wash: rgba(236, 236, 236, 0.06);

  /* INSIDE THE DIAMOND the ground is white, so the two values that paint on it
     are the only ones on this page that are not from the dark ramp above.
     They used to be read straight out of site.css as --graphite and --smoke,
     which was right for exactly as long as that file WAS the light half of the
     ramp. Since v0.3.7 it is the dark half, shared with every other page, and
     those names now resolve to #ECECEC and #C9C9C9: white type on a white
     room. So the white world carries its own two, and the ratios quoted in the
     note above (15:1 and 5.2:1 against the darkest patch the crystal shader is
     allowed to put behind text) are the ratios for these. */
  --crystal-fg: #1a1a1a;
  --crystal-muted: #5c5c5c;
}

/* The landing chrome exists only when the journey can run. With JavaScript
   off nothing below is displayed and the page is the footer-only page it has
   been since v0.2.1, on the shared ground: site.css owns the black and the
   colour-scheme for every page now, so the two overrides that used to live
   here said nothing the shared sheet was not already saying. */

.loader,
.nav,
.journey,
.finale {
  display: none;
}

/* ---------------------------------------------------------------- loader */

html.js .loader {
  display: flex;
}

/* The loading room does not fade out as one thing, because one part of it
   has somewhere to be. The ground and the name go; the hairline flies to
   where the box's lid seam actually is on screen and the lid cracks on it.
   home.js measures that target by projecting the box's own rim through the
   film's opening camera, and writes it here as --seam-*. */
.loader {
  position: fixed;
  inset: 0;
  z-index: 60;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.65rem;
  visibility: visible;
  transition: visibility 0s 1300ms;
}

.loader::before {
  content: "";
  position: absolute;
  inset: 0;
  /* Behind the name and the bar, but still inside the loader's own stacking
     context, so it covers the canvas fading up underneath. */
  z-index: -1;
  background: var(--ink);
  transition: opacity 700ms var(--ease-soft);
}

.loader.is-done {
  visibility: hidden;
  pointer-events: none;
}

.loader.is-done::before {
  opacity: 0;
}

.loader.is-done .loader__mark,
.loader.is-done .loader__sub {
  /* The entrance animations below are filled `both`, and a filling animation
     outranks a plain declaration in the cascade; without dropping them here
     the name simply refuses to leave. */
  animation: none;
  opacity: 0;
  transition: opacity 440ms var(--ease-soft);
}

.loader.is-done .loader__bar {
  /* translate, then rotate, then scale along the bar's own now-tilted axis,
     which is what lands it flush on a seam seen at an angle. */
  transform: translate(var(--seam-dx, 0px), var(--seam-dy, 0px))
    rotate(var(--seam-rot, 0deg)) scaleX(var(--seam-sx, 1));
  opacity: 0;
  transition: transform 900ms cubic-bezier(0.16, 0.84, 0.28, 1),
    opacity 400ms var(--ease-soft) 780ms;
}

/* No flight without motion: the hairline simply goes. */
@media (prefers-reduced-motion: reduce) {
  .loader.is-done .loader__bar {
    transform: none;
    transition: opacity 320ms linear;
  }
}

.loader__mark {
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(2.3rem, 7vw, 3.4rem);
  font-weight: 400;
  letter-spacing: 0.03em;
  color: var(--pearl);
}

/* The name settles in the way the beats will: rising a little and
   condensing out of wider tracking. Clock driven, so it is gated on motion
   being welcome rather than flattened after the fact. */
@media (prefers-reduced-motion: no-preference) {
  .loader__mark {
    animation: loader-mark 1100ms var(--ease-out) both;
  }

  .loader__sub {
    animation: loader-sub 1100ms var(--ease-out) 120ms both;
  }
}

@keyframes loader-mark {
  from {
    opacity: 0;
    transform: translateY(12px);
    letter-spacing: 0.09em;
  }
  to {
    opacity: 1;
    transform: translateY(0);
    letter-spacing: 0.03em;
  }
}

@keyframes loader-sub {
  from {
    opacity: 0;
    letter-spacing: 0.6em;
  }
  to {
    opacity: 1;
    letter-spacing: 0.42em;
  }
}

.loader__sub {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 400;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  color: var(--fog);
}

.loader__bar {
  width: min(13rem, 56vw);
  height: 1px;
  margin-top: 1.4rem;
  background: var(--hairline-faint);
  overflow: hidden;
}

.loader__fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--pearl);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 450ms var(--ease-soft);
}

/* -------------------------------------------------------------------- nav */

html.js .nav {
  display: flex;
}

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 50;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: max(0.65rem, env(safe-area-inset-top))
    max(clamp(1rem, 3vw, 2rem), env(safe-area-inset-right)) 0.65rem
    max(clamp(1rem, 3vw, 2rem), env(safe-area-inset-left));
  /* A breath of scrim so the wordmark holds against whatever the render
     puts behind it; it fades to nothing a little way down. */
  background: linear-gradient(rgba(5, 5, 6, 0.8), rgba(5, 5, 6, 0));
  transform: translateY(-110%);
  opacity: 0;
  transition: transform 600ms var(--ease-out), opacity 500ms ease,
    background-color 400ms ease;
  /* The costume switches with the ground: these are the dark-room values,
     and .nav--ink swaps them while the reader is inside the stone. */
  --nav-fg: var(--pearl);
  --nav-muted: var(--silver);
  --nav-dim: var(--hairline);
  --nav-wash: var(--wash);
  --nav-solid: var(--pearl);
  --nav-solid-fg: #0a0a0b;
}

.nav.is-boot {
  transform: translateY(0);
  opacity: 1;
}

.nav.is-hidden {
  transform: translateY(-110%);
  opacity: 0;
}

/* A hidden nav must not be a place focus can go. The bar is only translated
   and faded, so its wordmark and both pills stayed in the tab order the whole
   way down the track: a keyboard reader who had scrolled tabbed into a control
   sitting off the top of the screen at zero opacity, and because the bar is
   fixed there was nothing they could scroll to bring it back.

   :focus-visible, not :focus-within. Clicking a pill focuses it, and
   :focus-within would pin the bar open for the rest of the film after one
   ordinary click that did not navigate. :has() carries the specificity of its
   argument, which makes this (0,2,0), level with .nav.is-hidden, so it has to
   sit after it to win. */
.nav:has(:focus-visible) {
  transform: translateY(0);
  opacity: 1;
}

.nav--ink {
  background: linear-gradient(rgba(244, 244, 244, 0.88), rgba(244, 244, 244, 0));
  --nav-fg: var(--crystal-fg);
  --nav-muted: #3a3a3a;
  --nav-dim: rgba(26, 26, 26, 0.55);
  --nav-wash: rgba(26, 26, 26, 0.05);
  --nav-solid: #1a1a1a;
  --nav-solid-fg: #f4f4f4;
}

.nav__brand {
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 3.5vw, 1.45rem);
  font-weight: 400;
  letter-spacing: 0.02em;
  /* 44px floor, the same tap target the rest of the site holds, carried on
     the line box rather than by flexing this link. The tail span is preceded
     by an ordinary space, and a flex box would make that span a separate flex
     item and strip the space, rendering "AdrianoJewelry": the exact trap
     index.html spells out on the booking button, which is why that one uses a
     non-breaking space. Half-leading keeps the glyphs centred, so the wordmark
     does not shift inside the bar. */
  line-height: 44px;
  color: var(--nav-fg);
  text-decoration: none;
  white-space: nowrap;
  transition: color 400ms ease, opacity 0.25s ease;
}

.nav__brand:hover,
.nav__brand:focus-visible {
  opacity: 0.75;
}

.nav__actions {
  display: flex;
  align-items: center;
  gap: clamp(0.4rem, 1.5vw, 0.6rem);
}

.nav__btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 44px, not 40: the floor every other control on the site already holds. */
  min-height: 44px;
  padding: calc(0.42rem + var(--caps-drop)) clamp(0.8rem, 2.5vw, 1.25rem)
    calc(0.42rem - var(--caps-drop));
  border: 1px solid var(--nav-dim);
  border-radius: 999px;
  color: var(--nav-fg);
  font-family: var(--font-body);
  font-size: clamp(0.58rem, 1.9vw, 0.68rem);
  font-weight: 400;
  white-space: nowrap;
  text-decoration: none;
  background: var(--nav-wash);
  cursor: pointer;
  transition: border-color 0.25s ease, background-color 0.4s ease,
    color 0.4s ease, opacity 0.25s ease, transform 0.25s ease;
}

.nav__btn:hover,
.nav__btn:focus-visible {
  border-color: var(--nav-fg);
}

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

.nav__btn--book {
  border-color: transparent;
  background: var(--nav-solid);
  color: var(--nav-solid-fg);
}

.nav__btn--book:hover,
.nav__btn--book:focus-visible {
  border-color: transparent;
  opacity: 0.85;
}

/* ---------------------------------------------------------------- journey */

html.js .journey {
  display: block;
}

.journey {
  /* The scroll track: eighteen screens of story (v0.4.8; twenty-two while
     the showcase inside the diamond was a four-formation choreography,
     and the four screens come back off that one chapter, which the reader
     asked to be a single scattered field passed in about three seconds.
     Every range OUTSIDE the diamond is rescaled by 11/9 in place, so the
     dark room, the dive and the coda keep the exact scroll length they
     were tuned at and only the showcase gets shorter.) Everything visible
     lives in the sticky pin; this element only supplies the road. */
  height: 1800vh;
  position: relative;
}

/* A thumb is not a wheel (v0.4.4, asked for as making the scrolling on
   mobile "like x3 powerful"). Eighteen screens of road is cut for a
   scroll wheel's cheap centimetres; on a phone the same road was a long
   drag of swipes through the same film. Every window in the film is a
   FRACTION of the track rather than a length, so shortening the road is
   the entire change: one swipe advances the story three times as far,
   and no beat, chapter or beam moves by a thousandth. */
@media (pointer: coarse) {
  .journey {
    height: 600vh;
  }
}

.journey__pin {
  position: sticky;
  top: 0;
  /* dvh, and the svh that stood here was the whole of the phone bug report
     (v0.5.4). svh sized the film to the SMALLEST viewport on the argument
     that the address bar must not resize the render mid-scroll, and that
     the sliver it leaves under the pin is the same near-black as the page
     ground. Both halves of that are wrong on a real phone, where the bar is
     HIDDEN for most of a scroll and the visible viewport is 60 to 110px
     taller than the pin: inside the diamond the canvas is white, so the
     sliver is a hard black band across the bottom of the whole chapter; and
     portrait composition seats the props in the lower field, so the box and
     the coda stone ended at the canvas's edge and were cut by an invisible
     line with black below it, which is exactly what the reader photographed.
     dvh keeps the film's frame equal to what is actually visible in every
     bar state; the resizes it costs are coalesced into the frame step in
     home.js, which is where every resize on this page lands anyway. */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

.journey__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* First frames fade up over the ink ground, so there is no black flash
     and no pop. */
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}

.journey__canvas.is-ready {
  opacity: 1;
}

/* A quiet photographic vignette over the dark-room chapters, driven by the
   film (--vig): it holds the corners down around the box and the stone and
   lifts away entirely inside the white of the diamond.

   Both of these are viewport-sized gradients whose opacity the film rewrites
   on EVERY frame of a scroll, and an opacity change is only free if the thing
   changing has a layer of its own. Without the promotion each frame of the
   film re-rasterises two full-screen gradients on the CPU alongside
   everything the canvas is already doing. Two layers is a few megabytes; the
   raster was a large part of a core for the length of the film. */
.journey__vig {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(
    120% 95% at 50% 44%,
    transparent 55%,
    rgba(0, 0, 0, 0.42) 100%
  );
  opacity: 1;
  will-change: opacity;
}

.journey__veil {
  position: absolute;
  inset: 0;
  z-index: 2;
  /* Not one flat white: a breath of falloff toward the corners, so the
     flash reads as light blooming over the lens rather than a page. */
  background: radial-gradient(120% 95% at 50% 46%, #ffffff 58%, #f2f2f2 100%);
  opacity: 0;
  pointer-events: none;
  will-change: opacity;
}

/* WebGL off: the journey collapses to one quiet dark card carrying the
   fallback line and the hero copy, and the page continues to the finale and
   the footer as ordinary scrolling content. */
.journey.is-fallback {
  height: auto;
}

.journey.is-fallback .journey__pin {
  position: static;
  height: auto;
  min-height: 72svh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.journey.is-fallback .journey__canvas,
.journey.is-fallback .journey__vig,
.journey.is-fallback .journey__veil,
.journey.is-fallback .beat,
.journey.is-fallback .journey__cue {
  display: none;
}

/* --silver, not --fog, in the four places below. Everything else on this page
   paints over the RENDER, which is a picture the film controls; the finale and
   the WebGL-off card paint straight onto the site's silk, which is a texture
   with a bright end. --fog measures 7.3:1 on flat ink and 3.5:1 on the silk's
   brightest patch, so it stopped clearing the floor the moment the ground
   stopped being flat. This is the same move site.css makes with its muted
   step, for the same measured reason. */
.journey.is-fallback .journey__fallback {
  display: block;
  margin: 0;
  padding: 0 1.5rem;
  max-width: 30rem;
  text-align: center;
  color: var(--silver);
  font-size: 0.9rem;
  line-height: 1.8;
}

.journey__fallback {
  display: none;
}

/* The scroll cue. Appears only if the reader has not moved four seconds
   after the room opens; the first scroll retires it.

   THE `hidden` HERE HAS TO BE HONOURED, and honouring it is not automatic.
   The UA sheet says `[hidden] { display: none }`, but that is one plain
   declaration among equals: the author `display: flex` below is later and
   more specific, so it wins, and the element the markup marked hidden is laid
   out anyway. Which would be a harmless invisibility (opacity 0) were it not
   for the hairline's `infinite` animation. An animation that never ends
   keeps the compositor awake for the LIFE OF THE PAGE, at the display's
   refresh rate, whether or not anything it draws is visible. On a page whose
   stacking context also holds a full-viewport WebGL canvas, that is the GPU
   process recompositing the whole frame sixty times a second, forever, for a
   cue most readers never see. Measured on this machine: two thirds of a core
   and a busy GPU on a page that had drawn exactly one frame and gone to
   sleep. So the attribute is restored here, and the animation below is scoped
   to the state that actually wants it. */
.journey__cue[hidden] {
  display: none;
}

.journey__cue {
  position: absolute;
  z-index: 6;
  left: 50%;
  bottom: max(4svh, calc(env(safe-area-inset-bottom) + 1.2rem));
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  font-weight: 400;
  letter-spacing: 0.5em;
  text-indent: 0.5em;
  color: var(--fog);
  opacity: 0;
  transition: opacity 900ms var(--ease-soft);
  pointer-events: none;
}

.journey__cue.is-shown {
  opacity: 1;
}

.journey__cue.is-gone {
  opacity: 0;
  transition-duration: 350ms;
}

.journey__cue-line {
  width: 1px;
  height: 2.2rem;
  background: linear-gradient(var(--hairline), transparent);
  transform-origin: top center;
}

/* Only while the cue is actually up. An `infinite` animation is a standing
   order to the compositor, so it belongs to the one state that wants it and
   not to the stylesheet at large; the moment the cue is retired the page goes
   quiet again. Reduced motion gets the hairline standing still.

   `will-change` is not a hint here, it is the whole cost. A transform
   animated on an element that shares its parent's layer repaints THAT LAYER,
   and this hairline's layer is the pin, which carries a viewport-sized
   radial gradient. So a one-pixel line pulsing in the corner was rasterising
   a full-screen vignette sixty times a second: measured at 1.3 cores for a
   1px by 2.2rem hairline. Given its own layer it costs one composited
   transform instead, and it is asked for only while the animation runs. */
@media (prefers-reduced-motion: no-preference) {
  .journey__cue.is-shown:not(.is-gone) .journey__cue-line {
    /* FIVE, not `infinite`. A running animation holds the whole compositor
       open at the display's refresh rate for as long as it lasts, and that
       bill is the same whether it drives a full-screen film or a hairline:
       measured here at two thirds of a core, for ever, on a page that is
       otherwise asleep. Five drops is about eleven seconds of asking, which
       is longer than any reader needs to be told which way the page goes,
       and after it the hairline simply stands there and the machine is
       quiet again. A cue that pulses for ever is nagging anyway. */
    animation: cue-drop 2.2s var(--ease-soft) 5;
    will-change: transform, opacity;
  }
}

@keyframes cue-drop {
  0% {
    transform: scaleY(0);
  }
  45% {
    transform: scaleY(1);
  }
  100% {
    transform: scaleY(1);
    opacity: 0;
  }
}

/* ------------------------------------------------------------------ beats */

/* A beat is one passage of copy tied to one stretch of scroll. home.js
   writes a single number onto it (--k, its way through enter, hold and
   exit) and everything else is derived here, so the same arithmetic runs
   every line on the page and the whole system scrubs backwards for free.

   Lines wrapped in .bl are masked and rise into view; --i staggers them.
   The eyebrow and the sub take the same envelope without the mask. */

/* Beats are gated by opacity alone, never visibility or display: a line
   outside its window computes opacity 0 from --k, so the eye sees one beat
   at a time while a screen reader keeps the whole story, in order. */
.beat {
  --k: 0;
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  padding: 0 max(clamp(1.25rem, 6vw, 5.5rem), env(safe-area-inset-left));
  pointer-events: none;
}

.beat--low {
  justify-content: flex-end;
  padding-bottom: clamp(3.5rem, 12svh, 8rem);
}

.beat--center {
  align-items: center;
  text-align: center;
  padding-right: max(clamp(1.25rem, 6vw, 5.5rem), env(safe-area-inset-right));
}

.bl {
  display: block;
  overflow: hidden;
  /* The mask must not shave descenders while the line settles. */
  padding-bottom: 0.08em;
  margin-bottom: -0.08em;
  /* The exit lift rides the MASK, not the line inside it. The mask exists
     for the entrance alone (the line rises into view from below its own
     box); an exit translated on the inner line lifts the glyphs up through
     the mask's top edge, and a half-faded, blurred line with its ascenders
     sheared off on a straight cut is a glitch, visible the moment anything
     lit passes behind the copy, which portrait framing guarantees. Moving
     the exit here carries mask and line together, so nothing is clipped.
     The exit is unstaggered at this level; the lines keep their stagger in
     opacity and blur below, which is where the eye reads it. */
  --xw: clamp(0, calc((var(--k) - 0.78) / 0.17), 1);
  --xxw: calc(var(--xw) * var(--xw) * (3 - 2 * var(--xw)));
  transform: translateY(calc(var(--xxw) * -46%));
}

.bl__in {
  display: block;
  /* The whole animation, derived from the beat's one number:
     --e enters this line (staggered by --i), --x exits it. Both are pushed
     through a smoothstep so the motion has the site's ease even though the
     value arrives linear from the scroll. The entrance is quick and the
     exit late, so most of every window is spent legible and still. */
  --e: clamp(0, calc((var(--k) - 0.02 - var(--i) * 0.045) / 0.15), 1);
  --x: clamp(0, calc((var(--k) - 0.78 - var(--i) * 0.015) / 0.17), 1);
  --ee: calc(var(--e) * var(--e) * (3 - 2 * var(--e)));
  --xx: calc(var(--x) * var(--x) * (3 - 2 * var(--x)));
  transform: translateY(calc((1 - var(--ee)) * 114% - var(--xx) * 46%));
  opacity: calc(var(--ee) * (1 - var(--xx)));
  filter: blur(calc(var(--xx) * 5px));
}

/* A line inside a mask leaves the exit travel to the mask above; the lines
   that stand unwrapped (the eyebrow and the sub have no mask to shear them)
   keep the combined transform. */
.bl > .bl__in {
  transform: translateY(calc((1 - var(--ee)) * 114%));
}

/* THE DARK BEATS CARRY DARK HALOS, the mirror of the white ones the ink
   beats already wear, and a phone is why (v0.4.5). On a wide frame the words
   always stand on black silk: the camera's sx slides the prop to the other
   half, so the type never shares a pixel with it. Portrait stacks the words
   OVER the prop instead, and there are frames where a bright surface passes
   straight through the copy: measured at p 0.30 at 320x800, the sub sat on
   the box's lit rim, fog grey on mid grey, and could not be read. A halo
   hugs the glyphs, costs the picture nothing on black (black glow on black
   silk is invisible by construction), and darkens the local backdrop just
   enough that the same line reads wherever the film happens to be. The ink
   beats override every one of these with their own white set. */
.beat__eyebrow {
  margin: 0 0 1rem;
  font-family: var(--font-mono);
  font-size: clamp(0.56rem, 1.8vw, 0.64rem);
  font-weight: 400;
  letter-spacing: 0.34em;
  text-indent: 0.34em;
  color: var(--fog);
  text-shadow: 0 0 16px rgba(0, 0, 0, 0.6), 0 1px 5px rgba(0, 0, 0, 0.5);
}

/* The eyebrow's own entrance: it condenses out of wider tracking as it
   arrives, which no other element does: the one flourish of the system. */
.beat__eyebrow .bl__in {
  letter-spacing: calc(0.34em + (1 - var(--ee)) * 0.3em);
}

/* THE DISPLAY SIZE IS CAPPED BY THE FRAME'S HEIGHT AS WELL AS ITS WIDTH,
   which is one min() and covers a case no width clamp can see: a phone held
   LANDSCAPE. At 844x390 the vw term sets this line at 50px, "Held by four
   claws," runs 420px of an 844px frame, and the beat is vertically centred,
   which is exactly where the ring stands; the second line was set through
   polished metal even after home.js had carried the subject off centre.
   9.5svh is the largest cap that changes nothing anywhere else: at 900px of
   height it computes to 85px against a clamp that has already stopped at
   60.8, at 844 (a phone in portrait) to 80 against 42, and at 800 to 76
   against 32.8, so every frame the site was tuned on keeps its exact type
   and only the short ones pay. On the 390px-tall landscape frame it lands at
   37px and the line comes in at 310px, clear of the shank. */
.beat__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: min(clamp(2.05rem, 4.6vw + 0.7rem, 3.8rem), 9.5svh);
  font-weight: 400;
  line-height: 1.06;
  letter-spacing: 0.01em;
  color: var(--pearl);
  text-wrap: balance;
  text-shadow: 0 0 30px rgba(0, 0, 0, 0.6), 0 1px 8px rgba(0, 0, 0, 0.5);
}

.beat__sub {
  margin: 1.1rem 0 0;
  max-width: 24rem;
  font-size: clamp(0.86rem, 2.6vw, 0.96rem);
  font-weight: 400;
  line-height: 1.65;
  color: var(--fog);
  text-shadow: 0 0 20px rgba(0, 0, 0, 0.6), 0 1px 6px rgba(0, 0, 0, 0.5);
}

/* Ink beats stand on the white inside of the stone, over the drifting
   pieces: a soft white halo keeps the words legible across whatever is
   passing behind them without drawing a card around them, and a wide
   radial clearing under the whole block veils the pieces right behind the
   words. White on white, so it never reads as a shape, only as room. */
.beat--ink::before {
  content: "";
  position: absolute;
  /* Under the beat's own text, above the canvas the pieces render in: the
     beat's z-index makes it a stacking context, so -1 stays inside it. */
  z-index: -1;
  left: 50%;
  top: 50%;
  /* SMALLER AND MUCH WEAKER THAN IT WAS (v0.4.0), and weaker again now that
     the field itself gets out of the way (v0.4.1). At 104vw by 74svh and 0.97
     white at its middle this was not a clearing, it was a PANEL: it covered
     most of the frame, it sat over the gallery by construction, and every
     piece that came down the tunnel behind the copy was washed to a ghost by
     it. The reader could see the ones out at the walls and asked where the
     rest had gone.

     Two things carry the type instead, and both are local. The halo on the
     glyphs themselves, which costs the picture nothing at all; and home.js
     pushing any piece whose path crosses the band of type sideways as it
     passes, so on any frame wide enough to have room the words stand in real
     empty room rather than in a bleached patch. What is left here is for the
     frames that have no room to give: a phone's copy is most of its width, a
     piece shoved clear of it would be shoved off the screen, and that is the
     one case where something still has to pass behind a line. */
  width: min(46rem, 86vw);
  height: min(21rem, 50svh);
  transform: translate(-50%, -50%);
  background: radial-gradient(
    closest-side,
    rgba(255, 255, 255, 0.52),
    rgba(255, 255, 255, 0.24) 55%,
    rgba(255, 255, 255, 0) 100%
  );
  opacity: calc(min(var(--k) / 0.14, 1) * (1 - clamp(0, (var(--k) - 0.82) / 0.16, 1)));
}

/* The halos do the work the panel above used to do, and they do it where it
   is needed: hugging the letterforms rather than spread over the whole room.
   Doubled up since v0.4.0, a wide soft one to lift whatever is behind the
   line and a tight hard one to hold the stroke's own edge. */
.beat--ink .beat__title {
  color: var(--crystal-fg);
  text-shadow: 0 0 34px rgba(255, 255, 255, 0.98), 0 0 14px rgba(255, 255, 255, 0.95),
    0 0 5px rgba(255, 255, 255, 0.9);
}

.beat--ink .beat__sub {
  color: var(--crystal-muted);
  text-shadow: 0 0 24px rgba(255, 255, 255, 0.98), 0 0 10px rgba(255, 255, 255, 0.95),
    0 0 4px rgba(255, 255, 255, 0.9);
}

.beat--ink .beat__eyebrow {
  color: var(--crystal-muted);
  text-shadow: 0 0 18px rgba(255, 255, 255, 0.98), 0 0 6px rgba(255, 255, 255, 0.9);
}

/* The coda beat stands beside the returned stone: the stone is carried
   stage left, so these words anchor right, mirrored from the film's opening
   lockup. Under a squarer viewport there is no beside, and it trades to the
   top-of-frame layout the portrait film uses; home.js moves the camera's
   subject down at the same 23/20 aspect line. */
.beat--coda {
  align-items: flex-end;
  text-align: right;
}

@media (max-aspect-ratio: 23/20) {
  .beat--coda {
    align-items: flex-start;
    text-align: left;
    justify-content: flex-start;
    padding-top: max(16svh, calc(env(safe-area-inset-top) + 5rem));
    padding-bottom: 0;
  }
}

/* "Step inside." It does not rise like copy; it grows like a doorway. */
.beat--enter {
  align-items: center;
  justify-content: center;
  text-align: center;
}

/* Since v0.3.0 this word stands over the stone's own face, which by that
   point in the dive is the brightest thing in the film, and pearl on white
   is nothing at all. So it carries its own shadow: a soft dark clearing
   beneath it, the mirror of the white one the ink beats stand in. Keyed to
   --k like everything else, so on the darker frames at the head of the beat
   it is barely present and nothing is dimmed that did not need to be. */
.beat--enter::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: 50%;
  top: 50%;
  width: min(54rem, 98vw);
  height: min(20rem, 46svh);
  transform: translate(-50%, -50%);
  background: radial-gradient(
    closest-side,
    rgba(0, 0, 0, 0.58),
    rgba(0, 0, 0, 0.32) 52%,
    rgba(0, 0, 0, 0) 100%
  );
  opacity: calc(min(var(--k) / 0.2, 1) * (1 - clamp(0, (var(--k) - 0.68) / 0.28, 1)));
}

.beat__enter {
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  /* Same height cap as .beat__title, and this word needs it more: it GROWS
     to 2.07x as its window plays, so a landscape phone was scaling a 5rem
     line past the top and bottom of a 390px frame. */
  font-size: min(clamp(2.4rem, 8vw, 5rem), 11svh);
  font-weight: 400;
  color: var(--pearl);
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.5), 0 2px 34px rgba(0, 0, 0, 0.72);
  opacity: calc(min(var(--k) / 0.25, 1) * (1 - clamp(0, (var(--k) - 0.68) / 0.28, 1)));
  transform: scale(calc(0.92 + var(--k) * 1.15));
  letter-spacing: calc(0.02em + var(--k) * 0.06em);
}

/* ---------------------------------------------------------------- gallery */

/* THERE IS NO GALLERY MARKUP ANY MORE (v0.4.6). The constellation inside
   the diamond renders inside the film's own canvas as one instanced draw:
   the pieces, their contact shadows, the room's beams crossing them and
   the glints are all fragments of the render now, not layers over it.
   What used to stand here: a .gallery container with seventy-four
   absolutely-positioned items, each promoted to its own compositor layer
   with a transform written per frame, and two full-viewport
   screen-blended .beam elements the film steered with custom properties.
   That was the chapter's real cost on a phone, and the ink beats' own
   clearing (below, unchanged) is the only piece of the chapter that
   remains DOM. If pieces ever need to stand over the canvas again, read
   the v0.4.5 notes in git history first: the visibility !important guard,
   the beam custom-property placement and the per-item layer rules all
   existed for measured reasons. */

/* ----------------------------------------------------------------- finale */

html.js .finale {
  display: flex;
}

.finale {
  min-height: 88svh;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: clamp(4rem, 12svh, 8rem)
    max(clamp(1.25rem, 5vw, 3rem), env(safe-area-inset-right))
    clamp(4.5rem, 14svh, 9rem)
    max(clamp(1.25rem, 5vw, 3rem), env(safe-area-inset-left));
  /* A breath of light behind the closing words, the same pool the dark
     room's own backdrop carries, so the finale is a lit black rather than
     a void. Decoration only: pearl on this wash still clears 16:1. */
  background: radial-gradient(
    62% 48% at 50% 42%,
    rgba(236, 236, 236, 0.05),
    rgba(236, 236, 236, 0) 72%
  );
}

.finale__eyebrow {
  margin: 0 0 1.15rem;
  font-family: var(--font-mono);
  font-size: clamp(0.56rem, 1.8vw, 0.64rem);
  font-weight: 400;
  letter-spacing: 0.34em;
  text-indent: 0.34em;
  color: var(--silver);
}

.finale__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.3rem, 7vw, 4.3rem);
  font-weight: 400;
  line-height: 1.08;
  letter-spacing: 0.01em;
  color: var(--pearl);
  text-wrap: balance;
}

.finale__sub {
  margin: 1.25rem 0 0;
  max-width: 26rem;
  font-size: clamp(0.88rem, 2.6vw, 0.98rem);
  line-height: 1.65;
  color: var(--silver);
}

.finale__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.7rem 0.8rem;
  margin-top: clamp(1.75rem, 5svh, 2.6rem);
}

/* Quieter than the two pills above it: this is the third option, not a
   competing one. The number itself takes the pearl so the tap target reads
   as a link at a glance, and the non-breaking hyphens in the markup stop a
   narrow phone splitting a phone number across two lines. */
.finale__call {
  margin: clamp(1rem, 3svh, 1.5rem) 0 0;
  font-size: clamp(0.82rem, 2.4vw, 0.9rem);
  line-height: 1.6;
  color: var(--silver);
}

.finale__call a {
  color: var(--pearl);
  text-decoration-color: var(--hairline);
  text-underline-offset: 0.22em;
}

.finale__call a:hover,
.finale__call a:focus-visible {
  text-decoration-color: currentColor;
}

.finale__btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: calc(0.68rem + var(--caps-drop)) clamp(1.3rem, 4vw, 1.9rem)
    calc(0.68rem - var(--caps-drop));
  border: 1px solid var(--hairline);
  border-radius: 999px;
  color: var(--pearl);
  font-family: var(--font-body);
  font-size: clamp(0.64rem, 2.2vw, 0.72rem);
  font-weight: 400;
  white-space: nowrap;
  text-decoration: none;
  background: var(--wash);
  cursor: pointer;
  transition: border-color 0.25s ease, background-color 0.25s ease,
    color 0.25s ease, opacity 0.25s ease, transform 0.25s ease;
}

.finale__btn:hover,
.finale__btn:focus-visible {
  border-color: var(--pearl);
  background: rgba(236, 236, 236, 0.12);
}

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

.finale__btn--book {
  border-color: transparent;
  background: var(--pearl);
  color: #0a0a0b;
}

.finale__btn--book:hover,
.finale__btn--book:focus-visible {
  border-color: transparent;
  background: #ffffff;
  opacity: 0.92;
}

/* The finale settles in as it arrives; site.css's reduced-motion block
   flattens this to an instant appearance. */
.finale__eyebrow,
.finale__title,
.finale__sub,
.finale__actions,
.finale__call {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 900ms var(--ease-out), transform 900ms var(--ease-out);
}

.finale.is-in .finale__eyebrow {
  transition-delay: 0ms;
}

.finale.is-in .finale__title {
  transition-delay: 90ms;
}

.finale.is-in .finale__sub {
  transition-delay: 190ms;
}

.finale.is-in .finale__actions {
  transition-delay: 290ms;
}

.finale.is-in .finale__call {
  transition-delay: 380ms;
}

.finale.is-in .finale__eyebrow,
.finale.is-in .finale__title,
.finale.is-in .finale__sub,
.finale.is-in .finale__actions,
.finale.is-in .finale__call {
  opacity: 1;
  transform: translateY(0);
}

/* ------------------------------------------------------- footer, on ink */

/* THIS BLOCK IS GONE, and its absence is the point (v0.3.7).
   The universal footer partial has always stayed byte-identical across the
   site, and this page used to dress it for the dark ground by overriding eight
   of the tokens site.css drew it with. site.css is the dark half of the ramp
   now and every page stands on the same black silk, so those eight overrides
   were saying what the shared sheet already says, and one of them
   (--page-muted at --fog) was quietly UNDOING the shared sheet's decision to
   lift the muted step for a textured ground. The footer is dressed once, over
   there, for every page including this one.

   Site.css draws focus rings in --pewter, which is the right value on the
   silk and the wrong one over the film's own render, so the landing chrome
   restates them in its own foreground values. */

.nav :focus-visible {
  outline: 2px solid var(--nav-fg);
  outline-offset: 3px;
}

.finale :focus-visible {
  outline: 2px solid var(--pearl);
  outline-offset: 3px;
}

/* ------------------------------------------------------------ small ends */

/* 480, not 380, and the 100px is the whole point.

   The full row is brand 133px + 12px gap + both pills 277px + 32px of
   gutter, which needs 455px with the webfonts loaded and 459px in the swap
   fallback. The tails used to come off at 380, so every phone between 381
   and 454 was rendering a row that did not fit. And .nav is position: fixed,
   which means the overflow produces NO scrollbar and does not move
   scrollWidth: the Contact Us pill was simply clipped off the right edge in
   silence, 53% of it gone at 390px, on an iPhone 12 through 15. It and the
   wordmark are the only navigation this page has.

   That band is most of the phones in use, which is exactly why this needed
   measuring rather than eyeballing: 320 was fine (tails hidden, row 304px)
   and so was desktop, so both ends of the range this file tests looked
   correct while the middle was broken. */
@media (max-width: 480px) {
  .nav__brand-tail,
  .nav__btn-tail {
    display: none;
  }
}

/* THE DISPLAY TYPE KEEPS ITS SHARE OF A LANDSCAPE FRAME, not its size.
   The clamp above reaches its 3.8rem ceiling at about 1090px of width, so
   from there down the line is a fixed 520px and its SHARE of the frame only
   grows as the frame narrows: 27% at 1920, 36% at 1440, 47% at 1100. The
   prop's share moves the other way and then stops, because below aspect 1.35
   the fit is what sets the camera distance and the ring is pinned at its
   largest. Both peak together between roughly 1.0 and 1.35, which is
   1024x768, 1280x1024 and any window dragged to half a large screen, and 47%
   of copy beside 52% of ring does not fit in 100% of frame. home.js gives
   back some of it by holding more world (see `sq` in cameraAt); this is the
   other half, and it is the half that keeps the type in proportion to the
   picture rather than to the font size somebody chose at 1440.

   4.4vw is the largest cap that leaves every frame this site was tuned on
   exactly as it was: it only starts to bite below 1382px of width, so 1440
   and 1920 compute 63.4px and 84.5px against a clamp that has already
   stopped at 60.8. Landscape only, because on a phone in portrait a vw cap
   would set the hero at 17px. */
@media (min-aspect-ratio: 9/10) {
  .beat__title {
    font-size: min(clamp(2.05rem, 4.6vw + 0.7rem, 3.8rem), 9.5svh, 4.4vw);
  }
}

/* Portrait composition: the camera drops the prop into the lower field and
   the words take the top. Same threshold as home.js's camera bias. */
@media (max-aspect-ratio: 9/10) {
  .beat,
  .beat--low {
    justify-content: flex-start;
    padding-top: max(16svh, calc(env(safe-area-inset-top) + 5rem));
    padding-bottom: 0;
  }

  .beat--enter {
    justify-content: center;
    padding-top: 0;
  }

  /* The clearing has to travel with the words. Centred, as it is on a wide
     frame, it sits a third of a screen below type that has just moved to the
     top, which leaves the ink beats reading straight across whatever piece
     happens to be drifting behind them.

     AND ON PORTRAIT IT IS STRONGER, because it is the only protection there
     is (v0.4.5). On a wide frame the near pieces PART around the band of
     type, so 0.52 only ever has to veil the far ones; a phone's copy is most
     of its width, nothing can be pushed clear of it, and every piece in the
     band passes straight behind the words. Measured at 320x800: a dark
     detail in a piece (a leopard's pave shadow, about 40 of 255) under the
     0.52 veil composites to 152, where the eyebrow's #5c5c5c is 2.5:1 and
     unreadable; under 0.68 it lands at 186, 3.6:1 before the glyph halos do
     their work. Wider too, since the portrait sub runs to the walls. */
  .beat--ink::before {
    /* 26%, measured, not 34%: under the 16svh pad the whole ink block spans
       about 15% to 35% of the frame on a phone, its centre near 25%, and at
       34% the veil's strong middle sat BELOW the sub while the eyebrow got
       the ellipse's last few percent. Photographed at 320x800: the eyebrow
       line crossed a pave piece at an effective veil of 0.08 and vanished. */
    top: 26%;
    width: min(46rem, 92vw);
    height: min(20rem, 44svh);
    background: radial-gradient(
      closest-side,
      rgba(255, 255, 255, 0.68),
      rgba(255, 255, 255, 0.34) 55%,
      rgba(255, 255, 255, 0) 100%
    );
  }

  .beat__sub {
    max-width: 21rem;
  }
}

/* ------------------------------------------------------------------ print */
/* The film cannot land on paper: the journey is a scroll track whose every
   frame is drawn to a canvas, so a printer would get whichever single frame
   the reader happened to park on, under chrome designed for a black ground.
   Print keeps the words and cuts the theatre. The loader, the nav and the
   whole pinned track go; the finale's copy stays as the landing page's one
   statement, its pills go the way of every other action on paper, and the
   phone number it repeats as text is the part with a use there. The dark
   ramp's tokens flip to ink so every rule reading them restyles itself,
   the same move site.css makes over the shared tokens; keep the two blocks
   in step. */
@media print {
  :root {
    --ink: #ffffff;
    --ink-lifted: #ffffff;
    --pearl: #111111;
    --silver: #444444;
    --fog: #767676;
    --hairline: rgba(0, 0, 0, 0.5);
    --hairline-faint: rgba(0, 0, 0, 0.16);
    --wash: transparent;
  }

  /* html.js prefixes match the specificity of the gates that display these
     at all (html.js .nav and friends above); bare class selectors here lose
     that fight and the whole 1800vh track prints as twenty empty sheets. */
  html.js .loader,
  html.js .nav,
  html.js .journey,
  .finale__actions {
    display: none;
  }

  /* 88svh of centring and a pool of light are staging, and print has no
     stage: the finale sets as ordinary copy at the top of the sheet. */
  .finale {
    min-height: 0;
    background: none;
  }

  /* The entrance waits for an IntersectionObserver that a printer never
     scrolls to, so the finished state is forced here or the page prints
     with its copy at opacity zero. */
  .finale__eyebrow,
  .finale__title,
  .finale__sub,
  .finale__call {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
