/* Site furniture: preloader, cursor, grain, header, footer, scroll rail.
   Everything here is present on every page and owns the z-index ladder.

   z-index ladder, bottom to top:
     0    page content
     5    section decoration
     40   sticky header
     60   dialog backdrop (native)
     80   grain + scanline overlay
     90   custom cursor
     120  page-transition curtain
     140  preloader
     200  skip link

   Above all of it sits the browser's own top layer, which no z-index can
   reach into: a modal <dialog>, and the cursor, which joins it as a manual
   popover so the dialog cannot bury it. That also lifts the cursor over the
   curtain and the loading screen, where it used to be lost the same way.
   See initCursor() in chrome.js.                                           */

/* ------------------------------------------------------------ 1. Preloader */

/* THE TOWER.

   What used to be here was a progress bar with "Loading inventory" written
   under it, which is a sentence about nothing — the browser is loading a
   marketing page, not anybody's stock. This draws the thing the product is
   actually about instead: a building goes up, floor by floor, and then settles
   into the units it is made of.

   Two beats per floor. The slab is poured first — one brand-coloured plate
   sweeping left to right, the floor under construction. The plate then clears
   and that floor's units fade up carrying their own state colours. By the top
   the tower is not a progress bar any more, it is a sales position: green at
   the bottom thinning upward, because that is the order inventory sells in.

   The numbers are real. The hairline and the count track the fonts and the
   load event exactly as the old bar did, so the tower never finishes building
   before the page is ready to be looked at. */

.preloader {
  position: fixed;
  inset: 0;
  z-index: 140;
  display: grid;
  place-items: center;
  background: var(--bg-deep);
  /* Removed from the layer entirely once done, so it can never trap a click. */
  transition: visibility 0s linear var(--dur-curtain);
}

/* No JS means no loading sequence to watch. */
html:not(.js) .preloader { display: none; }

.preloader__inner {
  display: grid;
  gap: var(--space-5);
  justify-items: center;
  width: min(24rem, 80vw);
}

/* ---- the site */

.preloader__site { position: relative; width: 100%; }

/* Narrow and tall, so the thing has the proportions of a building rather than
   of a spreadsheet. The plot it stands on is wider than it is. */
.preloader__plot {
  /* ONE number governs the whole tower. Height is derived from it below, never
     chosen separately — that is what left the units stretched: a clamped height
     divided by the floors has no idea what the column width came out as, so the
     cells were whatever fell out of two unrelated sums. */
  --tower-w: min(12rem, 46vw, 24vh);
  --gap: 2px;
  /* Defaults so the box is the right size before a floor exists. chrome.js
     writes the real counts onto the tower, and must keep them in step. */
  --cols: 12;
  --rows: 20;

  width: var(--tower-w);
  margin-inline: auto;
}

.preloader__tower {
  position: relative;
  display: flex;
  /* DOM order is ground up, so floor 0 is the one that lands first. */
  flex-direction: column-reverse;
  justify-content: flex-start;
  gap: var(--gap);

  /* A unit is square, so the floor's height IS the column width, and the
     tower's height is that times the floors plus the gaps between them.
     Written out rather than left to aspect-ratio because the gaps are a fixed
     pixel count that no ratio can account for — and because the box has to be
     the right size BEFORE the floors are written into it, or the screen jumps
     the moment they arrive. */
  --cell: calc((var(--tower-w) - (var(--cols) - 1) * var(--gap)) / var(--cols));
  height: calc(var(--rows) * var(--cell) + (var(--rows) - 1) * var(--gap));
}

/* The parapet, laid across the top when the last floor lands. It is the beat
   that turns a filled grid into a finished building. */
.preloader__tower::before {
  content: "";
  position: absolute;
  left: -7px;
  right: -7px;
  top: -6px;
  height: 2px;
  border-radius: 1px;
  background: var(--brand-lift);
  box-shadow: 0 0 12px var(--brand-glow);
  transform: scaleX(0);
}
.preloader__tower.is-topped::before { animation: parapet 480ms var(--ease-out) forwards; }
@keyframes parapet { to { transform: scaleX(1); } }

.preloader__floor {
  position: relative;
  flex: 1;
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  gap: var(--gap);
  opacity: 0;
  transform: translateY(9px);
}

/* The slab: the floor as one poured plate, before it has any units in it. */
.preloader__floor::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 1px;
  background: var(--brand);
  box-shadow: 0 0 14px var(--brand-glow);
  transform-origin: left;
  opacity: 0;
}

.preloader__unit {
  --lit: 0.22;
  border-radius: 1px;
  background: var(--available);
  transform-origin: bottom;
  opacity: 0;
}
.preloader__unit[data-state="blocked"] { --lit: 0.76; background: var(--blocked); }
.preloader__unit[data-state="sold"]    { --lit: 0.86; background: var(--sold); }

.preloader__floor.is-built { animation: floor-land 420ms var(--ease-out) forwards; }
.preloader__floor.is-built::after { animation: floor-slab 760ms var(--ease-out) forwards; }
.preloader__floor.is-built .preloader__unit {
  animation: floor-units 380ms var(--ease-out) forwards;
  animation-delay: calc(220ms + var(--u) * 13ms);
}

@keyframes floor-land {
  to { opacity: 1; transform: none; }
}
/* Poured, held, then gone — the units underneath are what is left. */
@keyframes floor-slab {
  0%   { opacity: 0; transform: scaleX(0); }
  18%  { opacity: 1; transform: scaleX(1); }
  34%  { opacity: 1; transform: scaleX(1); }
  100% { opacity: 0; transform: scaleX(1); }
}
@keyframes floor-units {
  from { opacity: 0; transform: scaleY(0.25); }
  to   { opacity: var(--lit); transform: none; }
}

/* Ground line and the light pooling on it. Without these the tower floats. */
.preloader__ground {
  position: relative;
  display: block;
  height: 1px;
  margin-top: 7px;
  background: linear-gradient(90deg, transparent, var(--line-bright), transparent);
}
.preloader__ground::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 1px;
  width: 78%;
  height: 46px;
  transform: translateX(-50%);
  background: radial-gradient(60% 100% at 50% 0%, var(--brand-wash), transparent 72%);
}

/* Topping out: the whole frame lifts once when the last floor lands. A
   transform on the container, not a filter — a filter would re-raster 288
   cells on a machine that has just finished loading a page. */
.preloader__tower.is-topped { animation: top-out 640ms var(--ease-out); }
@keyframes top-out {
  0%, 100% { transform: none; }
  40%      { transform: translateY(-5px); }
}

/* ---- the readout */

.preloader__meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  width: 100%;
}
.preloader__mark { width: 7.25rem; height: auto; opacity: 0.86; }

.preloader__readout {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}
.preloader__floors { color: var(--ink-4); }
.preloader__count { color: var(--ink); }

.preloader__line {
  display: block;
  width: 100%;
  height: 1px;
  background: var(--line);
  overflow: hidden;
}
.preloader__fill {
  display: block;
  height: 100%;
  background: var(--brand);
  box-shadow: 0 0 10px var(--brand-glow);
  transform: scaleX(0);
  transform-origin: left;
  will-change: transform;
}

/* The curtain that lifts off the loaded page. Two panels meeting in the
   middle, so the reveal has a seam instead of a fade. */
.curtain {
  position: fixed;
  inset: 0;
  z-index: 120;
  pointer-events: none;
  display: grid;
  grid-template-rows: 1fr 1fr;
}
.curtain__panel {
  background: var(--bg-deep);
  transform: scaleY(0);
  will-change: transform;
}
.curtain__panel:first-child { transform-origin: top; }
.curtain__panel:last-child { transform-origin: bottom; }
.curtain.is-active { pointer-events: all; }

/* Set before first paint by the inline head script on a return visit. */
html.has-visited .curtain__panel { transform: scaleY(1); }
html.has-visited .preloader { display: none; }

/* ---------------------------------------------------- 2. Grain + scanlines */

/* A still grain plate reads as a dead texture; this one walks. It is the
   cheapest thing on the page that makes a flat dark ground feel photographed. */
.grain {
  position: fixed;
  inset: -150%;
  z-index: 80;
  pointer-events: none;
  /* soft-light, not overlay. Overlay pivots hard around the mid-tones, and the
     accent colour (#8b89ff) sits right on that pivot — it left visible noise
     crawling over every purple word while leaving the white ones clean.
     soft-light touches the dark ground where the texture is wanted and barely
     moves saturated type. */
  opacity: 0.32;
  mix-blend-mode: soft-light;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
  animation: grain-walk 700ms steps(1) infinite;
}
@keyframes grain-walk {
  0%   { transform: translate(0, 0); }
  20%  { transform: translate(-3%, 4%); }
  40%  { transform: translate(4%, -2%); }
  60%  { transform: translate(-2%, -4%); }
  80%  { transform: translate(3%, 3%); }
  100% { transform: translate(0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .grain { animation: none; opacity: 0.22; }
}

/* ----------------------------------------------------- 3. The custom cursor */

/* Pointer devices only. A touch user gets nothing extra and loses nothing,
   and a keyboard user is never chasing a dot they cannot move. */
.cursor { display: none; }

@media (hover: hover) and (pointer: fine) {
  html.js.has-cursor,
  html.js.has-cursor * { cursor: none; }

  /* The dot and the ring are siblings pinned to the viewport and moved
     independently, because the ring lags behind the dot — a single wrapper
     could only ever give them one position. Both are driven by transform
     alone; size changes are a scale, never a width, so the ring never
     triggers layout while it is chasing the pointer. */
  .cursor {
    position: fixed;
    inset: 0;
    z-index: 90;
    display: block;
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--dur-base) var(--ease-out);
    /* Undoing the UA's popover box, which otherwise arrives as a small
       bordered white panel in the middle of the screen. Every one of these is
       load-bearing; `background` most of all. */
    width: auto;
    height: auto;
    margin: 0;
    padding: 0;
    border: 0;
    overflow: visible;
    background: transparent;
    color: inherit;
  }
  .cursor.is-awake { opacity: 1; }

  /* Set only where popovers are unsupported, and only while a dialog is open:
     the drawn cursor cannot be reached, so the real one comes back. */
  html.js.has-cursor.has-dialog,
  html.js.has-cursor.has-dialog * { cursor: auto; }
  html.has-dialog .cursor { opacity: 0; }

  .cursor__dot,
  .cursor__ring {
    position: fixed;
    top: 0; left: 0;
    will-change: transform;
  }

  .cursor__dot {
    width: 6px; height: 6px;
    margin: -3px 0 0 -3px;
    border-radius: 50%;
    background: var(--ink);
    transition: opacity var(--dur-fast) var(--ease-out);
  }

  .cursor__ring {
    width: 42px; height: 42px;
    margin: -21px 0 0 -21px;
    border: 1px solid var(--line-bright);
    border-radius: 50%;
    display: grid;
    place-items: center;
    transition: background var(--dur-base) var(--ease-out),
                border-color var(--dur-base) var(--ease-out);
  }

  .cursor__label {
    font-size: 8px;
    line-height: 1;
    text-transform: uppercase;
    color: var(--bg-deep);
    opacity: 0;
    transition: opacity var(--dur-fast) var(--ease-out);
    white-space: nowrap;
  }

  /* Over anything clickable. */
  .cursor.is-hot .cursor__ring { background: var(--brand); border-color: transparent; }
  .cursor.is-hot .cursor__dot { opacity: 0; }

  /* Over a surface that says what it does — the tower, the board, the ledger. */
  .cursor.is-labelled .cursor__ring { background: var(--ink); border-color: transparent; }
  .cursor.is-labelled .cursor__label { opacity: 1; }
  .cursor.is-labelled .cursor__dot { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  html.js.has-cursor,
  html.js.has-cursor * { cursor: auto; }
  .cursor { display: none !important; }
}

/* --------------------------------------------------------- 4. Scroll rail */

.rail {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 2px;
  z-index: 45;
  pointer-events: none;
  background: transparent;
}
.rail__fill {
  height: 100%;
  width: 100%;
  transform: scaleX(0);
  transform-origin: left;
  background: linear-gradient(90deg, var(--brand-deep), var(--brand-lift));
}

/* -------------------------------------------------------------- 5. Header */

/* No transform and no will-change here, deliberately. Either one makes this
   element the containing block for position:fixed DESCENDANTS, and the mobile
   menu panel is one — it would resolve inset:0 against the header instead of
   the viewport and open as a 72px-tall sliver. The header does not move any
   more, so neither is needed. */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 40;
  padding: var(--space-4) var(--gutter);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  max-width: var(--container-wide);
  margin-inline: auto;
  height: var(--header-h);
  /* Asymmetric on purpose. The bar is a pill, so at 72px tall its corner radius
     is 36px and the curve is still cutting inward at the height the wordmark
     sits — an equal inset leaves the logo pressed against the bend. The right
     side needs less, because the filled waitlist button occupies its corner
     rather than leaving a curve to fall into. */
  padding-inline: var(--space-6) var(--space-4);
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  transition: background var(--dur-slow) var(--ease-out),
              border-color var(--dur-slow) var(--ease-out),
              backdrop-filter var(--dur-slow) var(--ease-out);
}
.site-header.is-stuck .site-header__inner {
  background: color-mix(in srgb, var(--bg) 62%, transparent);
  border-color: var(--line);
  backdrop-filter: blur(20px) saturate(140%);
}

.site-header__logo { flex: none; line-height: 0; }

/* OPTICALLY centred, not geometrically.

   The lockup's artwork box runs from the ascenders down to the descender of the
   `p` in Prop, which sits 19.76 units below the baseline in a box 84.97 tall.
   Centre that box and the letters you actually read — the band from cap height
   to baseline — land above the middle of the header, which is why the wordmark
   looked high while measuring as perfectly centred.

   Its optical centre is 10.3% of the image height above the box centre, so the
   image is pushed down by exactly that. Measured off the SVG's own path boxes;
   re-measure if the lockup is ever redrawn. */
.site-header__logo img {
  width: auto;
  height: 1.3rem;
  transform: translateY(10.3%);
}

.site-nav { position: relative; margin-inline: auto; display: grid; }
.site-nav__list { grid-area: 1 / 1; display: flex; align-items: center; gap: var(--space-2); }
.site-nav__cta { grid-area: 2 / 1; }

.site-nav__link {
  position: relative;
  display: block;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  color: var(--ink-2);
  text-decoration: none;
  overflow: hidden;
  transition: color var(--dur-base) var(--ease-out);
}
/* The label is printed twice, stacked; hover rolls one out and the other in. */
.site-nav__link span { display: block; will-change: transform; }
.site-nav__link span:last-child {
  position: absolute;
  inset: var(--space-2) var(--space-4) auto;
  transform: translateY(115%);
}
.site-nav__link:hover { color: var(--ink); }
.site-nav__link:hover span:first-child { transform: translateY(-115%); }
.site-nav__link:hover span:last-child { transform: translateY(0); }
.site-nav__link span { transition: transform var(--dur-base) var(--ease-out); }

.site-nav__link.is-current { color: var(--ink); }
.site-nav__link.is-current::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--brand-wash);
  border: 1px solid var(--brand-line);
}

.nav-toggle {
  display: none;
  flex: none;
  width: 2.75rem; height: 2.75rem;
  margin-left: auto;
  padding: 0;
  background: transparent;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 5px;
}
.nav-toggle__bar {
  display: block;
  width: 1.05rem; height: 1.5px;
  background: var(--ink);
  transition: transform var(--dur-base) var(--ease-out);
}

@media (max-width: 58rem) {
  .nav-toggle { display: flex; }

  /* The panel covers the viewport, so the logo and the close button have to be
     lifted above it — they live in the same header and would otherwise be
     painted over by their own menu. */
  .site-header__logo,
  .nav-toggle { position: relative; z-index: 2; }

  .site-nav {
    position: fixed;
    inset: 0;
    z-index: 1;
    margin: 0;
    padding: calc(var(--header-h) + var(--space-16)) var(--gutter) var(--space-12);
    background: var(--bg-deep);
    display: grid;
    align-content: start;
    clip-path: inset(0 0 100% 0);
    transition: clip-path var(--dur-slow) var(--ease-in-out);
  }
  .site-header.is-open .site-nav { clip-path: inset(0 0 0 0); }

  .site-nav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .site-nav { display: block; }
  .site-nav__link {
    padding: var(--space-5) 0;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
    font-family: var(--font-display);
    font-variation-settings: 'wdth' 106, 'wght' 700;
    font-size: var(--display-sm);
    text-transform: uppercase;
    color: var(--ink);
  }
  .site-nav__link span:last-child { display: none; }
  .site-nav__link.is-current::before { display: none; }
  .site-nav__link.is-current { color: var(--brand-lift); }

  .site-header.is-open .nav-toggle__bar:first-child { transform: translateY(3.25px) rotate(45deg); }
  .site-header.is-open .nav-toggle__bar:last-child { transform: translateY(-3.25px) rotate(-45deg); }

  html:not(.js) .site-nav { position: static; clip-path: none; padding: 0; background: none; }
  html:not(.js) .nav-toggle { display: none; }
  html:not(.js) .site-nav__link { font-size: var(--text-lg); font-family: var(--font-sans); text-transform: none; }
}

/* The desktop CTA is dropped on a narrow header and reappears inside the menu
   panel, so the site's only conversion goal is never simply removed.
   Scoped through .site-header deliberately: a bare .site-header__cta ties with
   .btn on specificity and components.css loads later, so it would lose. */
@media (max-width: 34rem) {
  .site-header .site-header__cta { display: none; }
}
@media (min-width: 34.0625rem) {
  .site-nav__cta { display: none; }
}

/* The menu panel's own CTA. */
.site-nav__cta { margin-top: var(--space-8); }
.site-nav__cta .btn { display: flex; width: 100%; }

/* -------------------------------------------------------------- 6. Footer */

/* A slab the page slides under. Only the top corners are rounded, the ground is
   one flat lifted surface, and nothing is drawn behind the copy — every section
   above this one is textured, so the last thing read is the quietest. */
.site-footer {
  position: relative;
  margin-top: clamp(3rem, 6vw, 5.5rem);
}

.site-footer__panel {
  position: relative;
  overflow: hidden;
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-bottom: 0;
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  padding-top: clamp(4.5rem, 7.5vw, 7rem);
}

.site-footer__inner {
  display: grid;
  gap: var(--space-2);
  justify-items: center;
  text-align: center;
  padding-bottom: var(--space-12);
}

/* The brand block spans the full width and stays centred at every size; the
   three columns beneath it flank the buttons only once there is room for it.
   The negative margin is optical, not structural: the lockup's viewBox includes
   the descender of the 'p', so a zero gap still reads as a gap. */
.site-footer__crest {
  display: grid;
  justify-items: center;
  margin-bottom: -0.75rem;
}

/* Sized against the container rather than the viewport, so the lockup keeps its
   proportion to the copy under it instead of to the window. */
.site-footer__wordmark {
  display: block;
  width: clamp(12rem, 34cqi, 25rem);
  height: auto;
}

.site-footer__heading {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink-4);
  margin-bottom: var(--space-4);
}

.site-footer__list,
.site-footer__links { display: grid; gap: var(--space-3); }

.site-footer__list a,
.site-footer__links a {
  font-size: var(--text-sm);
  color: var(--ink-2);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.site-footer__list a:hover,
.site-footer__links a:hover { color: var(--ink); }
.site-footer__region { margin-left: var(--space-2); font-size: var(--text-xs); color: var(--ink-4); }

.site-footer__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); justify-content: center; }

@media (min-width: 60rem) {
  .site-footer__inner {
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    column-gap: var(--space-12);
    row-gap: 0;
    text-align: left;
    justify-items: stretch;
    padding-bottom: var(--space-16);
  }
  .site-footer__crest { grid-column: 1 / -1; }
  .site-footer__contact { grid-column: 1; }
  .site-footer__actions { grid-column: 2; flex-wrap: nowrap; }
  .site-footer__nav { grid-column: 3; justify-self: end; text-align: left; }

  /* Four links read as a block, not a list, once they are two abreast. Two
     rows, not three: an odd column leaves one link stranded on its own. */
  .site-footer__links {
    grid-template-rows: repeat(2, auto);
    grid-auto-flow: column;
    column-gap: var(--space-10);
  }
}

.site-footer__legal {
  display: flex;
  justify-content: center;
  padding-block: var(--space-6);
  border-top: 1px solid var(--line);
  font-size: var(--text-xs);
  color: var(--ink-4);
}

/* The legal links sit in one pill rather than loose on the baseline, which is
   what stops the last line of the page reading as an afterthought. */
.site-footer__pills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2) var(--space-5);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-5);
}
.site-footer__pills a {
  color: var(--ink-3);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.site-footer__pills a:hover { color: var(--ink); }
