/* brandmax — page styles. Paint order: loader 100 · wordmark 10 · contact 2 ·
   content layer and chart type 1 · reveal canvas -1 · orb wrap -2 · contour
   field, projects ring and sphere -3. Those last three tie, and markup order
   settles it, so the sphere paints over the field it covers. Sections follow
   paint order; a rule that depends on file order says so. */

/* RESET, ROOT SCALE, TOKENS ---------------------------------------------- */

html {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;

  /* Mobile Safari's text autosizing inflates copy on rotation to landscape —
     only on the device, never in an emulator. Every measure below is cut to
     the type size it states, so the inflation is declined outright. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;

  /* Both axes, on html: while html computes to `visible` on either axis,
     body's used overflow propagates to the viewport and makes the viewport a
     non-scrolling box — the wheel survives, one-finger touch panning does
     not. `clip` beside `auto` still contains the horizontal. */
  overflow-x: clip;
  overflow-y: auto;
}

*,
*::before,
*::after {
  -webkit-box-sizing: inherit;
  -moz-box-sizing: inherit;
  box-sizing: inherit;
  margin: 0;
  padding: 0;
}

:root {
  --ink: #1d1d1d;
  --ink-raised: #252525;
  --paper: #e5e4e0;
  --rule: rgba(218, 218, 218, 0.2);
  --rule-strong: rgba(218, 218, 218, 0.6);
}

/* one em is 1/94.5 of the frame: type, chrome insets and the statement grid
   all scale with the viewport off one design width */
body {
  font-size: calc(100vw / 94.5);
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 400;
  line-height: 1.4;
  cursor: auto;
  color: var(--paper);
  background-color: var(--ink);
  overflow: clip;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
@media only screen and (max-width: 992px) {
  body { font-size: 2.4vw; }   /* narrow frames take a coarser unit */
}

::selection { background: var(--ink); color: var(--paper); text-shadow: none; }

a { color: inherit; text-decoration: none; }

/* layers the reader looks through rather than handles */
[data-inert] { pointer-events: none !important; user-select: none; }

/* TYPE FACES ------------------------------------------------------------- */

/* Self-hosted: no third-party request at runtime. Display faces block — a
   wordmark or headline may not flash a substitute at a different width, and
   the loader covers the fetch. The text face swaps: copy must not wait. */
@font-face {
  font-family: "Nimbus Sans";
  src: url("../fonts/NimbusSans-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Space Grotesk";
  src: url("../fonts/SpaceGrotesk-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: "PP Grafier";
  src: url("../fonts/PPGrafier-RegularDisplay.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: "At Aero Retina";
  src: url("../fonts/AtAero-Retina.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}
/* The delivery card's pair, OFL like the rest: a modern grotesque carrying
   400 and 500 from one variable file, and the mono for its chip. Both block —
   card type flashing a fallback mid-scene would be the one visible swap on
   the page — and both are latin subsets, which is all the card sets. */
@font-face {
  font-family: "Instrument Sans";
  src: url("../fonts/InstrumentSans-Var.woff2") format("woff2");
  font-weight: 400 500;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: "DM Mono";
  src: url("../fonts/DMMono-Medium.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: block;
}
/* Space Grotesk's monospace sibling — the pricing chip's voice */
@font-face {
  font-family: "Space Mono";
  src: url("../fonts/SpaceMono-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

/* PAGE SHELL ------------------------------------------------------------- */

.site-w { z-index: 1; width: 100%; overflow-x: clip; }

.flow-w {
  position: relative;
  z-index: 1;
  /* what the statement section adds to the document; the runway subtracts it
     again, so the page total and every beat scrubbed against it hold */
  --scene2-flow: 100vh;
}

/* LOADER ----------------------------------------------------------------- */

/* opaque sheet in the field's resting grey — track the field background in
   js/hero.js if that value moves */
.loader-w {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #e3e5ea;
  transition: opacity 0.65s cubic-bezier(0.4, 0, 0.2, 1);
}
.loader-w.is-done { opacity: 0; pointer-events: none; }

.loader-bar {
  display: block;
  width: min(28vw, 220px);
  height: 6px;
  background: rgba(29, 29, 29, 0.12);
  overflow: hidden;
}

/* js/main.js steps scaleX from the boot ledger; this transition carries the
   value between steps on the compositor, so the bar keeps gliding through the
   main-thread stalls that freeze a rAF-driven fill. Eased out only: progress
   that decelerates into each step reads as loading, not as a clock. */
.loader-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}

/* WORDMARK --------------------------------------------------------------- */

/* on screen for the whole runway, em-sized to track the root scale, with px
   floors so it never collapses on a phone */
.chrome-w {
  position: fixed;
  top: max(20px, 1.5em);
  left: max(24px, 1.9em);
  z-index: 10;
}

.brand {
  font-family: "Space Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700;
  font-size: max(30px, 2em);
  line-height: 1;
  letter-spacing: -0.045em;
  color: var(--ink);
}

/* ORB -------------------------------------------------------------------- */

.core-w {
  z-index: -2;
  justify-content: center;
  align-items: center;
  display: flex;
  position: fixed;
  inset: 0%;
}

/* Rest size, pre-script fallback only: js/stage.js measures the same thing and
   writes it back in px, and the floor here is that module's desktop rest
   fraction. Fixed at every width — the timelines run on phones too, and an
   absolute box would resolve `inset` against the whole document. */
.core {
  z-index: -1;
  border-radius: 100%;
  width: 80vh;
  height: 80vh;
  min-width: 47.62vw;
  min-height: 47.62vw;
  position: absolute;
}
/* portrait gives the short edge the same 80%; must follow the rule whose
   floor it relaxes */
@media (orientation: portrait) {
  .core {
    width: 80vw;
    height: 80vw;
    min-width: 0;
    min-height: 0;
  }
}

/* orb.js appends the renderer's canvas straight onto the element, so the
   child combinator is exact — and the sheet, not the renderer, owns the
   displayed box: the drawing buffer is sized independently. */
.core > canvas {
  background-color: transparent !important;
  height: 100% !important;
  width: 100% !important;
}

/* HERO FIELD LAYERS ------------------------------------------------------ */

/* the contour field sits below the orb wrap; the reveal — fluid trail and the
   object it uncovers — above the orb and below the content layer */
.field-w,
.reveal-w {
  position: fixed;
  inset: 0;
  pointer-events: none;
}
.field-w  { z-index: -3; }
.reveal-w { z-index: -1; }

.field-w canvas,
.reveal-w canvas { display: block; width: 100%; height: 100%; }

/* HERO SCENE ------------------------------------------------------------- */

.scene {
  height: 100vh;
  display: flex;
  align-items: center;
  padding: 0 8vw;
  color: var(--ink);
}

/* the first viewport belongs to the field layers and the cradled orb */
.scene--hero {
  pointer-events: none;
  justify-content: center;
  -webkit-user-select: none;
  user-select: none;
}

/* one unbroken line, sized so the statement spans the 94vw measure — the
   string sets 10.489em in this face. The h1 is the lockup, and so the
   containing block for the lead-in hung above it. */
.hero-headline {
  font-family: "PP Grafier", serif;
  font-style: normal;
  font-weight: 400;
  font-size: 8.9604vw;
  line-height: 9.3871vw;
  color: #fff;
  white-space: nowrap;
  position: relative;
}

/* inline-block for the reflection: the mirror sizes its bitmap from the
   element rect and its baseline from the computed line-height, and the two
   only agree on a block box */
.hero-line { display: inline-block; }

/* Hung above the line and out of flow, so neither the measure nor the scene's
   centring moves. The left edge is aligned by INK — the lead's first glyph
   carries a 0.031em sidebearing the line's does not — and the 0.055em stands
   its descender off the caps below. */
.hero-lead {
  --lead: 0.42;
  position: absolute;
  bottom: calc(100% + 0.055em);
  left: calc(0.031em / var(--lead));
  font-size: calc(1em * var(--lead));
  line-height: 1;
}
/* em rather than px, so a raised default text size moves the tier with it */
@media (max-width: 59.563em) {
  .hero-headline {
    font-size: 8.5337vw;
    line-height: 9.3871vw;
  }
}

/* STATEMENT -------------------------------------------------------------- */

/* The one scene besides the hero that owns document flow: one viewport,
   adjoining the hero with no offset, so the heading enters on the first
   scroll gesture and the block is read on the move against a parked orb. Copy
   is top-anchored by the container's opening padding rather than centred — a
   centred block's first line drifts down the document with aspect ratio. */
.statement-w {
  height: 100vh;
  font-family: "At Aero Retina", "Helvetica Neue", "Nimbus Sans", Helvetica, Arial, sans-serif;
  font-size: 1vw;
  color: var(--ink);
  -webkit-user-select: none;
  user-select: none;
}

/* padding, not a margin, which would collapse straight out of the section */
.statement-w .container { width: 100%; padding: 10em 1.25em 0; }

.statement-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: auto;
  grid-auto-columns: 1fr;
  gap: 1.25em;
  width: 100%;
}
/* the beat between the heading and the paragraph row */
.statement-grid + .statement-grid { margin-top: 10em; }

/* placement is its own layer: a grid area and nothing else */
.about-head { grid-area: 1 / 3 / 2 / 12; }
.about-lead { grid-area: 1 / 2 / 2 / 4; }
.about-body { grid-area: 1 / 5 / 2 / 8; }

.statement-head {
  text-transform: uppercase;
  font-size: 5em;
  font-weight: 400;
  line-height: 1;
}

/* The first-line indent as generated content: four glyphs that reserve the
   opening step without entering the DOM, the accessibility tree or innerText,
   so the heading is crawled from its first word and the mirror never prints
   the indent onto the chrome. THE TRAILING SPACE IS PART OF THE STRING — it
   is the word space before that word, and the wrap sits under a reflow cliff. */
.statement-head::before {
  content: "**** ";
  visibility: hidden;
}

/* A hairline on the body blocks: this face ships one weight, and synthetic
   bold lands at +42% ink with no dial on it. A stroke is the same mechanism
   with a dial, low enough to read as weight rather than paint, and it leaves
   advance widths — and so the measured wrap — untouched. */
.statement-body {
  font-size: 1em;
  font-weight: 400;
  line-height: 1.4;
  -webkit-text-stroke: 0.012em currentColor;
}
.statement-lead { text-transform: uppercase; }

/* below 992 the section pins a fixed base rather than tracking the frame */
@media only screen and (max-width: 992px) {
  .statement-w { font-size: 13px; }
  .statement-w .container {
    padding-top: 10rem;
    padding-left: 2vw;
    padding-right: 2vw;
  }
  .statement-grid { gap: 1.25rem; }
  .statement-grid + .statement-grid { margin-top: 10rem; }
  .about-head { grid-column: 1 / 13; }
  .about-lead { grid-column-end: 6; }
  .about-body { grid-column: 7 / 12; }
  .statement-head { font-size: 4rem; }
  .statement-body { font-size: 0.85rem; }
}

/* Portrait parks the orb as a crest across the top third of the frame, so the
   opening beat grows to stand the copy clear below it — sized against that
   crest (~29vh since uPortrait's 2026-08-14 re-cut to 1.25; see SWEEPS in
   js/main.js — the two numbers move together), not the type. Same media terms
   as the runway's portrait tier, so both switch on one edge. */
@media only screen and (max-width: 992px) and (orientation: portrait) {
  .statement-w .container { padding-top: 46vh; }
}

/* the narrowest tier drops the paragraph row into a column */
@media only screen and (max-width: 479px) {
  .statement-w .container {
    padding-left: 3vw;
    padding-right: 3vw;
  }
  .statement-grid + .statement-grid {
    display: flex;
    flex-direction: column;
    margin-top: 2rem;
  }
}

/* The heading's phone measure: sized off the frame, which holds
   characters-per-line where a fixed size would not — four lines at 360, 393
   and 430 alike. The divisor is a cliff, not a slope (7.9vw still sets four
   lines, 8.0vw sets five), so it sits one step below the edge where the
   fallback face cannot tip it, and the binding constraint is the longest
   adjacent PAIR of words: re-measure after any rewrite. Bounded at 820, a
   derived bound (64 / 0.078) where this meets the tier above's 64px within a
   twentieth of a pixel. Must follow the 992 tier: equal specificity. */
@media only screen and (max-width: 820px) {
  .statement-head { font-size: 7.8vw; }
}

/* Short landscape frames — a phone on its side, a foldable opened flat. The
   tier above assumes a tall frame: its 10rem decks and display sizes ask for
   ~670px of height, which on a 390px-tall screen runs the copy a full extra
   viewport past the section and over scene 3. Everything here is budgeted in
   vh, the unit the section's own height is stated in, so the block fits its
   one viewport by construction whatever the address bar is doing. Worst case
   sums under 90vh: 12 padding + 4 heading lines at 13 + 8 beat + paragraphs.
   Must follow the 992 and 820 tiers: equal specificity. */
@media only screen and (max-width: 992px) and (orientation: landscape) and (max-height: 700px) {
  /* the px floor clears the fixed wordmark: top max(20px,1.5em) + a 30px cap
     height puts its bottom near 50px, and 16vh alone dips under that at 390 */
  .statement-w .container { padding-top: max(16vh, 76px); }
  .statement-grid + .statement-grid { margin-top: 8vh; }
  .statement-head { font-size: min(4.6vw, 13vh); }
}

/* PROJECTS RING ---------------------------------------------------------- */

/* Behind the orb, level with the contour field — markup order settles the
   tie. Hidden and inert until js/projects-carousel.js reveals it, by
   `visibility` rather than `display`, so it never re-lays-out on the way in. */
.pcar-w {
  position: fixed;
  inset: 0;
  z-index: -3;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  will-change: opacity;
  /* how far the clip box reaches past the band, per side — see .pcar-view */
  --pcar-bleed: 120px;
}

/* --stage4-lift is written by js/main.js: scene 4 seats the band above the
   delivery card, so the plate rises with the orb's recentre by the same
   measured amount and the two never part. Rest value 0 — every other scene
   reads this as a no-op. */
.pcar-plate {
  position: relative;
  width: 100%;
  height: 563px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  transform: translateY(var(--stage4-lift, 0px));
}

/* its own box rather than the plate's background: the cards enter by
   occlusion, a full-width glow has nothing to hide behind, and opacity on the
   plate would drag every card with it */
.pcar-pool {
  position: absolute;
  inset: 0;
  background: radial-gradient(50% 31% at 50% 50%, #fff 0%, rgba(255, 255, 255, 0) 100%);
  opacity: 0;
  will-change: opacity;
}

.pcar-stage { position: relative; width: 100%; height: 100%; }

/* The edge feather, on a box taller than the band. Cards dissolve out of the
   frame rather than leaving it: 6% a side, against the WIDTH, so the feather
   widens with the screen. The box bleeds top and bottom because a card turned
   steeply toward the camera projects taller than its centre plane — 595.6px
   against a 563px band at the extreme — and a straight horizontal edge would
   shave the steepest corners. Symmetric, so the perspective origin (the
   centre of this box) does not move. */
.pcar-view {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(-1 * var(--pcar-bleed));
  bottom: calc(-1 * var(--pcar-bleed));
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 94%, rgba(0, 0, 0, 0) 100%);
  mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 6%, rgba(0, 0, 0, 1) 94%, rgba(0, 0, 0, 0) 100%);
}

/* The 3D spine: feather > perspective > centre > ring > seats. `perspective`
   resolves against the box it is declared on, so it gets a box that is
   exactly the clip box, origin stated outright; its VALUE comes from the
   module, which changes it with the ring at the breakpoint. `preserve-3d`
   must be unbroken from here down to the cards — any box between that omits
   it flattens the ring into a row of rectangles. */
.pcar-persp {
  position: absolute;
  width: 100%;
  height: 100%;
  perspective-origin: 50% 50%;
}

.pcar-centre {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100%;
  height: 100%;
  transform: translate(-50%, -50%);
  -webkit-transform: translate(-50%, -50%);
}

.pcar-centre,
.pcar-ring {
  transform-style: preserve-3d;
  -webkit-transform-style: preserve-3d;
}

/* the seat carrier, deliberately never rotated: the spin is baked into each
   seat's rotateY, and a rotation here would sit outside the depth translate
   that parks a waiting card behind the orb */
.pcar-ring { position: absolute; width: 100%; height: 100%; }

/* one seat, transform per-card from JS. backface-visibility is load-bearing —
   without it the far wall of the ring draws through the near one */
.pcar-holder {
  position: absolute;
  left: 50%;
  top: 50%;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}

/* 300px wide at 9:16; height is left to `aspect-ratio` so the browser's own
   division supplies the subpixel rounding */
.pcar-card {
  position: relative;
  width: 300px;
  aspect-ratio: 0.5625;
  border-radius: 8px;
  overflow: clip;
  /* the empty plate — what a card is until PROJECTS gives it media */
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    linear-gradient(160deg, rgba(233, 236, 240, 0.14) 0%, rgba(201, 206, 214, 0.05) 52%, rgba(14, 16, 20, 0.16) 100%),
    var(--ink-raised);
  box-shadow: inset 0 0 0 1px var(--rule);
}

.pcar-card img,
.pcar-card video { width: 100%; height: 100%; object-fit: cover; display: block; }

.pcar-tag {
  font-family: "Space Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700;
  font-size: 22px;
  letter-spacing: -0.04em;
  color: var(--rule-strong);
}

/* Below 810 the ring is shorter and much tighter (RING.mobile), and the
   numbers move together: ten seats at 36deg on a 300 radius put neighbours
   185px apart, so 180 is as wide as a card can be; the band must contain the
   card it now holds; the bleed keeps the desktop clearance over the seats'
   rise. The card holds 9:16 at every width — every file in ./media/ is
   authored at it, and a squarer box under `object-fit: cover` would crop 30%
   of each screenshot's height away. The condition pairs with a matchMedia in
   the module, written as the same negation so the two cannot drift: a rounder
   number moves which side an 810px device lands on. */
@media not all and (min-width: 810px) {
  .pcar-w { --pcar-bleed: 90px; }
  .pcar-plate { height: 360px; }
  .pcar-card {
    width: 180px;
    border-radius: 6px;
  }
  .pcar-tag { font-size: 15px; }
}

/* SERVICES SPHERE AND CHART ---------------------------------------------- */

/* Level with the field and the ring, last of the three in markup so it paints
   over the field it covers. Nothing is sized here: the composition is stated
   once, in the module, and converted against the camera's own frustum — a
   second statement in CSS is how the body and the orb seated inside it end up
   in different places at an untested width. */
.sphere-w {
  position: fixed;
  inset: 0;
  z-index: -3;
  pointer-events: none;
}

.sphere-w canvas { display: block; width: 100%; height: 100%; }

/* The chart's type is DOM, not texture: it must stay crisp at any size and be
   real text for the mirror to rasterise. z-index 1, after the content layer in
   markup, so this is the one layer that draws over the orb. Sizes come off
   --svc-arm, the chart's own axis arm, written each frame by
   js/services-chart.js. font-synthesis is off: no faked weight or slope. */
.svc-type {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  --svc-arm: 300px;

  font-family: "Helvetica Neue", "Nimbus Sans", Helvetica, Arial, sans-serif;
  font-weight: 400;
  letter-spacing: normal;
  font-synthesis: none;
  font-kerning: normal;
}

/* Each label is hung by a matrix whose translation column is its point on the
   sphere, so the element's LOCAL ORIGIN must land there: transform-origin is
   the corner, and the centring is a translate at the tail of that matrix and
   nothing else. will-change names OPACITY ONLY — hinting the transform keeps
   one raster and re-transforms it, and these labels are magnified by the
   perspective, so glyphs would be rasterised at layout size and scaled up.
   Hinting stays on: nothing here is tilted, and at thirteen pixels a stem
   across two pixel boundaries is a grey smear. */
.svc-type > * {
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: 0 0;
  white-space: pre-line;
  text-align: center;
  opacity: 0;
  will-change: opacity;
  text-rendering: auto;
}

/* the four cells are the diagram's heavy rank: a ratio of the arm, with --m
   carrying each label's perspective magnification, and width:auto so a
   two-line cell still sits square over the point it hangs from */
.svc-cell {
  font-size: max(14px, calc(var(--svc-arm) * 0.0784 * var(--m, 1)));
  line-height: 1.2;
  letter-spacing: 0.014em;
  font-weight: 400;
  color: #fff;
  width: auto;
}

/* The lower-left cell is the one label that is not a service — it is where the
   studio stands, and the only one printed ON the orb — so it takes the voice
   the page opens in. 1.18 would match the two x-heights; 1.06 is deliberately
   short of that, because a serif at a matched x-height reads LARGER. Tracking
   to zero, since a display face carries its own fit.
   Equal specificity with .svc-cell — it must stay after it. */
.svc-cell--serif {
  font-family: "PP Grafier", Georgia, "Times New Roman", serif;
  font-size: max(15px, calc(var(--svc-arm) * 0.0784 * 1.06 * var(--m, 1)));
  line-height: 1.08;
  letter-spacing: 0;
  font-weight: 400;
}

/* the slash stays in the grotesk: a display face draws its solidus as a
   hairline, which at twenty pixels on polished chrome is not there, and with
   font-synthesis off there is no heavier cut to reach for */
.svc-slash {
  font-family: "Helvetica Neue", "Nimbus Sans", Helvetica, Arial, sans-serif;
  font-size: 0.84em;
  margin: 0 0.09em;
  vertical-align: baseline;
}

/* the axis words, one rank below the cells by size and colour. A flat grey,
   not white at an opacity — the reveal writes opacity on these every frame,
   and a tint expressed the same way would be multiplied by the animation. */
.svc-cap {
  font-size: max(12px, calc(var(--svc-arm) * 0.0579 * var(--m, 1)));
  line-height: 1;
  letter-spacing: 0.012em;
  font-weight: 400;
  color: #dcdcdc;
  width: auto;
  white-space: nowrap;
}

/* DELIVERY CARD ---------------------------------------------------------- */

/* Scene 4's lower deck — the card standing under the browse's word, driven by
   that word's own reveal scalar (see setDeliveryReveal in js/main.js). This
   is the page's one card-UI composition, so it is set in px, not the root em:
   a card is furniture, and furniture does not scale with the frame the way
   display type does. Its opaque face carries its own palette so it reads over
   whatever the stage behind it is showing.

   The wrapper spans the frame only to place the plate — bottom-anchored,
   clear of the word above it — and NEVER takes the pointer; only the plate
   does, once .is-in says the scene is showing it. So the ripples and the
   word's erase keep every pixel around the card.

   Hidden rather than transparent, like the contact layer: a focusable button
   inside an invisible layer is still in the tab order. Opacity is written per
   frame by the module; zero here is the pre-script state. */
.dlv-w {
  position: fixed;
  inset: 0;
  z-index: 1;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  padding: 0 clamp(24px, 3.4vw, 66px) 5vh;
  color: #2b2b2b;
  font-family: "Instrument Sans", "Helvetica Neue", "Nimbus Sans", Helvetica, Arial, sans-serif;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  will-change: opacity, transform;
}
.dlv-w.is-in { visibility: visible; }
.dlv-w.is-in .dlv-card { pointer-events: auto; }

/* one rounded plate: a narrow header column and the project cards beside it */
.dlv-card {
  width: 100%;
  max-width: 1508px;
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 48px;
  background: #fafafa;
  border-radius: 24px;
}

.dlv-head { flex: 0 0 208px; }

.dlv-title {
  font-size: 24px;
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 0.9;
  margin-bottom: 8px;
}

.dlv-note {
  font-size: 14px;
  line-height: 1.3;
  color: #656565;
}

/* A small dark chip in a mono voice — machine tag, not display type. No mono
   face ships with the site, so the system's own carries it. Inverting on
   hover, with a hairline so the chip does not dissolve into the plate. */
.dlv-cta {
  margin-top: 16px;
  font-family: "DM Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  line-height: 1;
  color: #fafafa;
  background: #2b2b2b;
  border: 0;
  border-radius: 6px;
  padding: 10px 12px;
  cursor: pointer;
  box-shadow: inset 0 0 0 1px #2b2b2b;
  transition:
    background-color 0.25s cubic-bezier(0.22, 1, 0.36, 1),
    color 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}
.dlv-cta:hover,
.dlv-cta:focus-visible {
  background-color: #fafafa;
  color: #2b2b2b;
}
.dlv-cta:focus-visible {
  outline: 2px solid #2b2b2b;
  outline-offset: 2px;
}

.dlv-projects {
  flex: 1 1 auto;
  display: flex;
  gap: 24px;
  list-style: none;
}

/* each project is its own white card: copy at the top, facts at the foot */
.dlv-project {
  flex: 1 1 0;
  min-width: 0;
  min-height: 140px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 16px;
  padding: 16px;
  background: #ffffff;
  border-radius: 16px;
}

.dlv-desc {
  font-size: 17px;
  line-height: 1.3;
  padding: 8px;
}

.dlv-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.dlv-meta {
  display: flex;
  gap: 8px;
  min-width: 0;
}

/* the facts as quiet pills in the sheet's ground colour */
.dlv-pill {
  display: flex;
  align-items: center;
  height: 24px;
  padding: 0 12px;
  font-size: 13px;
  letter-spacing: 0.03em;
  background: #f1f1f1;
  border-radius: 12px;
  white-space: nowrap;
}

.dlv-nda {
  font-size: 12px;
  text-transform: uppercase;
  color: #a2a2a2;
}

/* the outbound mark — a real anchor to the delivered site, in a new tab; the
   NDA rows carry the quiet label instead */
.dlv-link {
  display: block;
  flex: 0 0 auto;
  width: 13px;
  height: 13px;
  color: #a2a2a2;
  cursor: pointer;
  transition: color 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}
.dlv-link:hover { color: #2b2b2b; }
.dlv-link svg { display: block; width: 100%; height: 100%; }

/* ── narrow frames: the same composition, one gesture deep ──────────────
   Below 1280 the header rises above the row and the row becomes a snap
   strip: the desktop's four-across survives as four-in-a-row you SWIPE, the
   next card's edge peeking past the plate as the invitation. The strip
   bleeds through the plate's own padding — negative margin, matched scroll
   padding — so at rest the first card sits flush with the header, and
   mid-swipe the cards run to the plate's edge instead of stopping short of
   it. The plate stays SHORT this way, which is the point: every tier keeps
   the desktop seating, bottom-anchored under the word with the stage in
   view, rather than trading the scene for a list. */
@media only screen and (max-width: 1280px) {
  .dlv-card {
    display: block;
    padding: 24px;
  }
  /* the header column folds into a bar: name and note left, chip right */
  .dlv-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 8px 16px;
    align-items: center;
    margin-bottom: 18px;
  }
  .dlv-title { margin-bottom: 0; }
  .dlv-note { grid-row: 2; }
  .dlv-cta {
    grid-column: 2;
    grid-row: 1 / 3;
    justify-self: end;
    margin-top: 0;
  }
  .dlv-projects {
    flex-wrap: nowrap;
    gap: 12px;
    overflow-x: auto;
    margin-inline: -24px;
    padding-inline: 24px;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 24px;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    /* glass reads the peeking card; a bar would be furniture on it */
    scrollbar-width: none;
  }
  .dlv-projects::-webkit-scrollbar { display: none; }
  /* The card keeps close to its DESKTOP measure rather than scaling to the
     viewport — a card stretched to the frame lays its copy out flat in one
     line over empty plate, where the desktop card's ~200px measure wraps it
     into two or three and the card fills itself. Narrower cards also show
     more of the neighbour, which is the strip's whole affordance. */
  .dlv-project {
    flex: 0 0 min(240px, 62vw);
    min-height: 150px;
    scroll-snap-align: start;
  }
  .dlv-desc { font-size: 15px; }
  .dlv-pill {
    height: 22px;
    padding: 0 10px;
    font-size: 12px;
    border-radius: 11px;
  }
}

/* A pointer that can hover has no swipe — it gets the cards as a DRAGGABLE
   surface instead (js/main.js), and the grab cursor is the whole affordance:
   no bar, same as glass. */
@media only screen and (max-width: 1280px) and (hover: hover) {
  .dlv-projects { cursor: grab; }
  .dlv-projects.is-dragging {
    cursor: grabbing;
    user-select: none;
  }
}

/* the narrowest tier tightens the plate, not the pattern */
@media only screen and (max-width: 640px) {
  .dlv-w { padding: 0 14px 4vh; }
  .dlv-card {
    padding: 20px;
    border-radius: 20px;
  }
  .dlv-head { margin-bottom: 14px; }
  .dlv-title { font-size: 21px; }
  .dlv-note { font-size: 13px; }
  .dlv-projects {
    gap: 10px;
    margin-inline: -20px;
    padding-inline: 20px;
    scroll-padding-inline: 20px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .dlv-cta,
  .dlv-link { transition: none; }
}

/* CONTACT ---------------------------------------------------------------- */

/* Pinned, not laid out: the runway's last viewport is a full screen AFTER the
   sheet finishes unrolling, so a flowed contact scene would arrive late.
   Fixed and faded against the morph, it arrives WITH the flattening (see
   applyContact in js/main.js). It covers the frame, so it must not take the
   pointer; only the two links do, once .is-in says they are on screen.
   --gutter repeats the wordmark's inset expression, so the copy stops where
   the chrome does. --line is the block's unit, and its divisor is measured:
   the address is the longest string at 10.171em, so 10.35 would span the
   gutters and 11.5 is that taken down a tenth. The 4em cap is the quieter
   voice for wide frames: em is viewport-cut (100vw/94.5 above 992px), so the
   type still scales, but the address holds at 10.171 × 4 ≈ 40.7em against a
   90.7em run — under half the frame instead of all of it. Below 992px the
   run term is already the smaller, so phones keep the full-width fit. */
.contact-w {
  --gutter: max(24px, 1.9em);
  --line: min(calc((100vw - 2 * var(--gutter)) / 11.5), 4em);

  position: fixed;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  padding: 0 var(--gutter);
  color: var(--ink);

  /* hidden rather than transparent: an invisible mailto: at the top of the
     page is still in the tab order */
  visibility: hidden;
  pointer-events: none;
}
.contact-w.is-in { visibility: visible; }
.contact-w.is-in .contact-line { pointer-events: auto; }

.contact { width: 100%; text-align: center; }

/* pre-script state; opacity and rise are written per frame, keyed to
   [data-in] — the same list the module walks */
.contact-w [data-in] { opacity: 0; will-change: opacity, transform; }

/* the only small thing in the block, so it carries the gesture: tracking OUT
   rather than in. The gap is the larger of a fraction of the type and a
   fraction of the frame — --line is capped by WIDTH, so on a tall phone the
   type stops growing while the frame keeps going. */
.contact-eyebrow {
  font-family: "Space Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700;
  font-size: max(14px, calc(var(--line) * 0.19));
  letter-spacing: 0.01em;
  line-height: 1;
  margin-bottom: max(calc(var(--line) * 0.34), 4.5vh);
}

/* fit-content with auto margins, not a full-width centred block: the underline
   is a background sized in % of the ELEMENT, so a full-width line would rule
   the frame instead of the words. Background rather than text-decoration,
   which cannot be animated across its own length — it grows from the left,
   the direction the line is read in. */
.contact-line {
  display: block;
  width: fit-content;
  margin-inline: auto;
  font-family: "Space Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700;
  font-size: var(--line);
  letter-spacing: -0.045em;
  line-height: 0.9;
  color: var(--ink);
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 0.055em;
  transition: background-size 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

/* leading of 0.9 packs the lines tighter than the glyphs are tall, so the gap
   goes between the two rather than into a looser line-height */
.contact-line + .contact-line { margin-top: 0.06em; }

/* the @ is the only character in the address that descends. Nudged by
   position rather than a transform, which needs inline-block and changes how
   the glyph kerns against its neighbours. */
.contact-line .at { position: relative; top: -0.045em; }

.contact-line:hover,
.contact-line:focus-visible { background-size: 100% 0.055em; }

@media (prefers-reduced-motion: reduce) {
  .contact-line { transition: none; }
}

/* SCROLL RUNWAY ---------------------------------------------------------- */

/* Every camera beat is scrubbed against page scroll, so the camera runs only
   as far as the document is tall. --scenes is the remainder of the
   10.5-viewport runway after the hero's own viewport; the pinned contact copy
   rides it rather than adding to it. --scene4-hold is written once from
   js/main.js — the factor by which the scene's pauses lengthen the camera
   timeline — and stretching the runway by exactly that keeps scroll distance
   per timeline unit constant; it rests at 1, so a blocked script gets the
   runway as it was. The statement's flow comes off OUTSIDE that product: it
   is real document, not stretched timeline, and subtracting it inside would
   give back twice what was taken. */
.stage-scroll {
  --scenes: 9.5;
  --scene4-hold: 1;
  height: calc(var(--scenes) * var(--scene4-hold) * 100vh - var(--scene2-flow, 0vh));
  pointer-events: none;
}

/* beats are scrubbed against PROGRESS, not distance, so a shorter runway
   compresses the sequence without moving any beat relative to another: a
   portrait phone gets eight screens rather than ten and a half, and the hero
   keeps its own, so the remainder is seven */
@media only screen and (max-width: 992px) and (orientation: portrait) {
  .stage-scroll { --scenes: 7; }
}

/* THE STATIC PAGE --------------------------------------------------------
   js/main.js writes .static-page onto <html> when the scenes cannot run: a
   browser whose WebGL boot threw, or a rendering crawler (navigator.webdriver
   — it renders but never scrolls, so scroll-revealed layers would stay hidden
   in its snapshot). The same shape the noscript block in index.html states —
   everything in flow and visible, the WebGL layers gone — except the pricing
   chip stays: script is alive on this path and the modal works. If the
   noscript rules change, change these with them. */
html.static-page body { background-color: #e3e5ea; color: #1d1d1d; }
html.static-page .loader-w,
html.static-page .stage-scroll,
html.static-page .core-w,
html.static-page .field-w,
html.static-page .reveal-w,
html.static-page .pcar-w,
html.static-page .sphere-w,
html.static-page .svc-type { display: none; }
html.static-page .hero-headline { color: #1d1d1d; }
html.static-page .contact-w {
  position: static;
  visibility: visible;
  pointer-events: auto;
  padding-block: 12vh;
}
html.static-page .contact-w [data-in] { opacity: 1; }
html.static-page .contact-w .contact-line { pointer-events: auto; }
html.static-page .dlv-w {
  position: static;
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
  transform: none;
}
html.static-page .dlv-card { pointer-events: auto; }
