/* ─────────────────────────────────────────────────────────────────────────
   Rules under "verbatim" headings are copied byte-for-byte from
   offbrand-2023.shared.0746f2a75.min.css and the site's inline <style> blocks.
   Everything else is the stage the orb runs on.
   ───────────────────────────────────────────────────────────────────────── */

/* ── verbatim: global reset + variables ──────────────────────────────────── */

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

body {
  font-size: 1.0582010582010584vw;
}

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

:root {
  --main-dark: #1d1d1d;
  --main-dark-alt: #252525;
  --main-light: #e5e4e0;
  --main-light-alt: #f1f0ec;
  --primary-color: #ff642f;
  --dark-grey: rgba(218, 218, 218, 0.2);
  --dark-grey60: rgba(218, 218, 218, 0.6);
  --light-grey: rgba(111, 111, 111, 0.2);
  --light-grey60: rgba(111, 111, 111, 0.6);
  --grey-rotate: #bfbebe;
  --large-radius: 6.25em;
  --med-radius: 1.375em;
  --small-radius: 0.625em;
  --perspective: 4000px;
  --grid-inner-scale: 1;
  --grid-item-ratio: 1.5;
}

::selection {
  background: var(--main-dark);
  color: var(--main-light);
  text-shadow: none;
}

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

[pointer-none] {
  pointer-events: none !important;
  user-select: none;
}

/* THE SCROLLPORT IS DECLARED HERE, and it has to be, because body's overflow
   does not stay on body. Whenever html computes to `visible` in both axes the
   used values of BODY's overflow propagate to the viewport — so the `clip`
   below was not clipping the document, it was making the viewport itself a
   non-scrolling box.

   That failure was silent on a desktop and total on a phone. The wheel kept
   working, which is what hid it: Lenis intercepts wheel and drives
   `window.scrollTo`, and a programmatic scroll is still honoured on a clipped
   viewport. Native one-finger touch panning is not — the compositor simply
   refuses a box that declares it does not scroll — so a phone could only move
   the page with the TWO-finger gesture, which pans the visual viewport and
   never asks the layout viewport's opinion. (Isolated by experiment: with
   Lenis destroyed and this rule left alone, touch still does nothing; with
   Lenis untouched and the propagation stopped, touch scrolls immediately. The
   smooth-scroll layer was never involved.)

   Naming BOTH axes on html stops the propagation — the viewport takes html's
   values instead — and html then keeps the horizontal containment the clip was
   here for. `clip` beside `auto` computes to `hidden` per the overflow
   shorthand's pairing rule, which is the same containment one scroll axis
   short. body keeps its own clip for its own subtree; fixed layers (the orb,
   the contact block, the sphere and the LANDO canvases) are unaffected either
   way, since their containing block is the viewport and no ancestor here
   establishes one. */
html {
  overflow-x: clip;
  overflow-y: auto;
}

body {
  overflow: clip;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ── loader ──────────────────────────────────────────────────────────────── */

/* An opaque sheet of the field's resting grey (must track
   PALETTE.BACKGROUND in js/lando-hero.js) under one bar in the ink —
   nothing else. The fill is REAL PROGRESS now, not a clock: js/main.js
   writes scaleX from the weighted ledger in js/boot-progress.js.

   Still compositor-driven, and for the original reason — a rAF fill was
   tried first and boot jank (shader compiles block the main thread) froze
   it mid-bar. The answer is the same either way: the main thread only
   STEPS the value, and a transform TRANSITION carries it between steps on
   the compositor, which keeps moving through a blocked main thread. */
.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;
}

.loader-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--main-dark);
  transform: scaleX(0);
  transform-origin: left;
  /* Long enough that a burst of stream callbacks reads as one continuous
     glide rather than a stutter, short enough that the last step does not
     hold the sheet up after everything is in. Eased out only — progress
     that decelerates into each step reads as loading; linear reads as a
     clock, which is exactly what this stopped being. */
  transition: transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}

/* ── site chrome: the wordmark ───────────────────────────────────────────── */

/* Self-hosted (fonts/): the page must not touch a CDN at runtime.
   font-display: block — a wordmark may not flash Helvetica while it loads. */
/* ── the services chart's face ───────────────────────────────────────────
   Nimbus Sans (URW, AGPL + font exception), self-hosted.

   biotope sets its figure in HELVETICA NEUE — measured, not assumed: their
   labels are outlined paths in the SVG, and the ink widths, the x-height to
   cap ratio and the stem weights all resolve to it. Their live text is served
   as helvetica-neue-lt-pro from an Adobe kit that is domain-locked to them, so
   naming it here buys nothing on its own.

   Nimbus Sans is URW's Helvetica — the same drawing, metrically identical, and
   free to serve. It is the closest thing to their face that can actually be
   shipped, and it is a world away from what was here before it: Inter, which
   is a fine typeface and the wrong one, and before that the Arial the browser
   was quietly substituting because the stack named a font nobody could load.

   The stack still names their kit first and system Helvetica Neue second, so a
   licensed kit or a Mac gets the real thing; everyone else gets Nimbus. Nobody
   gets Arial. */
@font-face {
  font-family: "Nimbus Sans";
  src: url("../fonts/NimbusSans-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Nimbus Sans";
  src: url("../fonts/NimbusSans-Bold.otf") format("opentype");
  font-weight: 700;
  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;
}

/* The hero headline's face — seabrook.studio's display serif, the same file
   their hero serves. Block for the same reason as the wordmark. */
@font-face {
  font-family: "PP Grafier";
  src: url("../fonts/PPGrafier-RegularDisplay.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

/* ── scene 2's face ──────────────────────────────────────────────────────
   "Ataero Retina OB Edition" — the file itsoffbrand.com serves, an
   OFF+BRAND-custom cut of AT Aero (the "dot edit"). One weight, 400, which
   is every weight their section uses. Self-hosted like everything else
   here — the page must not touch a CDN at runtime.

   The stack on .about-w falls through to Helvetica and Nimbus, so pulling
   this file degrades the scene without breaking it. Block, not swap — a
   display face this distinctive may not flash Helvetica first, and the
   loader's second covers the fetch. */
@font-face {
  font-family: "Ataero Retina OB Edition";
  src: url("../fonts/AtAero-Retina-dot-edit.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

/* Above every layer (.main-w content sits at 1), on screen for 100% of the
   runway — including the full-bleed chrome finale, where near-black still
   reads on the silver sheet. em-sized so it tracks the site's vw-based type
   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(--main-dark);
}

/* ── verbatim: orb ───────────────────────────────────────────────────────── */

/* DEVIATION from the site, which switches this to `position: absolute` below
   992px. Absolute resolves `inset: 0` against the document, not the viewport,
   so the orb would sit near the centre of a page thousands of pixels tall and
   scroll away with the content — nothing left for a scroll-scrubbed camera to
   move. The site can afford that because it runs no orb timelines on mobile at
   all; we run the same ones everywhere, so it stays pinned at every width. */
.orb-w {
  z-index: -2;
  justify-content: center;
  align-items: center;
  display: flex;
  position: fixed;
  inset: 0%;
}

/* Rest size, and a FALLBACK ONLY.
   js/stage.js measures the same thing and writes it back in px — the sweeps are
   stated in orb diameters, so the number has to exist in JS, and having it in
   two places is how the old mobile column drifted out of step with the CSS it
   was sized against. These rules are here so the pre-script layout is already
   close enough that nothing jumps when the module lands. */
.orb {
  z-index: -1;
  border-radius: 100%;
  width: 80vh;
  height: 80vh;
  min-width: 47.62vw;   /* the site's `min-width: 45em` at its own 1.0582vw root */
  min-height: 47.62vw;
  position: absolute;
}

/* Portrait wants the short edge at the same proportion the desktop gives it:
   80% — the subject carries the same weight on every screen. Must track
   REST_FRACTION in js/stage.js, which measures the same thing in px. */
@media (orientation: portrait) {
  .orb {
    width: 80vw;
    height: 80vw;
    min-width: 0;
    min-height: 0;
  }
}

.orb canvas {
  width: 100% !important;
  height: 100% !important;
  background-color: transparent !important;
}

/* ── verbatim: page ──────────────────────────────────────────────────────── */

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

/* One look only — Mercury on dark. The site's light scheme and its theme
   crossfade are not used here. */
body {
  background-color: var(--main-dark);
  color: var(--main-light);
}

/* ── stage ───────────────────────────────────────────────────────────────── */

/* What is left is what the orb needs from the document, and nothing else.
   Scenes get built inside .main-w — see index.html. */

body {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 400;
  line-height: 1.4;
  cursor: auto;
}

/* the content layer: the orb wrap sits at z-index -2, so anything built in
   here renders in front of it */
.main-w {
  position: relative;
  z-index: 1;
}

/* ── LANDO HERO layers ───────────────────────────────────────────────────── */

/* The field, BELOW the orb wrap (-2): the orb draws over it the way Lando's
   face draws over his background. */
.lando-hero-w {
  position: fixed;
  inset: 0;
  z-index: -3;
  pointer-events: none;
}

/* The reveal, ABOVE the orb, below the content (1): the fluid trail and the
   3D object it uncovers — the helmet mechanism. Transparent everywhere the
   trail is not. */
.lando-reveal-w {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

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

/* ── SCENE 4: projects carousel ──────────────────────────────────────────── */

/* The ring lives BEHIND the orb. -3 puts it under the orb wrap (-2) and level
   with the LANDO field, which is why it comes after that block in index.html:
   equal z-index, so document order breaks the tie. The two never share a
   moment anyway — the field belongs to the hero and this belongs to the beat
   where the orb has come home to the middle — but ordering it deliberately
   costs nothing and means neither has to know about the other.

   Hidden and inert until js/main.js reveals it; `visibility` rather than
   `display` so the ring keeps its layout and 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;
}

/* ── verbatim: the plate ──────────────────────────────────────────────────
   Their band is full-bleed wide and a fixed height, with a soft white pool
   behind the cards. The gradient is copied as authored — the computed value
   drops `at 50% 50%` because it is the default, and putting it back is what
   makes the rule readable rather than lucky.

   The pool is its own element rather than the plate's background, because it
   fades on its own clock: the cards enter by occlusion (the orb hides the
   stack), but a full-width glow has nothing to hide behind, so it has to
   breathe with the reveal — and opacity on the plate itself would drag every
   card with it. js/projects-carousel.js writes the pool's opacity per frame;
   0 here is the pre-JS park, same contract as .pcar-w above. */
.pcar-plate {
  position: relative;
  width: 100%;
  height: 563px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

.pcar-pool {
  position: absolute;
  inset: 0;
  background: radial-gradient(50% 31% at 50% 50%, rgb(255, 255, 255) 0%, rgba(171, 171, 171, 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 do not leave the frame, they dissolve out of it. 6% and 94% are
   theirs, measured on the mask of the scrolling viewport at every width — the
   stops are percentages of the WIDTH, so the feather widens with the screen
   instead of holding a fixed pixel margin, and nothing about it cares how tall
   this box is.

   DEVIATION, and the reason for --pcar-bleed: their clip box is exactly the
   563px band, and a card turned steeply toward the camera does not fit in it.
   A card is a flat rectangle, but its PROJECTED bounding box is taller than
   its centre plane — the near vertical edge sits closer to the camera than the
   far one and is drawn bigger. Swept through a full revolution the tallest
   front-facing card measures 595.6px against a 563px box, so the top and
   bottom corners of the steepest cards were being shaved off by a dead
   straight horizontal line. Bleeding the clip box past the band on both sides
   gives every card room to be a whole object.

   Symmetric on purpose: `perspective` resolves its origin at the centre of
   .pcar-persp, and the ring and its seats are centred on that same box, so
   growing it equally top and bottom leaves the projection untouched. Verified
   against the live original after the change — still 0.01px across every seat.

   The bleed is transparent and unmasked vertically, so it costs a slightly
   larger composited layer and nothing else. */
.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, box for box: feather > layer > align > persp > centre > ring.

   Two rules decide whether this is a cylinder or a flat row of rectangles, and
   both are easy to lose:

   `perspective` resolves against the box it is declared on and its origin
   defaults to that box's centre, so it needs a box of its own that is exactly
   the band — hence .pcar-persp doing nothing else. The VALUE is written from
   JS, because it changes with the ring at the breakpoint and the two have to
   move together.

   `preserve-3d` must be unbroken from the perspective down to the cards. Every
   box between them that omits it FLATTENS everything below into a single plane
   — the cards keep their transforms but lose their depth, and what you get is
   eight full-size rectangles in a row. It belongs on .pcar-centre and
   .pcar-ring, and on nothing above .pcar-persp, where it would do nothing. */
.pcar-layer {
  position: absolute;
  width: 100%;
  height: 100%;
}

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

/* The entrance is per card, not per ring — .pcar-centre is a fixed box and
   nothing writes to it. See ProjectsCarousel._applyGeometry: each seat carries
   its own depth, radius and opacity, staggered, so the ring assembles one card
   at a time instead of unfolding as one piece. */

.pcar-persp {
  position: absolute;
  width: 100%;
  height: 100%;
}

.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 own rotateY instead. A rotation here would sit outside each holder's
   transform, and therefore outside the depth translate that parks a waiting
   card behind the orb, so at half a turn "away from the viewer" would come out
   as "toward the viewer". See ProjectsCarousel._applyGeometry. */
.pcar-ring {
  position: absolute;
  width: 100%;
  height: 100%;
}

/* A seat on the cylinder — the transform is per-card and written from JS.
   backface-visibility matters: without it the far wall of the ring draws
   through the near one and the band fills with mirrored cards. */
.pcar-holder {
  position: absolute;
  left: 50%;
  top: 50%;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}

/* ── verbatim: the card ───────────────────────────────────────────────────
   300px wide at 9:16, 8px corners. The height is left to `aspect-ratio` on
   purpose: their computed height reads 533.328px, which is the browser's own
   300 / 0.5625 with subpixel rounding, and asking for the ratio reproduces it
   exactly rather than to three decimal places. */
.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(--main-dark-alt);
  box-shadow: inset 0 0 0 1px var(--dark-grey);
}

.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(--dark-grey60);
}

/* ── below their 810px breakpoint ─────────────────────────────────────────
   A shorter band with a shorter ring, not the desktop one scaled: fewer cards
   and a much tighter perspective. The ring count, step, radius and perspective
   that go with these live in RING.mobile.

   DEVIATION from the reference, and the one place this breakpoint stops being
   verbatim: their mobile card is 0.8 — a squarer box than the 0.5625 they use
   on desktop. Ours carries our own client work, and every file in ./media/ is
   720x1280, i.e. 0.5625 exactly. Under `object-fit: cover` a 9:16 frame forced
   into a 0.8 box loses 30% of its height off the top and bottom — on THESE
   images that is the header and the footer of every site shown, which is most
   of what identifies the project. The reference could afford the crop because
   their media was authored for the box. So the card keeps the desktop ratio at
   every width and the whole screenshot is on screen, uncut, exactly as it is
   on desktop.

   The three numbers move together and cannot be picked separately:

     width 180   the seat spacing is the ceiling. Ten seats at 36deg on a 300
                 radius put adjacent cards 2·300·sin(18°) = 185px apart, so
                 180 is as wide as a card can be before neighbours overlap —
                 the same 1.03 clearance ratio the desktop ring runs (312
                 apart, 300 wide). Getting there needed RING.mobile.radius
                 260 -> 300; at the old radius the ceiling was 160, which is
                 where that number came from.
     height 320  180 / 0.5625, i.e. not chosen at all.
     plate 360   the band has to contain the card it now holds. Everything
                 measured off the plate follows automatically: the braid is
                 RISE_FRAC of it, so its peak goes 49px -> 72px, and the
                 bleed goes to 90 to keep the same margin over it that
                 desktop keeps (113 against 120). */
@media (max-width: 809.98px) {
  .pcar-w { --pcar-bleed: 90px; }

  .pcar-plate { height: 360px; }

  .pcar-card {
    width: 180px;
    aspect-ratio: 0.5625;
    border-radius: 6px;
  }

  .pcar-tag { font-size: 15px; }
}

/* ── SCENE 3: the sphere ─────────────────────────────────────────────────── */

/* The body's canvas. -3 puts it under the orb wrap (-2) and level with the
   LANDO field and the projects ring, which is why it comes after both in
   index.html: equal z-index, so document order breaks the tie and this one
   paints over the contour field it covers.

   Nothing is sized here. The composition is stated once, in the module, in
   screen terms, and converted to world units against the camera's own frustum
   — a second statement of it in CSS is how the body and the orb that seats
   inside it end up in different places at a width nobody measured. */
.sphere-w {
  position: fixed;
  inset: 0;
  z-index: -3;
  pointer-events: none;
}

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

/* ── SCENE 3: the chart's type ───────────────────────────────────────────

   The reference's own face and its own proportions.

   biotope serves helvetica-neue-lt-pro from an Adobe Fonts kit (jli2aba),
   weights 400 and 500, and sets this diagram at 400 with NORMAL tracking —
   which is most of why it reads quiet and considered. The kit is domain-locked
   to their site, so the stack below names the same family first and falls
   through to Helvetica Neue and Arial; add a kit of your own and the real face
   drops straight in with nothing here to change. Their display serif, for the
   record, is adobe-garamond-pro.

   Everything is sized off --svc-arm — the chart's own axis arm, measured on
   screen and written by js/services-chart.js each frame. Their cells are 24px
   against a 300px arm and their axis words 13px, so the ratios below are theirs
   and the diagram holds its internal proportions at any viewport instead of
   only at the one it was drawn at.

   z-index 1 and after .page-w in index.html so document order breaks the tie:
   this is the one layer that deliberately draws over the orb. */
/* ── the face ────────────────────────────────────────────────────────────
   biotope's English face is helvetica-neue-lt-pro — read off their own
   stylesheet, not guessed: --app-g-font-family-en in
   biotope.co.jp/wp-content/themes/biotope/dist/assets/styles/app.css resolves
   to "helvetica-neue-lt-pro", "Helvetica Neue", -apple-system, ... (their
   display serif, for the record, is adobe-garamond-pro).

   That is HELVETICA NEUE LT PRO, served from an Adobe Fonts kit. It cannot be
   copied here, and not only for licence reasons: an Adobe kit checks the
   referring domain and will not render off one it was not issued for, so
   lifting the files buys a page that silently falls through to the next name
   in the stack. Two ways to actually have it —

     1. Adobe Fonts, included with any Creative Cloud plan. Make a kit for
        brandmax's domain, add Helvetica Neue LT Pro, and put the kit's one
        <link> in index.html. NOTHING HERE CHANGES: the family is already
        first in the stack below, so it takes over the moment it loads.
     2. Licence Helvetica Neue from Monotype for self-hosting, and add a
        @font-face beside the Nimbus ones above.

   Until then the fallback is Nimbus Sans — URW's Helvetica, metrically
   compatible and free to serve. It is the same skeleton one generation back:
   Neue regularised the terminals and tightened the fit, so the difference at
   this size is real but small, and it is a far closer stand-in than anything
   else that can be shipped. What it does NOT have is a Medium, which is the
   weight biotope sets its cells in — see .svc-cell for how that is answered.

   font-synthesis is off so that a browser can never fake a weight or a slope
   this file did not ask for; the faux-medium below is measured, and a second
   synthetic weight stacked on top of it would be neither. */
.svc-type {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  --svc-arm: 300px;

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

/* Centred by the transform ALONE — a translate(-50%, -50%) at the tail of the
   matrix, against the box's own size — and by nothing else. An earlier cut
   carried negative margins as well and every label sat half its own width to
   the left of the point it was hung from, which put the whole cluster off its
   crossing and read, correctly, as badly placed.

   transform-origin is the CORNER, not the middle, and that is load-bearing
   now: js/services-chart.js hands each label a matrix3d whose translation
   column is the point on the sphere and whose basis is the tangent frame
   there. That matrix has to land the element's LOCAL ORIGIN on the surface
   point; with the default 50% 50% origin it would land the box's centre
   somewhere near it instead, and every anchor and clearance measured after
   that would be measured from the wrong place. */
.svc-type > * {
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: 0 0;
  white-space: pre-line;
  text-align: center;
  opacity: 0;

  /* NO will-change ON THE TRANSFORM, and this is the single biggest reason the
     type read as low resolution.

     An element carrying a 3D transform gets its own composited layer, and the
     browser rasterises that layer ONCE at whatever scale it believes the
     element will be drawn at. will-change: transform is a promise that the
     transform is about to change — so the compositor keeps the existing raster
     and re-transforms it rather than redrawing, which is the whole point of
     the hint and exactly wrong here. These labels are magnified by the
     perspective (about 1.2x at the cells, more at the caps), so the glyphs
     were being rasterised at their layout size and then SCALED UP. That is a
     resampled bitmap of text, and it looks like one: soft edges, mushy
     counters, stems that shimmer as the body turns.

     Dropped, the browser re-rasters at the real composited scale and the type
     is drawn as vectors at the size it is actually shown. It costs a repaint
     on the frames where the transform changes, which for eight small elements
     is nothing next to what it buys.

     opacity is still hinted — that one composites without touching the raster,
     which is the case will-change was designed for. */
  will-change: opacity;

  /* Hinting BACK ON, and this is the third of the three things that were
     costing this type its edges.

     geometricPrecision turns hinting off and takes the outlines exactly as
     drawn. That was the right call while the labels were on tilted planes —
     the snap is computed against a pixel grid the glyph is not aligned to, so
     it fights the transform and makes the spacing jitter as the label moves.
     It is the wrong call now that nothing is tilted: hinting is precisely the
     mechanism that puts a stem on a pixel boundary instead of across two, and
     at thirteen pixels a stem across two boundaries is a grey smear where a
     white line should be. Left in, it was throwing away the sharpness the
     other two fixes had just bought.

     `auto` rather than optimizeLegibility, which in Blink mostly toggles
     ligatures and kerning and has nothing useful to say here. */
  text-rendering: auto;
}

/* ── the four cells ──────────────────────────────────────────────────────
   Every number here is solved off the reference's outlines rather than guessed
   from a screenshot. Their glyph bboxes give an x-height of 7.59 units and an
   ascender of 10.21 against a 148.9-unit arm, which puts the em at 14.5 —
   0.0974 of the arm. Their two-line blocks measure 29.6 units of ink, and
   backing the ascender and descender out of that leaves a line advance of 16.4,
   which is 1.13 em.

   The TRACKING is the part that would never have come from looking. Their ink
   runs about four percent wider than Helvetica's own advances over every
   string I checked — "Business", "Organizational", "Creative / Business", all
   within a thousandth of each other — and that is a deliberate +0.03em, not an
   accident of the face. It is most of why their labels read as set rather than
   typed, and it is the opposite of the negative tracking that was here.

   Cells are the heavy weight: their stems measure 0.215 of the x-height, which
   is Helvetica Medium. The axis words below are barely half that. That
   contrast is the diagram's hierarchy and it does more work than either size.

   width:auto so each block shrinks to its own longest line — the box has to be
   the type's own size or a two-line cell stops sitting square over the point it
   is hung from. */
.svc-cell {
  /* REGULAR, down from Bold — the businesslike setting, and biotope's.

     Their figure is set in Helvetica Neue at Medium, which sits between the
     two weights Nimbus ships. Of the two available, Regular is by far the
     closer: Bold is two steps away and carries a commercial, signage weight
     that a diagram of this kind should not have. Read at size, Bold was
     announcing the four services; Regular states them, which is the difference
     between a chart and a poster.

     No faux-medium to split the difference. -webkit-text-stroke does not draw
     a heavier font — it paints an outline along the contour, so joins fill in,
     counters get eaten from both sides, diagonals thicken more than stems, and
     the shape is antialiased twice. It reads as exactly what it is, a regular
     weight with paint round it, and the last thing this type needs is another
     softening pass. Regular is honest, and the gap to Medium is small enough
     that only a specimen would show it.

     Tracking opens a touch with the lighter weight: a Regular stem leaves more
     air inside each letter than a Bold one, so the fit has to give it a little
     more between them or the word closes up.

     Size is a ratio of the arm — it holds at every viewport — and --m carries
     each label's own depth on top of it. */
  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 axis words — the field the four cells are measured in.

   TITLE CASE, normal tracking, which is what biotope sets and what this should
   have stayed. A previous cut had them as wide-tracked capitals on the
   reasoning that tracked caps are what measuring instruments set their scales
   in. True, and beside the point: letterspaced micro-caps are also the single
   most over-used gesture in generated layout, and a diagram that is already a
   quadrant chart on a black sphere cannot afford one more thing that looks
   like it came out of a machine. It is a mannerism, and mannerisms read as
   style applied rather than as decisions taken.

   Title Case at normal fit is quieter and older and it is what the reference
   does. Set properly it needs no mannerism to carry.

   FULL WHITE, and the tint is gone. Two cuts held these back — 62%, then 78% —
   on the reasoning that the axis words are the field and the field should be
   quiet. Dimming type until it stops competing is not hierarchy, it is
   apology, and it is also what was making these look cheap: a white glyph at
   78% on black is a grey glyph, its stems are a fraction of a pixel of grey at
   this size, and the antialiasing has almost nothing to resolve against. Every
   softness in the raster shows up doubled. At full white the same glyph has
   the whole range to be drawn in.

   The separation from the cells is structural instead, and it is plenty:
   0.056 of the arm against their 0.076, and Regular against their Bold. Two
   real differences beat three half-hearted ones. */
/* ── the lower-left cell ──────────────────────────────────────────────────
   Set in the hero's face — PP Grafier, the display serif from
   "Ideas in Good Hands" — where the other three quadrants stay in the
   grotesk.

   It is the one cell that is not a service. The other three name what the
   studio does and belong to the instrument; this one is where the studio
   stands, and it is the only label printed ON the orb rather than placed in a
   field. Giving it the voice the site opens in ties the object to the promise
   the page made, and makes the odd one out look chosen instead of stray.

   ── the settings are not the grotesk's ───────────────────────────────────

   A serif cannot inherit a sans's numbers. Three things have to move:

   SIZE up, but not to parity. Grafier's x-height is markedly smaller than
   Nimbus's, so at a shared font-size it would read a full step smaller than
   its neighbours rather than equal to them — 1.18 is the multiplier that
   matches the two x-heights exactly, which is the measure the eye actually
   compares.

   1.06 is deliberately short of it. Matched x-heights make a serif look
   LARGER than the sans beside it, not equal: the ascenders, descenders and
   serifs all reach past the sans's, so the block occupies more of the page at
   the same nominal size. Backing off a tenth lands the two at the same
   apparent weight on the page rather than at the same measurement. This is
   also the one cell sitting on an object rather than in open field, and it has
   a limb to stay clear of.

   TRACKING to zero. The 0.014em on the cells is there to give Regular
   grotesk stems room; a display serif is drawn with its fit built in, and
   letterspacing one is the single most reliable way to make it look
   mishandled.

   LINE-HEIGHT tighter. Two lines of a display serif at 1.2 drift apart —
   the face carries its own vertical presence in the serifs and the
   ascenders, and wants the lines closer than a sans does.

   font-display is `block` on this face (see the @font-face above), so it
   never flashes a fallback serif at a different width. */
.svc-cell.--bl {
  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 in the lower-left cell, set in the GROTESK while the words around
   it stay in the serif.

   PP Grafier is a display face, and its solidus is drawn the way a display
   face draws one: a hairline, thinner than any stem in the letters beside it,
   because in a headline at 8vw that hairline is still several pixels wide. At
   twenty pixels on polished chrome it is most of a pixel of light grey and it
   simply is not there.

   Bolding it was not available and would have been wrong anyway. Grafier ships
   one weight, and .svc-type sets font-synthesis: none — deliberately, so the
   browser can never invent a weight this file did not ask for — which means
   font-weight: 700 here would either do nothing or, with synthesis on, smear
   the glyph sideways. A synthetic bold of a hairline is a thicker hairline
   with bad joins.

   So it borrows Nimbus's, which is already loaded and is a real stroke with a
   real weight. Two adjustments make it sit rather than sit on top:

   SIZE, to 0.84em. The grotesk's larger x-height and taller solidus mean a
   matched em would stand a head above the serif's caps.

   TRACKING, a little air either side. Grafier's own sidebearings around the
   slash are set for its hairline; a heavier mark in that gap needs more room
   or the three words close up into one. */
.svc-slash {
  font-family: "helvetica-neue-lt-pro", "Helvetica Neue", "Nimbus Sans", Helvetica, Arial, sans-serif;
  font-size: 0.84em;
  margin: 0 0.09em;
  vertical-align: baseline;
}

.svc-cap {
  /* ── size: measured off biotope's own outlines ─────────────────────────
     Their axis words are not live text — they are outlined paths in the
     figure's SVG (.lbl-t/.lbl-b/.lbl-l/.lbl-r), so there is no font-size to
     read and the size has to come out of the geometry. Their flat-topped
     capitals measure 6.284 units tall against the 148.9-unit axis arm; the
     round ones come out at 6.539, which is overshoot and not a different size.

     Cap height, not em, and that distinction is the whole conversion. They set
     Helvetica Neue, whose capitals are 0.714 of the em; we set Nimbus, which is
     Helvetica, at 0.729. Matching the nominal size would leave our capitals two
     percent short of theirs. Matching the CAP HEIGHT is what actually puts the
     same amount of letter on the page:

         6.284 / 0.729 = 8.62 em units,  8.62 / 148.9 = 0.0579

     Down from 0.070, which was a size chosen to survive the small-type mush and
     is simply larger than the reference.

     ── colour ───────────────────────────────────────────────────────────
     #DCDCDC is their figure's light grey — --app-g-color-gray-light, the value
     they stroke the axis cross in.

     For the record, their axis WORDS are pure white (.fill --white,
     --app-g-color-white). Their whole figure sits on a dark plate, so white
     labels there read as quiet in a way white labels on this sphere do not,
     next to cells that are also white. Their light grey is the value in their
     own palette that does the job here — it reads as a rank below the cells
     without leaving the reference's colour set. Swap this one line for #fff if
     literal parity is wanted over the effect.

     A flat colour, not white at an opacity: the reveal writes opacity on these
     elements every frame, and a tint expressed the same way would be multiplied
     by the animation. The colour is the colour; the opacity is the animation. */
  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;
  /* shrink-to-fit and anchored by an edge, not centred on the tip — see the
     caps table in js/services-chart.js */
  width: auto;
  white-space: nowrap;
}

/* ── scenes ──────────────────────────────────────────────────────────────── */

/* Only the hero owns a viewport in the document. The other four scenes are
   camera over an empty screen, and scene 5's copy is pinned rather than laid
   out (see .contact-w). Any 100vh scene added here must come OUT of --scenes
   below so the runway total (10.5 / 8 portrait) — and with it every scroll
   beat — stays where it was tuned. */
.scene {
  height: 100vh;
  display: flex;
  align-items: center;
  padding: 0 8vw;
  color: var(--main-dark);
}

/* the first viewport belongs to the LANDO layer and the cradled orb */
/* Same not-real-text contract as the statement: the section carries
   [pointer-none] in the markup (pointer-events AND user-select off), and
   the pair is restated here with the -webkit prefix .about-w also ships,
   so Safari below 16.4 — which only knows the prefixed form — keeps it. */
.scene--hero {
  pointer-events: none;
  justify-content: center;
  -webkit-user-select: none;
  user-select: none;
}

/* seabrook.studio's hero type: PP Grafier RegularDisplay, white, one
   unbroken line. Their 10.9375vw font-size makes "Ideas reimagined" span
   exactly their 94vw container; "Ideas in Good Hands" is a longer string
   (10.489em vs 8.593em in this face), so every size here is theirs
   × 0.81924 — the line holds the same edge-to-edge fit instead of
   clipping ~10vw off each end. Their position:absolute belongs to a
   load-animation wrapper we don't run — the scene's flex centring puts
   it in the middle instead.

   The h1 is now the lockup rather than the type: it holds the statement line
   and the "Your" hung above it (.hero-line / .hero-lead below). Every number
   here still belongs to the statement line, which is the point — the lead-in
   was added without spending a pixel of it. */
.hero-headline {
  font-family: "PP Grafier", serif;
  font-style: normal;
  font-weight: 400;
  font-size: 8.9604vw;        /* their 10.9375vw × 0.81924 */
  line-height: 9.3871vw;      /* their 11.4583vw × 0.81924 */
  color: #fff;
  white-space: nowrap;
  /* the containing block for the lead-in, which hangs above it */
  position: relative;
}

/* ── the statement line ───────────────────────────────────────────────────
   The words, unchanged: this span is the element the size above was tuned
   for, and it still spans the 94vw container edge to edge.

   INLINE-BLOCK, and that is for the mirror. _rasterize (js/orb.js) sizes its
   bitmap from getBoundingClientRect and places the baseline from the computed
   line-height — the two agree on a block box, where the rect IS the line box.
   An inline span's rect is the font's content area instead (1.32em in this
   face against a 1.0476em line), so the same line would come back in a taller
   frame with the type riding high in it. Inline-block gives the span the
   block's box back while leaving it in the h1's line, so the reflected
   headline is the same bitmap in the same place it was before the lockup. */
.hero-line {
  display: inline-block;
}

/* ── the lead-in ──────────────────────────────────────────────────────────
   "Your" is set small and hung above the line, and the line itself does not
   move. That is the whole point of the treatment.

   Set inline at full size, the string becomes "Your Ideas in Good Hands" —
   13.147em in this face against 10.489em — and holding the same 94vw fit
   drops the headline to 94/13.147 = 7.150vw. A fifth of the type, given away
   to a four-letter word. Even a reduced "Your" on the line costs: at this
   size it still pushes the fit to 8.10vw. Out of the line, the statement
   keeps every pixel of 8.9604vw and the word arrives ahead of it.

   OUT OF FLOW, so the composition does not move either. The scene centres its
   flex child, so a lead-in in the flow would grow the block and push the line
   down off the middle of the screen — off the orb it is set across. Absolute,
   the h1's box is still one line high and the line stays exactly where it was.

   0.42 of the line. Small enough to be plainly subordinate, large enough to
   still be the headline's voice rather than a label in it — at 1440 that is
   54px against the line's 129px, and against the 30px wordmark it cannot be
   mistaken for chrome. Sized in em off the h1, so the breakpoint below moves
   both with one number.

   FULL WHITE, like the line. A grey "Your" would be the same apology the
   quadrant labels above are argued out of.

   ── the left edge ────────────────────────────────────────────────────────
   Aligned by INK, not by box. Grafier's I carries a 0.031em left sidebearing
   and its Y carries none — the arm tip sits flush on the origin — so two boxes
   at the same left would print the Y a full 4px left of the I at this size,
   which on an edge this long reads as a mistake. The indent is that sidebearing
   expressed in the lead's own em (0.031 ÷ 0.42), which keeps it exact at every
   viewport and at the breakpoint.

   ── the vertical ─────────────────────────────────────────────────────────
   What is being spaced is the descender of "Your" against the CAPS of "Ideas",
   not one box against another — the h1's box already stands 0.16em clear of
   its own caps in leading, so the boxes touching says nothing about what the
   eye sees. bottom: 100% alone lands the tail of the y a fifth of the lead's
   size above the caps, which is close enough to read as crowding; the 0.055em
   lifts it to a little over a quarter. Near enough that the word and the line
   are one thought, open enough that nothing is touching. */
.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;
}

/* their one breakpoint for it, scaled the same way */
@media (max-width: 59.563em) {
  .hero-headline {
    font-size: 8.5337vw;      /* their 10.4167vw × 0.81924 */
    line-height: 9.3871vw;
  }
}

/* ── scene 2: the statement ──────────────────────────────────────────────
   itsoffbrand.com's second home section — see index.html for the structure
   and for the character budget the copy in it lives inside (it was lorem'd
   at the reference's counts, then written to them). Rules under the
   verbatim heading below carry their measured values: the reference sets
   its whole page at body
   { font-size: 1vw } and states the section in em against it, so the
   wrapper pins its own 1vw base and everything inside is theirs unchanged.
   Below 992px they snap the base to a fixed 13px and restate the big sizes
   in rem (16px root); 479px collapses the paragraph row to a column. Both
   tiers are copied at their values, on our own 992 tier line — their
   991.98 draws it through the same screens.

   ── the flow ledger ─────────────────────────────────────────────────────
   The one scene beside the hero that owns document flow: one viewport of
   it, adjoining the hero with NO offset. That adjacency is the reference's
   own layout, measured on the live site: their statement section starts at
   exactly 100vh, and their heading stands only their 10em spacer below it,
   entering the frame at 18.8vh of scroll with the hero type still leaving
   above it. This section keeps that spacer and that adjacency, so the
   heading enters on the same beat and the whole statement is on screen by
   0.9 viewports — before the orb has finished its sweep (it lands at
   scroll 1.5; the runway resolves to 10 viewports of scroll per timeline
   unit). Three earlier cuts hung the section 110, 50, then 20vh below the
   hero, and every one read as late: any gap is empty field between the
   hero's last line and the statement's first, and the reference simply
   does not have one.

   From there the copy simply passes — flow against the parked orb, read
   on the move, the reference's own behaviour — and the return sweep
   follows it out. A sticky pin once held the composition through the
   park here (rise, lock, release — the section grew --scene2-hold taller
   and .about-pin stuck at the frame); it was removed by request, and the
   record matters because the geometry was RIGHT: if a lock ever comes
   back, it is those three acts again, not a retiming of this flow.

   --scene2-flow is the total the scene adds to the document — exactly the
   section's viewport now — and .stage-scroll below subtracts it: the page
   is as tall as it was before the scene existed, so no beat moves, with
   the script blocked or without. It lives on .main-w, the ancestor the
   section and the runway share. */
.main-w {
  --scene2-flow: 100vh;
}

/* The wrapper is ours — their per-node Webflow ids become the three
   .about-* placement classes — and everything under it is theirs. Ink is
   --main-dark, where the reference paints --main-light through
   mix-blend-mode: difference: over a light field the two compute to the
   same near-black, and a blend here could not reproduce theirs anyway,
   because .main-w's stacking context isolates its children from the orb
   canvas below — difference would blend against nothing. The orb answers
   instead: the copy is [data-reflect], and inversion-over-the-chrome
   becomes reflection-in-the-chrome, which is this site's own way of tying
   copy to the object. */
/* The copy is TOP-ANCHORED in the viewport of section, not centred: its
   offset is the reference's own spacer-10 (in index.html), the same 10em
   on every screen. Centring was tried and it is what made the copy late —
   a centred block's first line stands (100vh − content)/2 further down
   the document, which is scroll the reader has to spend before seeing it,
   and the offset drifted with aspect ratio besides. The spacer is the
   reference's number and it does not drift. */
.about-w {
  height: 100vh;
  font-family: "Ataero Retina OB Edition", "Helvetica Neue", "Nimbus Sans", Helvetica, Arial, sans-serif;
  font-size: 1vw;
  color: var(--main-dark);
  -webkit-user-select: none;
  user-select: none;
}

/* ── verbatim: their section, at their sizes ─────────────────────────── */

.about-w .c { width: 100%; padding: 0 1.25em; }

.about-w .grid-main {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: auto;
  grid-auto-columns: 1fr;
  gap: 1.25em;
  width: 100%;
}

/* their #w-node grid-areas, one per block: the heading across cols 3-11,
   the caps lead on 2-3, the body on 5-7 */
.about-head { grid-area: 1 / 3 / 2 / 12; }
.about-lead { grid-area: 1 / 2 / 2 / 4; }
.about-body { grid-area: 1 / 5 / 2 / 8; }

.about-w .h-b {
  text-transform: uppercase;
  font-size: 5em;
  font-weight: 400;
  line-height: 1;
}

/* the heading's first-line indent: four glyphs nobody sees. visibility
   rather than their opacity 0 — the mirror's innerText skips it. */
/* The heading's first-line indent: four glyphs nobody sees, and — since the
   SEO pass — four glyphs nobody READS either. This was a <span>****</span>
   held at visibility:hidden in the markup, which reserved the correct box and
   stayed out of innerText (so the mirror never printed it on the chrome), but
   sat squarely inside the document: the site's only h2 was being crawled as
   "**** Where aesthetics elevate…".

   Generated content keeps every property that worked and drops the one that
   did not. It is not in the DOM, so it reaches neither the heading text nor
   the accessibility tree nor innerText; and because it is still the same four
   glyphs set in the same face at the same size, the box it reserves is
   identical BY CONSTRUCTION — including when the Nimbus fallback is serving,
   where a hand-measured `text-indent` in em would have drifted.

   The trailing space is part of the content string on purpose: the markup
   used to read `</span> Where`, and that separator has to survive the move or
   the indent shortens by a word space and the heading's measured wrap — which
   sits deliberately just under a reflow cliff — loses its margin. */
.about-w .h-b::before {
  content: "**** ";
  visibility: hidden;
}

.about-w .spacer-10 { flex: none; height: 10em; }

/* A HAIRLINE ON THE BODY BLOCKS, and the stylesheet argues against exactly
   this a few hundred lines up — see .svc-cell, where a faux medium was
   refused because -webkit-text-stroke paints an outline along the contour
   rather than drawing a heavier font: joins fill, counters get eaten from
   both sides, and the shape is antialiased twice.

   All of that is still true. It is accepted here because the two settings
   are not the same problem. The chart's cells are the diagram's hierarchy,
   set large and read one word at a time, and there the artefacts of a fake
   ARE the read. These are two paragraphs of continuous body copy at 14.4px
   whose job is to sit a step away from a 5em heading in the same one-weight
   face — AtAero ships 400 and nothing else, so there is no honest heavier
   cut to reach for, and the only native alternative the browser offers is
   synthetic bold, which is the same stroke-based fake at +42% ink. Measured:
   400 and 500 are pixel-identical, 600 and 700 both land on that +42%.
   Between a fake that can be dialled and a fake that cannot, this takes the
   one with a dial, and sets it low enough to read as weight rather than as
   paint.

   0.012em — a fifth of the way to the synthetic bold's step. Chosen against
   rendered samples of the real copy at the real size, not by eye off a
   number.

   Free of the composition, which is the other reason it is safe: a stroke
   does not touch advance widths (measured — identical to the glyph at every
   weight), so the character-count budget scene 2's wrap was measured under
   is untouched, at every width and in both tiers. Verified after the change:
   heading 4 lines and lead/body 4/5 from 1920 down to 1200, 3/4 at 992, 6/2/5
   at 393, and the document still 21708 at 1920x1080 — the same numbers the
   composition was signed off at.

   The reflection needs nothing: _rasterize in js/orb.js already reads
   webkitTextStrokeWidth off the computed style and strokes over fill in the
   browser's own paint order, so the mirror thickens on exactly this rule. */
.about-w .text-small {
  font-size: 1em;
  font-weight: 400;
  line-height: 1.4;
  -webkit-text-stroke: 0.012em currentColor;
}
.about-w .caps { text-transform: uppercase; }

/* ── verbatim: their two lower tiers ───────────────────────────────────── */

@media only screen and (max-width: 992px) {
  .about-w { font-size: 13px; }        /* their body snaps to a fixed 13px */
  .about-w .c { padding: 0 2vw; }
  .about-w .grid-main { gap: 1.25rem; }
  .about-w .h-b { font-size: 4rem; }
  .about-w .text-small { font-size: .85rem; }
  .about-w .spacer-10 { height: 10rem; }
  .about-head { grid-column: 1 / 13; }
  .about-lead { grid-column-end: 6; }
  .about-body { grid-column: 7 / 12; }
}

/* ── the dome's zone, portrait only ──────────────────────────────────────
   Portrait parks the orb as a crest in the frame's top quarter (uPortrait
   in js/main.js), so the statement's top spacer grows from the reference's
   10em to stand the copy off below it: at the park's first frame the dome,
   the heading and both paragraphs stack clear of each other, and the copy
   crosses the dome only on its way out. Sized against that crest — the
   dome reaches ~23vh at uPortrait 1.4, the spacer gives it 40 — not
   against the type; the two numbers move together, lower orb = taller
   spacer. Landscape keeps the reference's own spacer: the orb parks
   BESIDE the copy there, not above it. Same media terms as the runway's
   portrait tier, so the spacer and the choreography it answers switch
   together. */
@media only screen and (max-width: 992px) and (orientation: portrait) {
  .about-w .c > .spacer-10:first-child { height: 40vh; }
}

@media only screen and (max-width: 479px) {
  .about-w .c { padding: 0 3vw; }
  .about-w .grid-main.m-flex { display: flex; flex-direction: column; }
  .about-w .spacer-10.mob-2 { height: 2rem; }
}

/* ── the heading's phone measure ──────────────────────────────────────────
   DEVIATION, and the one place copying the reference's number was the wrong
   move. Their h-b is a fixed 48px on a phone and ours was the same 48px —
   the tier above it a fixed 64px — because their section was cloned at their
   sizes. A fixed size means the characters per line change with the screen,
   and the reference could afford that because THEIR copy fits it: "WITH
   EMOTION + INNOVATION, WE PUSH THE BOUNDARIES OF DIGITAL CREATIVITY." sets
   seven lines at 393 with one short line in it.

   Ours is 73 characters against their 71 — but the total was never what set
   the portrait wrap. The long words are. AESTHETICS, IMMERSIVE and
   EXPERIENCES each claim a line of their own at 48px, and what came out at
   393 was eight lines with WITH alone on one and ELEVATE alone on another:
   three stubs, a block 384px tall before the body copy had started, and a
   stack that could not fit a 852px screen at all (it ended at 910). That is
   not a size that needs nudging, it is a measure that needs fixing.

   So the heading is sized off the FRAME here, not off a number, which holds
   the characters-per-line constant instead of the pixels. 9vw is the measured
   value. Across the range —

     360   32.4px   6 lines, block ends 720/852
     393   35.4px   6 lines, block ends 738/852
     430   38.7px   6 lines, block ends 739/852

   — one wrap everywhere: WHERE / AESTHETICS ELEVATE / BRANDS TO NEW /
   HEIGHTS THROUGH / IMMERSIVE / EXPERIENCES. Lines of 18, 13 and 15 carry
   the middle and nothing is stranded, where the fixed 48px set eight lines
   with WITH alone on one and ELEVATE alone on another.

   ── the divisor is a CLIFF, not a slope, and it moved with one word ──────

   This was 11.2vw (44px at 393) while the heading read "...TO NEW HEIGHTS
   WITH IMMERSIVE EXPERIENCES", which was the largest size that set six
   lines. WITH -> THROUGH broke it: the pair "THROUGH IMMERSIVE" is 17
   characters where "WITH IMMERSIVE" was 14, the measure at 44px holds about
   14, and THROUGH dropped onto a line of its own — seven lines, the exact
   stranding this rule exists to prevent, and a block ending at 834 of 852.

   The reflow back to six is DISCONTINUOUS: measured at 393 in half-pixel
   steps, 36.5px still sets seven lines and 36.0px sets six, because six only
   becomes available once "AESTHETICS ELEVATE" (18) fits a line, which pulls
   the whole wrap up at once. There is nothing usable in between. 9vw = 35.4px
   at 393 sits deliberately below that cliff rather than on it, so a font
   fallback (the Ataero file is study-use-only; the stack drops to Nimbus with
   different metrics) cannot tip it over.

   The cost of the word is a fifth of the type's size, and it is worth knowing
   before the copy is edited again: on a phone this heading is width-bound,
   the binding constraint is the longest adjacent PAIR, and a three-letter
   change to one word can be worth 9px of display type.

   Bounded at 711, where 9vw resolves to 63.99px against the fixed 64px of the
   tier above — the two meet within a hundredth of a pixel, so nothing steps.
   (The old bound was 560, chosen the same way against the old divisor.) */
@media only screen and (max-width: 711px) {
  .about-w .h-b { font-size: 9vw; }
}

/* ── scene 5: contact ────────────────────────────────────────────────────── */

/* PINNED, not laid out. A section in the document can only arrive when the
   scroll reaches it, and the last viewport of the runway is a full screen
   AFTER the sheet has finished unrolling — so a flowed contact scene made you
   scroll past a finished flatten to reach the copy. Fixed and faded against
   the morph instead, it arrives WITH the flattening: see applyContact in
   js/main.js, which drives it off the same playhead as the unfold.

   z-index 2 puts it over the orb (-2) and the reveal layer (-1) and under the
   wordmark (10). It covers the frame, so it must not take the pointer — the
   orb's ripples live under it for the whole page. Only the two links do, and
   only once .is-in says they are on screen.

   The block is centred, so the gutter is no longer an edge the copy sits on —
   it is the margin the type may not grow into, and it stays the WORDMARK's
   inset (the same expression as .chrome-w, resolving to the same pixel at
   every width) so the copy stops exactly where the chrome does on both sides.

   --line is the block's unit — the big lines are set at it and every gap is a
   fraction of it, so the block scales as one object.

   The divisor is measured, not chosen. "HELLO@BRANDMAX.CA" in Space Grotesk
   Bold at -0.045em tracking is 10.171em wide; it is the longest string in the
   block, so the size that makes it span the gutters is the size that fills the
   screen — 10.35 with a hair of air. 11.5 is that taken down 10%: the email
   stops just short of the right gutter rather than reaching for it, which
   leaves the block sitting IN the frame instead of straining against it.
   Deriving it from the live frame rather than a hand-tuned vw is what keeps it
   exact on a viewport nobody measured — including the one the wordmark's own
   max(24px, 1.9em) switches over at. */
.contact-w {
  --gutter: max(24px, 1.9em);
  --line: calc((100vw - 2 * var(--gutter)) / 11.5);

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

  /* hidden rather than transparent until the reveal starts: an invisible
     mailto: link 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;
}

/* The opacity and the rise are written per-frame by applyContact; these are
   the pre-JS state, so nothing flashes before the timeline is built. Keyed to
   [data-in], which is the same list the JS walks — one place to add to. */
.contact-w [data-in] {
  opacity: 0;
  will-change: opacity, transform;
}

/* Above both lines, and the only thing in the block that is not enormous —
   it carries the whole gesture, so it gets tracking OUT rather than in, the
   opposite of the headline.

   The gap is the larger of a fraction of the type and a fraction of the
   frame, which are the same number on a landscape screen and are not on a
   tall one. --line is capped by WIDTH, so on a portrait phone the type stops
   growing while the frame keeps going, and a gap tied to the type alone
   leaves the whole block a thin strip stranded in the middle of it. Below
   that crossover the frame sets the air instead. */
.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);
}

/* line-height 0.9 rather than the body's 1.4: at this size the default
   leading opens a corridor between the two lines and they stop reading as
   one block. Tracking matches the wordmark's -0.045em — the same face set
   the same way, three orders of magnitude apart in size. */
/* The underline is drawn as a background rather than text-decoration, which
   cannot be animated across its own length — it grows from the left edge, the
   direction the line is read in. 0.055em against a 164px line is a rule, not a
   hairline, which is the weight this type wants. */
/* fit-content + auto margins rather than a full-width centred block: the
   underline below is a background sized in % of the ELEMENT, so a full-width
   line would draw a rule across the whole frame instead of under the words.
   Each line centres on its own, which is what puts the two on a shared axis
   despite the email being the longer string. */
.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(--main-dark);
  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);
}

/* line-height 0.9 packs the two lines tighter than their glyphs are tall, so
   they overflow their own line boxes and the @ was landing on the phone
   number. Leading between the two rather than a looser line-height, which
   would also open the gap under the eyebrow. */
.contact-line + .contact-line { margin-top: 0.06em; }

/* And the @ itself, which hangs lower than every other glyph on the line —
   it is the only character in the address with anything below the baseline,
   so it collides on its own. Nudged up off it. `position: relative` rather
   than a transform: a transform needs inline-block, and that changes how the
   glyph kerns against the H and the B on either side of it. */
.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; }
}

/* ── the contact block, portrait mobile: the grand stack ─────────────────
   On a desktop the email is the block's measure — the longest string sets
   --line and everything hangs off it. On a phone that same measure is what
   keeps the type small: 17 characters across 345px is a 30px line, which
   is a caption, not a finale. So the portrait tier changes the MEASURE
   rather than fudging the size: the address stacks (the .seg spans in
   index.html go block), the longest line becomes BRANDMAX.CA at 6.5em, and
   --line is re-derived against 6.9 — the same fill-the-gutters logic as
   the desktop's 11.5, applied to the stacked shape. The type lands ~50px
   on a 393px phone, two thirds larger, and every string still spans the
   frame the way the desktop email does.

   The eyebrow rises with it — half the line instead of a fifth, floored
   well above the desktop's 14px: at this scale it is the finale's opening
   voice, not a label. And the lines breathe: the email-to-number gap grows
   from the desktop's 0.06em (two single lines packed as one lockup) to
   0.35em — three big lines read as a STACK, and a stack needs air between
   its storeys or the address's second line and the number read as one
   piece. Same media terms as the runway's portrait tier. */
@media only screen and (max-width: 992px) and (orientation: portrait) {
  .contact-w { --line: calc((100vw - 2 * var(--gutter)) / 6.9); }
  .contact-line .seg { display: block; }
  .contact-line + .contact-line { margin-top: 0.35em; }
  .contact-eyebrow { font-size: max(20px, calc(var(--line) * 0.5)); }
}

/* The scroll runway.

   Every orb timeline is scrubbed against page scroll, so the camera only runs
   as far as the document is tall. --scenes is the REMAINDER of the measured
   ~10.5-viewport runway after the hero's own viewport — the total stays 10.5,
   so the sweeps keep the pacing they were tuned at. Scene 5's copy costs
   nothing here: it is pinned, and rides the runway it is revealed against
   rather than adding to it.

   Scene 2's statement DOES cost, and pays: it is in flow, so the runway
   subtracts its whole footprint — --scene2-flow, gap and section together,
   inherited from .main-w — and the document is exactly as tall as it was
   when the scene did not exist. The subtraction is deliberately outside the
   --scene4-hold product: the section's flow is real viewports of document,
   not timeline that got stretched, so it comes off after the stretch or the
   runway would give back twice what was taken. Holds with the script
   blocked too — the hold factor rests at 1 but the statement still occupies
   its flow, so the no-JS page keeps the same 10.5-viewport total it always
   had. */
/* --scene4-hold is written once from js/main.js: (0.95 + Σ holds) / 0.95,
   the factor by which the scene pauses — orb parked right, parked left, and
   the scene-4 browse — lengthened the camera timeline. (The name predates
   the first two joining the ledger.) The runway is stretched by exactly
   that, so scroll-distance-per-timeline-unit does not change and every
   corridor keeps the scroll pace it was tuned at. It lives in CSS rather
   than being folded into --scenes so the breakpoint below still owns the
   base number. Defaults to 1: with the script blocked, this is the runway
   as it was. */
.stage-scroll {
  --scenes: 9.5;
  --scene4-hold: 1;
  height: calc(var(--scenes) * var(--scene4-hold) * 100vh - var(--scene2-flow, 0vh));
  pointer-events: none;
}

/* Every beat is scrubbed against scroll PROGRESS, not distance, so shortening
   the runway compresses the whole sequence without moving a single beat
   relative to any other. A phone gets eight screens rather than ten and a half
   — the same piece, about a third fewer swipes to reach the end of it. The
   hero stays, so the remainder is seven. */
@media only screen and (max-width: 992px) and (orientation: portrait) {
  .stage-scroll { --scenes: 7; }
}

@media only screen and (max-width: 992px) {
  body { font-size: 2.4vw; }
}
