﻿/* ==========================================================================
   Digitell Space — one room, nine crafts
   Palette: cold electric blue on ink, set against a warm bone white.
   ========================================================================== */

@property --lens-r { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --sweep  { syntax: '<percentage>'; inherits: false; initial-value: 0%; }

/* --------------------------------------------------------------- reset -- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body, h1, h2, h3, h4, p, figure, blockquote, ul, ol, dl, dd { margin: 0; }
ul[class], ol[class] { list-style: none; padding: 0; }
img, video, svg { display: block; max-width: 100%; }
button, input, textarea, select { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }

/* -------------------------------------------------------------- tokens --
   Two themes. Light is the default; dark is kept intact under
   [data-theme="light"]. Everything below is expressed against these names,
   so the whole page follows a single swap.
   ------------------------------------------------------------------------ */
:root {
  /* BRAND BLUE — the default. The ground is the studio's own indigo
     (#08088A, sampled from the app icon), deepened a touch because it
     covers whole screens; type sits on it in white. */
  --ink:      #0A0A7A;   /* default section ground */
  --ink-2:    #16169B;   /* panel on that ground   */
  --ink-3:    #2222B5;
  --signal:   #6E6EF5;   /* the brand, lit — accent fills */
  /* THE ACCENT AS SMALL TEXT ON BONE. #6E6EF5 measures 3.56:1 there:
     fine for a display-size .lit, short of AA for anything set small.
     Labels, prose links and the numerals take a darker step of the same
     hue; fills and headlines keep the brand blue. */
  --signal-ink: #4B4BE8;
  --signal-2: #8C8CFF;
  --beam:     #B9C4FF;   /* accent type on the blue */
  --bone:     #EEF0FF;   /* the pale band, blue-tinted not white */
  --bone-dim: #C7CBEA;
  --ember:    rgba(255, 255, 255, .34);  /* retired: friction is now dim, not orange */

  --line:       rgba(255, 255, 255, .16);
  --line-solid: rgba(255, 255, 255, .34);
  --on-ink-1:   rgba(255, 255, 255, .96);
  --on-ink-2:   rgba(255, 255, 255, .70);
  --on-ink-3:   rgba(255, 255, 255, .56);   /* .48 measured 4.2:1 on the ink ground */
  --on-bone-1:  rgba(10, 10, 45, .94);
  --on-bone-2:  rgba(10, 10, 45, .64);
  --on-bone-3:  rgba(10, 10, 45, .58);      /* .44 measured 3.0:1 on bone */

  /* the punctuation bands: deepest blue, used by the WebGL sections */
  --deep:       #050540;
  --deep-2:     #0B0B62;
  --on-deep-1:  rgba(255, 255, 255, .96);
  --on-deep-2:  rgba(255, 255, 255, .70);
  --on-deep-3:  rgba(255, 255, 255, .54);   /* .46 measured 4.5:1 on the deep ground */
  --deep-line:  rgba(255, 255, 255, .16);

  --display: 'Bricolage Grotesque', 'Arial Narrow', system-ui, sans-serif;
  --body:    'Instrument Sans', system-ui, -apple-system, sans-serif;
  --mono:    'IBM Plex Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;

  /* fluid scale — mega is capped by viewport height so the hero always fits */
  --t-mega:  min(clamp(2.9rem, 8vw, 7.5rem), 17vh);
  --t-h1:    clamp(2.6rem, 8.4vw, 7.6rem);
  --t-h2:    clamp(2rem, 5vw, 4.4rem);
  --t-h3:    clamp(1.35rem, 2.6vw, 2.35rem);
  --t-lead:  clamp(1.02rem, 1.35vw, 1.22rem);
  --t-body:  clamp(.95rem, 1.05vw, 1.03rem);
  --t-mono:  clamp(.64rem, .78vw, .72rem);

  --gut:  clamp(1.25rem, 4.4vw, 3.5rem);
  --bay:  clamp(4.5rem, 9vw, 8.5rem);
  --max:  1560px;

  --ease: cubic-bezier(.16, 1, .3, 1);
  --ease-io: cubic-bezier(.65, 0, .35, 1);
}

/* the airy alternative, offered by the nav switch */
[data-theme="light"] {
  --ink:      #FFFFFF;
  --ink-2:    #EEF1FB;
  --ink-3:    #E2E7F8;
  --signal:   #08088A;
  --signal-ink: #08088A;   /* already dark enough on this theme's bone */
  --signal-2: #2A2ADB;
  --beam:     #4B4BE8;
  --bone:     #F1F3FC;
  --bone-dim: #C9CDE4;
  --ember:    rgba(10, 10, 45, .30);

  --line:       rgba(10, 10, 40, .12);
  --line-solid: rgba(10, 10, 40, .26);
  --on-ink-1:   rgba(10, 10, 31, .94);
  --on-ink-2:   rgba(10, 10, 31, .64);
  --on-ink-3:   rgba(10, 10, 31, .44);
  --on-bone-1:  rgba(10, 10, 31, .94);
  --on-bone-2:  rgba(10, 10, 31, .64);
  --on-bone-3:  rgba(10, 10, 31, .44);

  --deep:       #06064A;
  --deep-2:     #0A0A6B;
  --on-deep-1:  rgba(255, 255, 255, .95);
  --on-deep-2:  rgba(255, 255, 255, .68);
  --on-deep-3:  rgba(255, 255, 255, .44);
  --deep-line:  rgba(255, 255, 255, .16);
}

/* ---------------------------------------------------------------- base -- */
html { scroll-behavior: smooth; }
body {
  background: var(--ink);
  color: var(--on-ink-1);
  font-family: var(--body);
  font-size: var(--t-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
body.is-locked { overflow: hidden; }

::selection { background: var(--signal); color: #fff; }

:focus-visible {
  outline: 2px solid var(--beam);
  outline-offset: 3px;
  border-radius: 3px;
}

.skip {
  position: absolute; left: 50%; top: 0; transform: translate(-50%, -140%);
  z-index: 300; padding: .8rem 1.4rem; border-radius: 0 0 12px 12px;
  background: var(--bone); color: var(--ink); font: 600 .85rem var(--body);
  transition: transform .25s var(--ease);
}
.skip:focus { transform: translate(-50%, 0); }

/* ------------------------------------------------------------ type kit -- */
.mono {
  font-family: var(--mono);
  font-size: var(--t-mono);
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
}
.eyebrow { color: var(--beam); }
.sec--bone .eyebrow { color: var(--signal-ink); }

/* Bricolage is a condensed-capable grotesque; kept open enough that big
   settings stay readable rather than turning into a wall of letters. */
.display {
  font-family: var(--display);
  font-weight: 800;
  font-stretch: 90%;
  line-height: .95;
  letter-spacing: -.022em;
  text-transform: uppercase;
  word-spacing: .04em;
}
.h2 { font-size: var(--t-h2); font-stretch: 86%; line-height: .98; letter-spacing: -.028em; }
.h3 {
  font-family: var(--display);
  font-weight: 700; font-stretch: 84%;
  font-size: var(--t-h3);
  line-height: 1.02; letter-spacing: -.03em;
}
.lead { font-size: var(--t-lead); line-height: 1.55; color: var(--on-ink-2); text-wrap: pretty; }
.sec--bone .lead { color: var(--on-bone-2); }

/* ------------------------------------------------------------- layout -- */
.sec { position: relative; padding: var(--bay) var(--gut); }
/* A BONE SECTION REBINDS THE INK TOKENS (2026-09-06). site.css serves every
   page, and a rule written against the indigo ground — --on-ink-2 for a
   paragraph, --on-ink-3 for a label — lands on a bone section sooner or
   later and paints white on near-white. It had, in four places: the contact
   page's three routes were invisible cards, the founder card on About was a
   name floating in an empty box, its pillar labels were gone, and the
   privacy page's date was not there at all. Rebinding here fixes the whole
   class of it, including the ones nobody has written yet. */
.sec--bone {
  background: var(--bone); color: var(--on-bone-1);
  --on-ink-1: var(--on-bone-1);
  --on-ink-2: var(--on-bone-2);
  --on-ink-3: var(--on-bone-3);
}
.sec--rule { border-top: 1px solid var(--line); }
.wrap { max-width: var(--max); margin-inline: auto; }

.sec-head {
  display: flex; flex-wrap: wrap; gap: clamp(1.5rem, 3vw, 3rem);
  align-items: flex-end; justify-content: space-between;
  padding-bottom: clamp(2.2rem, 4vw, 3.6rem);
}
.sec-head__note { max-width: 34ch; margin-left: auto; }
@media (max-width: 780px) { .sec-head__note { margin-left: 0; } }

.footnote { margin-top: 1.4rem; font-size: .76rem; color: var(--on-ink-3); }
.sec--bone .footnote { color: var(--on-bone-3); }

/* ------------------------------------------------------------ buttons -- */
.btn {
  position: relative; display: inline-flex; align-items: center; gap: .6rem;
  padding: clamp(.85rem, 1.3vw, 1.05rem) clamp(1.4rem, 2vw, 1.9rem);
  border-radius: 100px; overflow: hidden;
  font-weight: 600; font-size: clamp(.88rem, 1vw, .95rem); white-space: nowrap;
  transition: background-color .3s var(--ease), color .3s var(--ease),
              border-color .3s var(--ease), transform .5s var(--ease);
  will-change: transform;
}
.btn--fill  { background: var(--signal); color: #fff; }
.btn--fill:hover  { background: var(--beam); color: var(--ink); }
.btn--ghost { border: 1px solid var(--line-solid); color: var(--on-ink-1); }
.btn--ghost:hover { background: rgba(255, 255, 255, .14); border-color: var(--bone); }
/* on the pale bands the ghost button was white type on near-white */
.sec--bone .btn--ghost { border-color: rgba(10, 10, 45, .26); color: var(--on-bone-1); }
.sec--bone .btn--ghost:hover { background: rgba(10, 10, 45, .06); border-color: rgba(10, 10, 45, .48); }
.btn--dark  { background: var(--ink); color: var(--bone); }
.btn--dark:hover { background: var(--signal); }
.btn__arr { display: inline-block; transition: transform .35s var(--ease); }
.btn:hover .btn__arr { transform: translateX(4px); }

.btn__sheen {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(100deg, transparent 38%, rgba(255,255,255,.42) 50%, transparent 62%);
  transform: translateX(-120%);
  animation: sheen 5.5s var(--ease-io) infinite;
}
@keyframes sheen { 0%, 62% { transform: translateX(-120%); } 100% { transform: translateX(120%); } }

/* ------------------------------------------------- chrome: progress bar -- */
.progress {
  position: fixed; inset: 0 0 auto; height: 2px; z-index: 130;
  background: rgba(255, 255, 255, .12);
}
.progress__bar {
  height: 100%; width: 0;
  background: linear-gradient(90deg, var(--signal), var(--beam));
  transform-origin: 0 50%;
}

/* ------------------------------------------------------- chrome: nav --- */
/* THREE ZONES ON THE PAGE, not a floating card. The pill — bordered,
   blurred, translucent — is the 2020 chrome this page has otherwise moved
   past: it drew a box around navigation that reads better as type sitting
   directly on the work.

   Only the CTA is permanent. The wordmark and the links belong to the top of
   the page and leave with it; the call button has a job on every screen, so
   it stays put for the whole scroll. */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 120;
  display: grid; align-items: center;
  grid-template-columns: 1fr auto 1fr;
  gap: 1rem;
  padding: clamp(1rem, 2.2vw, 1.7rem) var(--gut);
  pointer-events: none;          /* the gaps between the zones stay clickable-through */
}
.nav > * { pointer-events: auto; }

/* what leaves on scroll */
.nav__mark,
.nav__links,
.nav .tswitch {
  transition: opacity .45s var(--ease), transform .45s var(--ease);
}
.nav.is-tight .nav__mark,
.nav.is-tight .nav__links,
.nav.is-tight .tswitch {
  opacity: 0; transform: translateY(-14px); pointer-events: none;
}

.nav__mark {
  justify-self: start;
  font-family: var(--display); font-weight: 700; font-stretch: 86%;
  font-size: clamp(.95rem, 1.25vw, 1.12rem); letter-spacing: -.02em;
  color: var(--on-ink-1); text-decoration: none;
  white-space: nowrap;
}
.nav__mark:hover { color: var(--bone); }

.nav__links {
  justify-self: center;
  display: flex; align-items: center;
  gap: clamp(1rem, 2.2vw, 2.1rem);
}
.nav__link {
  position: relative;
  font-size: .9rem; letter-spacing: -.005em;
  color: var(--on-ink-2); text-decoration: none;
  padding: .25rem 0;
}
/* a rule that draws in from the left, instead of a chip that lights up */
.nav__link::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: currentColor;
  transform: scaleX(0); transform-origin: left center;
  transition: transform .35s var(--ease);
}
.nav__link:hover { color: var(--bone); }
.nav__link:hover::after { transform: scaleX(1); }
.nav__link.is-current { color: var(--bone); }
.nav__link.is-current::after { transform: scaleX(1); }

.nav__end {
  justify-self: end;
  display: flex; align-items: center; gap: clamp(.5rem, 1vw, .9rem);
}
.nav__cta { flex: none; padding: .62rem 1.2rem; font-size: .86rem; }

/* The nav floats over whatever section is under it, and half of those are
   bone. Bone bars on a bone ground are an invisible menu button, so the
   burger carries its own white disc — the same shape and colour as the call
   pill beside it — and draws its bars in the brand blue. One appearance that
   holds on both grounds beats tracking which section is underneath. */
.nav__burger {
  display: none; flex: none; width: 38px; height: 38px; border-radius: 100px;
  flex-direction: column; align-items: center; justify-content: center;
  margin-left: auto;
  background: #fff;
  border: 1px solid rgba(10, 10, 45, .10);
  box-shadow: 0 8px 22px rgba(10, 10, 122, .16);
  transition: box-shadow .3s var(--ease);
}
.nav__burger:hover { box-shadow: 0 10px 26px rgba(10, 10, 122, .26); }
.nav__burger span {
  display: block; width: 17px; height: 1.5px; border-radius: 2px;
  /* --ink, the same navy the call pill sets its own label in, rather than
     the lighter --signal: 15:1 on the white disc against 4:1 */
  background: var(--ink);
  transition: transform .35s var(--ease), opacity .2s;
}
.nav__burger span + span { margin-top: 5px; }
.nav.is-open .nav__burger span:first-child { transform: translateY(3.25px) rotate(45deg); }
.nav.is-open .nav__burger span:last-child  { transform: translateY(-3.25px) rotate(-45deg); }

.menu {
  position: fixed; inset: 0; z-index: 115;
  display: flex; flex-direction: column; justify-content: center; gap: .2rem;
  padding: 6rem var(--gut) 3rem;
  background: var(--ink);
  clip-path: inset(0 0 100% 0);
  transition: clip-path .7s var(--ease);
  visibility: hidden;
}
.menu.is-open { clip-path: inset(0 0 0 0); visibility: visible; }
.menu__link {
  font-family: var(--display); font-weight: 700; font-stretch: 80%;
  font-size: clamp(2.2rem, 11vw, 3.6rem); line-height: 1.06;
  letter-spacing: -.04em; text-transform: uppercase;
  opacity: 0; transform: translateY(28px);
  transition: opacity .5s var(--ease), transform .6s var(--ease), color .2s;
}
.menu.is-open .menu__link { opacity: 1; transform: none; }
.menu__link:hover { color: var(--beam); }
.menu__foot { margin-top: 2.5rem; color: var(--on-ink-2); font-size: .9rem; }
.menu__foot a:hover { color: var(--beam); }

@media (max-width: 900px) {
  .nav { grid-template-columns: 1fr auto; gap: .6rem; }
  .nav__links { display: none; }
  .nav__burger { display: flex; }
  /* The CTA STAYS on a phone. It used to be hidden here and left to the
     menu, but it is the one control that persists down the whole page, so
     dropping it on the smallest screens removes the only way to act
     without opening something first. It shrinks instead, and the theme
     toggle gives up its place — that one has a home in the menu. */
  .nav__cta { padding: .5rem .85rem; font-size: .78rem; }
  .nav__cta .btn__arr { display: none; }
  .nav .tswitch { display: none; }
}

/* ---------------------------------------------------------- preloader -- */
/* The monogram fills bottom-up as the page loads, then the whole panel
   collapses to a centre line and is gone. Two rules keep it from becoming a
   tax on the visitor: it is capped hard (a preloader that outlives the load
   is just a delay), and it only plays in full ONCE per session — every
   subsequent page in the same visit gets a brief veil instead, because this
   is a multi-page site and nobody wants the intro five times. */
.boot { display: none; }
.js .boot {
  position: fixed; inset: 0; z-index: 400;
  display: grid; place-items: center;
  background: var(--ink);
  clip-path: inset(0 0 0 0);
  transition: clip-path .8s var(--ease);
}
.boot.is-done { clip-path: inset(50% 0 50% 0); }

.boot__inner {
  width: min(62vw, 232px);
  transition: opacity .32s var(--ease), transform .5s var(--ease);
}
.boot.is-done .boot__inner { opacity: 0; transform: translateY(-10px); }

/* --- the sphere draws itself -------------------------------------------
   The wireframe is the loader. Each line is normalised with pathLength="1",
   so one dasharray of 1 covers any shape and boot() only has to write a
   dashoffset from 1 (undrawn) to 0 (drawn) — no per-shape perimeter maths,
   and an <ellipse> reports no getTotalLength() anyway.

   The stroke is NON-SCALING: a hairline at every size, which is the weight
   the rest of the page draws its rules at. Scaled with the viewBox it would
   have come out fat on a phone and invisible on a desk. */
.boot__stage { position: relative; display: grid; place-items: center; }
/* the charge behind the glass, held back until the end: it is the moment
   the object stops being a drawing, so it must not be on for the wait */
.boot__stage::before {
  content: ''; position: absolute; width: 132%; aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in oklab, var(--beam) 46%, transparent), transparent 62%);
  opacity: var(--glow, 0); transition: opacity .4s var(--ease);
  pointer-events: none;
}
.boot__orb {
  position: relative; display: block;
  width: 100%; height: auto; color: var(--beam);
  transition: transform .8s var(--ease);
}
.boot.is-done .boot__orb { transform: scale(1.08); }
/* NO NON-SCALING-STROKE, and the reason is the dash. It seems the obvious
   way to hold a hairline at any size — but it puts the DASH PATTERN into
   screen pixels too, which throws away the pathLength="1" normalisation the
   draw-on depends on: every line came out stippled with 1px dashes instead
   of one growing stroke. So the width is in viewBox units instead, and the
   orb's size is capped tightly enough (min(62vw, 232px)) that the line
   lands between about 0.9 and 1.1 real pixels across that whole range. */
.boot__wire > * { stroke-dasharray: 1; stroke-dashoffset: 1; }

.boot__meta {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  margin: 1.6rem 0 0;
}
.boot__name {
  font-size: .56rem; letter-spacing: .28em; text-transform: uppercase;
  color: var(--on-ink-3);
}
/* --on-ink-1, not --bone. The veil's ground is --ink, which the light theme
   rebinds to WHITE — and --bone is near-white in both, so the count was
   invisible on a light-theme first paint. The on-ink tokens follow the
   ground they are named for, which is the whole point of them. */
.boot__count { font-size: .7rem; color: var(--on-ink-1); font-variant-numeric: tabular-nums; }

/* the repeat visit: no theatre, just enough veil to cover the paint */
.boot.is-brief .boot__inner { opacity: 0; }

/* ================================================================ HERO == */
.hero {
  /* Three fields, widest to narrowest:
       --hero-title  the headline may run this wide
       --hero-read   the lead and the buttons stay this narrow, to stay readable
       --hero-edge   where the wall takes over; the scrim fades across it and
                     the lens in site.js switches on at the same fraction
     On small screens all three collapse to one column. */
  --hero-title: 100%;
  --hero-read:  46ch;   /* held to the column by min(…, 100%) at the use site */
  --hero-edge:  100%;

  position: relative;
  min-height: 100svh;
  display: flex; flex-direction: column;
  overflow: hidden;
  isolation: isolate;
}
@media (min-width: 900px) {
  /* --hero-title must stay inside --hero-edge. .hero__line is white-space:pre,
     so max-width sizes the box but never wraps the text — if the headline is
     allowed wider than the edge it simply runs out over the photographs. */
  .hero { --hero-title: 64%; --hero-edge: 68%; }
}

/* the wall — real work, dormant until you look at it */
.wall {
  position: absolute; inset: -6% -2%; z-index: 0;
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: clamp(.5rem, .9vw, .95rem);
}
.wall__col {
  display: flex; flex-direction: column; gap: clamp(.5rem, .9vw, .95rem);
  will-change: transform;
  animation: drift-up 74s linear infinite;
}
.wall__col:nth-child(2) { animation: drift-down 88s linear infinite; }
.wall__col:nth-child(3) { animation: drift-up 62s linear infinite; }
.wall__col:nth-child(4) { animation: drift-down 96s linear infinite; }
.wall__col:nth-child(5) { animation: drift-up 80s linear infinite; }
.wall__col:nth-child(n+3) { display: none; }
.wall img {
  width: 100%; height: auto; border-radius: 10px;
  background: var(--ink-2);
}
@keyframes drift-up   { from { transform: translate3d(0, 0, 0); }    to { transform: translate3d(0, -50%, 0); } }
@keyframes drift-down { from { transform: translate3d(0, -50%, 0); } to { transform: translate3d(0, 0, 0); } }

@media (min-width: 760px)  { .wall { grid-template-columns: repeat(3, 1fr); } .wall__col:nth-child(3) { display: flex; } }
@media (min-width: 1140px) { .wall { grid-template-columns: repeat(4, 1fr); } .wall__col:nth-child(4) { display: flex; } }
@media (min-width: 1640px) { .wall { grid-template-columns: repeat(5, 1fr); } .wall__col:nth-child(5) { display: flex; } }

/* the lens — a hole cut in the veil, following the pointer */
.veil {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  --mx: 50%; --my: 40%;
  /* atmospheric dimming lives here so the lens punches all the way through:
     the work reads as texture at rest, and snaps to full colour where you
     point. Dimming belongs on the veil, never on .wall — a filter on the wall
     would apply inside the lens too and there would be nothing to reveal. */
  backdrop-filter: saturate(.30) brightness(.50) blur(2px);
  -webkit-backdrop-filter: saturate(.30) brightness(.50) blur(2px);
  background: linear-gradient(180deg, rgba(10,10,122,.70) 0%, rgba(10,10,122,.24) 26%, rgba(10,10,122,.32) 62%, rgba(10,10,122,.91) 100%);
  -webkit-mask-image: radial-gradient(circle var(--lens-r) at var(--mx) var(--my),
                      transparent 0%, transparent 46%, #000 100%);
          mask-image: radial-gradient(circle var(--lens-r) at var(--mx) var(--my),
                      transparent 0%, transparent 46%, #000 100%);
  transition: --lens-r .45s var(--ease);
}
.hero.is-lit .veil { --lens-r: clamp(190px, 22vw, 330px); }

/* the lens ring — makes the interaction legible */
.lens-ring {
  --ls: clamp(180px, 21vw, 310px);
  --lx: 50vw; --ly: 40vh; --lk: .5;
  position: absolute; left: 0; top: 0; z-index: 2; pointer-events: none;
  width: var(--ls); height: var(--ls);
  margin: calc(var(--ls) / -2) 0 0 calc(var(--ls) / -2);
  border: 1px solid rgba(127, 178, 255, .5);
  border-radius: 50%;
  opacity: 0;
  transform: translate3d(var(--lx), var(--ly), 0) scale(var(--lk));
  transition: opacity .4s var(--ease), transform .2s var(--ease);
}
.hero.is-lit .lens-ring { opacity: 1; --lk: 1; }

/* ------------------------------------------------------ pointer follower -- */
.cur {
  position: fixed; left: 0; top: 0; z-index: 200; pointer-events: none;
  display: grid; place-items: center;
  width: 10px; height: 10px; margin: -5px 0 0 -5px;
  border-radius: 100px;
  background: var(--beam);
  opacity: 0;
  transform: translate3d(var(--cx, 50vw), var(--cy, 50vh), 0) scale(var(--ck, 1));
  transition: opacity .3s var(--ease), width .38s var(--ease), height .38s var(--ease),
              margin .38s var(--ease), background-color .3s var(--ease);
  mix-blend-mode: normal;
}
.cur.is-on { opacity: 1; }
.cur.is-label {
  width: 64px; height: 64px; margin: -32px 0 0 -32px;
  background: var(--beam);
}
.cur__label {
  font-family: var(--mono); font-size: .58rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase; color: var(--ink);
  opacity: 0; transform: scale(.7);
  transition: opacity .25s var(--ease), transform .3s var(--ease);
}
.cur.is-label .cur__label { opacity: 1; transform: none; }
@media (hover: none), (prefers-reduced-motion: reduce) { .cur { display: none; } }

/* ------------------------------------------------------------- lightbox -- */
.lb {
  position: fixed; inset: 0; z-index: 250;
  display: grid; grid-template-rows: 1fr auto;
  padding: clamp(1rem, 3vw, 2.4rem);
  background: rgba(5, 5, 58, .92);
  opacity: 0; visibility: hidden;
  transition: opacity .4s var(--ease), visibility .4s;
}
/* the blur is only mounted while open — an always-on full-viewport
   backdrop-filter forces the whole page into a composited layer */
.lb.is-open {
  opacity: 1; visibility: visible;
  backdrop-filter: blur(18px) saturate(1.1);
  -webkit-backdrop-filter: blur(18px) saturate(1.1);
}
.lb__stage { display: grid; place-items: center; min-height: 0; }
.lb__img {
  max-width: min(94vw, 1180px); max-height: 74vh;
  width: auto; height: auto; object-fit: contain;
  border-radius: 10px; background: var(--ink-2);
  box-shadow: 0 40px 100px rgba(0, 0, 0, .6);
}
/* a clip in the preview: the same box as a still, and the browser's own
   controls rather than a second set of buttons on a page that already has
   close, previous and next along the bottom */
.lb__vid {
  max-width: min(94vw, 1180px); max-height: 74vh;
  width: auto; height: auto;
  border-radius: 10px; background: #05053f;
  box-shadow: 0 40px 100px rgba(0, 0, 0, .6);
}
.lb__bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 1rem;
  padding-top: clamp(.9rem, 2vw, 1.4rem);
  border-top: 1px solid var(--line); margin-top: clamp(.9rem, 2vw, 1.4rem);
}
.lb__meta { min-width: 0; }
.lb__name { font-family: var(--display); font-weight: 700; font-stretch: 88%; font-size: clamp(1.05rem, 2vw, 1.5rem); letter-spacing: -.02em; }
.lb__craft { margin-top: .25rem; color: var(--beam); }
.lb__nav { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
.lb__count { margin-right: .6rem; color: var(--on-ink-3); }
.lb__btn {
  display: grid; place-items: center; width: 42px; height: 42px;
  border-radius: 100px; border: 1px solid var(--line-solid); color: var(--on-ink-1);
  font-size: 1rem; line-height: 1;
  transition: background-color .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease);
}
.lb__btn:hover { background: var(--bone); color: var(--ink); border-color: var(--bone); }
.lb__close { position: absolute; right: clamp(1rem, 3vw, 2.4rem); top: clamp(1rem, 3vw, 2.4rem); z-index: 2; }

/* ---------------------------------------------------------- rolling text -- */
.roll { position: relative; display: inline-block; overflow: hidden; vertical-align: middle; }
.roll span { display: block; transition: transform .46s var(--ease); }
.roll span:last-child { position: absolute; inset: 0; transform: translateY(105%); }
.roll:hover span:first-child,
a:hover > .roll span:first-child,
button:hover > .roll span:first-child { transform: translateY(-105%); }
.roll:hover span:last-child,
a:hover > .roll span:last-child,
button:hover > .roll span:last-child { transform: translateY(0); }
@media (prefers-reduced-motion: reduce) { .roll span:last-child { display: none; } }

/* scrim so the type always wins */
.hero__scrim {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  /* only the reading side is protected; the wall side is left clear so the
     lens can reveal it completely */
  background:
    /* a floor under the stats bar and the ticker, which sit over bright work */
    linear-gradient(180deg, transparent 52%, rgba(10,10,122,.74) 76%, rgba(10,10,122,.97) 92%),
    /* the reading field: solid under the headline, handing over to the wall
       across --hero-edge */
    linear-gradient(96deg,
      #0A0A7A 0%, #0A0A7A 50%,
      rgba(10,10,122,.94) 60%, rgba(10,10,122,.70) 68%,
      rgba(10,10,122,.30) 78%, transparent 90%);
}
@media (max-width: 900px) {
  /* No side-by-side layout to protect, so the whole column gets backing — but
     there is no lens on touch either, so this is the only state the wall ever
     has. Keep it light enough that the drift is still visible behind the type. */
  .hero__scrim { background: linear-gradient(180deg, rgba(10,10,122,.86) 0%, rgba(10,10,122,.74) 38%, rgba(10,10,122,.88) 76%, rgba(10,10,122,.97) 100%); }
  .veil {
    backdrop-filter: saturate(.42) brightness(.66) blur(1.5px);
    -webkit-backdrop-filter: saturate(.42) brightness(.66) blur(1.5px);
  }
}
.hero__glow {
  position: absolute; z-index: 3; pointer-events: none;
  left: 50%; top: 46%; width: min(1180px, 120%); aspect-ratio: 16/9;
  transform: translate(-50%, -50%);
  background: radial-gradient(50% 50% at 50% 50%, rgba(29, 78, 216, .34), transparent 68%);
  filter: blur(52px);
  mix-blend-mode: screen;
}

.hero__body {
  position: relative; z-index: 4;
  flex: 1; display: flex; flex-direction: column;
  gap: clamp(.7rem, 2vh, 1.8rem);
  max-width: var(--max); width: 100%; margin-inline: auto;
  /* The whole hero — body, stats bar and ticker — has to land inside 100svh,
     including on a 1366×650 laptop window. The rem floors are deliberately low
     so the vh terms can actually compress; a floor above the vh value pins the
     spacing open and pushes the ticker off screen. */
  padding: clamp(4rem, 10.5vh, 7rem) var(--gut) clamp(.8rem, 2.4vh, 2rem);
}
/* Centred in whatever the foot leaves, by AUTO MARGINS rather than by
   justify-content: the foot is pinned to the bottom of the same column, so
   centring the column would have carried the foot up with it. This puts the
   headline in the middle of the frame and leaves the foot on the floor. */
/* The headline and the claim under it are ONE block, centred together in
   whatever the foot leaves: the title takes the slack above and the claim
   takes it below, so the pair sits in the middle of the frame and the foot
   stays on the floor. (The title alone had margin-block:auto; leaving that
   would have centred the title and hung the claim off the bottom edge.) */
.hero--mark .hero__title { margin-block: auto 0; }
.hero--mark .hero__claim { margin-bottom: auto; }
.hero__title {
  /* SENTENCE CASE, and set open. .display is the page's caps voice — heavy,
     condensed, shouted — and it is right for the statement strip, where three
     verbs cross the screen. It is wrong for a sentence: sixty characters of
     condensed 800-weight caps is a wall, and this headline has a claim to
     make rather than a word to land. Normal width, a weight down, and the
     letters get their own shapes back. */
  text-transform: none;
  font-stretch: 100%;
  font-weight: 600;
  letter-spacing: -.028em;
  /* Three guards, and the smallest wins:
       clamp(...)  the width-driven size
       10.4vh      short browser windows — a real one loses ~150px to tab and
                   bookmark bars, and the stats bar and ticker have to survive
       9.2vw       narrow phones, where "companies generate" is white-space:pre
                   in an overflow:hidden line and would be silently cut
     12vw -> 9.2vw with the new headline: the longest line went from thirteen
     characters to eighteen, and the old guard left it 28px over the column at
     390px wide — cut mid-word, with nothing on screen to say so.
     Verified 320×700 through 1920×1080: hero inside 100svh, foot on screen,
     headline uncut. Retune all three together, not one in isolation. */
  font-size: min(clamp(2.6rem, 7vw, 6.4rem), 10.4vh, 9.2vw);
  max-width: var(--hero-title);
  line-height: .90;
}
.hero__line { display: block; overflow: hidden; padding-bottom: .06em; white-space: pre; }
.hero__line > span { display: block; }

/* line-level fallback; JS upgrades this to a per-character stagger */
.js .hero__line > span { transform: translateY(105%); transition: transform 1.05s var(--ease); }
body.is-booted .hero__line > span { transform: translateY(0); }
.hero__line:nth-child(2) > span { transition-delay: .09s; }
.hero__line:nth-child(3) > span { transition-delay: .18s; }
.hero__line:nth-child(4) > span { transition-delay: .27s; }

.js .hero__line.is-split > span { transform: none; transition: none; }
.js .hero__line .ch {
  display: inline-block; font-style: normal;
  transform: translateY(108%);
  transition: transform .92s var(--ease);
  transition-delay: calc(var(--d, 0) * 26ms + var(--l, 0) * 90ms);
}
body.is-booted .hero__line .ch { transform: none; }

/* line two carries the brand gradient. splitHeadline deliberately skips a
   --grad line: background-clip:text reads each element's own background, so
   per-character splitting would restart the gradient on every glyph. The
   whole line rises as one instead. */
.hero__line--grad > span {
  background: linear-gradient(97deg, #fff 6%, var(--beam) 52%, var(--signal-2) 96%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.js .hero__line--grad > span {
  display: inline-block;
  transform: translateY(110%); opacity: 0;
  transition: transform .95s var(--ease) .22s, opacity .6s var(--ease) .22s;
}
body.is-booted .hero__line--grad > span { transform: none; opacity: 1; }

/* THE CLAIM UNDER THE HEADLINE (2026-09-09, client revision).
   One figure and one link: the figure is the client's own published number,
   and the link goes to the wall of Google reviews further down this page,
   which is the evidence being offered. The two are separated by a rule
   rather than a dash so the link is visibly a different thing to click, and
   the whole row is small enough that it never competes with the headline it
   sits under. */
.hero__claim {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .5rem .9rem;
  margin: clamp(1.1rem, 2.4vh, 1.9rem) 0 0;
  font-size: clamp(.9rem, 1.05vw, 1.02rem); line-height: 1.4;
  color: var(--on-ink-2);
}
.hero__claimLink {
  display: inline-flex; align-items: center; gap: .4rem;
  padding-left: .9rem;
  border-left: 1px solid var(--line-solid);
  color: var(--on-ink-1); text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--beam) 55%, transparent);
  padding-bottom: .08em;
  transition: color .3s var(--ease), border-bottom-color .3s var(--ease);
}
.hero__claimLink .btn__arr { transition: transform .35s var(--ease); }
.hero__claimLink:hover, .hero__claimLink:focus-visible { color: var(--beam); border-bottom-color: var(--beam); }
.hero__claimLink:hover .btn__arr { transform: translateX(4px); }
/* a wrapped row has no vertical rule to hang the link off */
@media (max-width: 559px) { .hero__claimLink { padding-left: 0; border-left: 0; } }
@media (prefers-reduced-motion: reduce) {
  .hero__claimLink, .hero__claimLink .btn__arr { transition: none; }
  .hero__claimLink:hover .btn__arr { transform: none; }
}

/* the hairline that separates the headline from the argument */
@keyframes heroScroll {
  0%   { transform: translateY(-100%); }
  60%  { transform: translateY(100%); }
  100% { transform: translateY(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .scrollmark__line::after { animation: none; transform: none; }
}


/* ------------------------------------------------------------ stats bar -- */
/* Full-bleed rule, grid-aligned cells. These used to float on the right over
   the photographs, where they read as part of the wall rather than the hero. */


/* ------------------------------------------------------------- ticker -- */

.ticker__track {
  display: flex; flex: none; will-change: transform;
  animation: marquee 34s linear infinite;
}

.ticker__set {
  display: flex; align-items: center; gap: 1.9rem; padding-right: 1.9rem;
  white-space: nowrap; color: var(--on-ink-2);
}
.ticker__set i { color: var(--signal-2); font-style: normal; }
@keyframes marquee     { from { transform: translate3d(0,0,0); }      to { transform: translate3d(-50%,0,0); } }
@keyframes marquee-rev { from { transform: translate3d(-50%,0,0); }   to { transform: translate3d(0,0,0); } }

/* ----------------------------------------------------------- manifesto -- */
.manifesto { padding-block: clamp(6rem, 14vh, 11rem); }
.manifesto__text {
  margin-inline: auto;
  font-family: var(--display);
  font-weight: 500; font-stretch: 90%;
  font-size: clamp(1.6rem, 4.4vw, 3.5rem);
  line-height: 1.2; letter-spacing: -.025em;
  text-wrap: pretty;
  max-width: 20ch;
}
@media (min-width: 900px) { .manifesto__text { max-width: 22ch; } }
/* The break between the two sentences is a SMALL space (2026-09-14, client's
   word): half a line, not a paragraph gap. At this size anything larger reads
   as two blocks rather than one thought carried on. */
.manifesto__text p + p { margin-top: .42em; }
.manifesto__text w { opacity: .16; transition: opacity .18s linear; }
.manifesto__text w.hi { color: var(--beam); }

/* ================================================================ WORK == */
.work__filters {
  display: flex; flex-wrap: wrap; gap: .45rem;
  padding-bottom: clamp(1.6rem, 3vw, 2.4rem);
}
.chip {
  padding: .5rem .95rem; border-radius: 100px;
  border: 1px solid rgba(7, 11, 24, .16);
  font-family: var(--mono); font-size: var(--t-mono); font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase; color: var(--on-bone-2);
  transition: background-color .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease);
}
.chip:hover { border-color: var(--ink); color: var(--on-bone-1); }
.chip[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--bone); }
.chip b { font-weight: 500; opacity: .5; margin-left: .45rem; }

/* CSS Grid, not multicol: transformed elements inside a multicol fragment
   load their images but never paint them in Blink. */
.grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-flow: dense;
  gap: clamp(.55rem, 1vw, 1rem);
}
@media (min-width: 700px)  { .grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1100px) { .grid { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 1620px) { .grid { grid-template-columns: repeat(5, 1fr); } }

.tile {
  position: relative; display: block; width: 100%;
  aspect-ratio: 4 / 5;
  padding: 0; text-align: left; -webkit-appearance: none; appearance: none;
  border-radius: 12px; overflow: hidden;
  background: var(--ink-2);
  /* The tilt stays on the image: a 3D context on the tile fights its own
     overflow clip. The assembly transform below is deliberately 2D only. */
  transition: opacity .45s var(--ease);
}

/* ---- the wall assembles -------------------------------------------------
   Tiles arrive from the side of the grid they belong to, tipped and short of
   full size, then close inward into the layout. Each one carries its own
   --dx / --rot, measured once from where it actually sits, so the cascade
   still reads correctly at 2, 3, 4 or 5 columns.

   Driven by one-shot CSS transitions off an IntersectionObserver, never by a
   per-frame transform: this grid is two dozen large images, and moving them
   all every frame re-rasters the whole layer and locks the renderer.
   ------------------------------------------------------------------------ */
.js .tile {
  opacity: 0;
  transform: translate(var(--dx, 0px), var(--dy, 34px))
             rotate(var(--rot, 0deg)) scale(var(--ds, .88));
  transition: opacity .55s var(--ease), transform 1s var(--ease);
}
.js .tile.is-in { opacity: 1; transform: none; }
/* The tilt lives on the image, not on the tile: transforming the clipping
   container fights its own overflow and its border radius. */
/* --par is the scroll parallax (touch only — see the loop in site.js). It
   leads the chain so it stays a plain screen-space nudge instead of being
   foreshortened by the perspective. The extra resting scale that gives the
   shift room to move without uncovering an edge is applied ONLY where the
   parallax actually runs; desktop keeps its original 1.03 framing. */
.tile img {
  width: 100%; height: 100%; object-fit: cover;
  transform: translate3d(0, var(--par, 0px), 0)
             perspective(820px)
             rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg))
             scale(var(--z, 1.03));
  transition: transform .55s var(--ease), filter .5s var(--ease);
}
/* The transform transition exists to let the pointer tilt settle on release.
   On a touch device there is no tilt and no release — but there IS a
   per-frame parallax, and a .55s ease on that renders it as a laggy chase.
   Drop transform from the transition exactly where the tilt cannot happen. */
@media (hover: none) {
  .tile img { transition: filter .5s var(--ease); --z: 1.06; }
  /* headroom for the drift, only where the drift happens */
  .reason__art img { --parz: 1.08; }
  .receipt img { --parz: 1.06; }
}
/* screenshots read from the top edge — that's where the design lives */
.tile--top img { object-position: top center; }
/* a few pieces earn a double square, so the wall has rhythm */
.tile--big { grid-column: span 2; grid-row: span 2; aspect-ratio: auto; }
@media (max-width: 699px) { .tile--big { grid-row: span 1; aspect-ratio: 4 / 5; } }
/* A screenshot of a site or a social page is landscape, and the part that
   identifies it — the header, the profile, the name — is along its top. The
   4/5 crop the product art wants takes the middle of one of these and loses
   exactly that, so they sit wide and anchor to their top edge instead. */
.tile[data-craft="web"], .tile[data-craft="social"] { aspect-ratio: 5 / 4; }
.tile[data-craft="web"] { grid-column: span 2; aspect-ratio: 16 / 10; }
.tile[data-craft="web"] img, .tile[data-craft="social"] img { object-position: top center; }
@media (max-width: 699px) { .tile[data-craft="web"] { grid-column: span 2; } }

/* ONE SHAPE PER CRAFT (2026-09-09), AND ONE FIT WITH IT (2026-09-13).
   The wall used to hold four crafts of roughly one shape; it now holds nine
   of very different ones, and a single 4:5 crop serves none of them:

     campaign posts are SQUARE — a 4:5 crop shaves the top and bottom off
       artwork that was composed to its own edges
     ad accounts and Search Console are WIDE, and they are evidence: a
       cropped dashboard is not evidence, so these are contained whole on a
       white slide, the same rule the services page's panel shots follow
     films and product renders are 9:16, 4:5 or 16:9, and they CROP

   THE 2026-09-13 REVISION. The client said the graphic design wall was
   "aligned properly" and the rest was not, and they were reading the page
   correctly. Graphic design works because every source is square, the tile
   is square, and cover leaves ZERO dead ground: twenty identical full-bleed
   squares. Measured against that, four crafts failed, and all four failed
   the same way — a card whose picture does not fill it:

     3D ANIMATION  seven 9:16 film posters CONTAINED in a square, painting
       71px of navy down both sides, sitting in the same rows as six stills
       that bled full. One card size, two treatments, and the eye reads the
       difference immediately.
     VIDEO EDITING three card sizes in one row (321x402, 321x402, 657x411),
       because portrait films took a span-1 tile and landscape ones a span-2,
       and dense flow then left holes between them.
     PAID and SEO  contained evidence at source ratios from 0.73 to 3.01, so
       every card showed its screenshot at a different size — up to 176px of
       white down each side of one card and none on the next.

   WHAT CHANGED, AND WHY IT IS NOT THE SAME ANSWER EVERYWHERE. A film poster
   is a DOOR, not the work: data-video opens the real film in the lightbox at
   its own ratio and plays it uncropped, so cropping the poster asserts
   nothing false and costs nothing — those crafts get cover, like the design
   wall. A dashboard IS the work: crop it and the numbers it exists to show
   leave the frame. Cover was tried on paid and seo and it is instructive —
   it scores perfectly on every geometric measure and turns "Keywords that
   rank on the first page of Google search results" into "at rank on e of
   Google esults". So the evidence crafts keep contain, and their tidiness
   comes from the pixels instead: see tools/prep-thumbs.py.

   4:5 RATHER THAN SQUARE for the two film crafts. Square was the obvious
   choice — it is the shape the design wall already uses — but it crops more:
   a 9:16 film keeps 56% of its height in a square and 70% in a 4:5, and four
   of 3D animation's thirteen sources are ALREADY exactly 4:5, so they crop
   by nothing at all. Square also cut the arch out of design-18 and the
   branch out of design-26, which is the set dressing that makes them
   renders. Nothing essential leaves the frame at 4:5. */
.tile[data-craft="design"] { aspect-ratio: 1; }
/* NO PADDING, and that is a fix rather than a tidy-up. The tile is 16:10 and
   the screenshots are now cut to 16:10 — but padding sits INSIDE the box, and
   taking an equal number of pixels off a wide box's four sides leaves an
   inner box that is wider than 16:10 (638x390, or 1.636). A picture cut to
   1.600 therefore could not fill it, and every re-cut screenshot came out
   seven pixels short on each side: a hairline of white down both edges of
   every card, which is exactly the defect this was meant to remove. The
   white ground stays, because the wide strips still need somewhere to sit. */
.tile[data-craft="paid"], .tile[data-craft="seo"], .tile[data-craft="email"] {
  aspect-ratio: 16 / 10; grid-column: span 2;
  background: #fff;
}
/* Contained, and anchored to the TOP rather than centred. The letterbox
   cannot go to zero on a set of screenshots nobody controls the shape of,
   but where it FALLS is ours: centring floats every screenshot at a
   different height and the wall reads as scattered, while a common top edge
   gives every card the same starting line and the leftover white collects at
   the foot of each card, where it reads as the margin of a sheet. */
.tile[data-craft="paid"] img, .tile[data-craft="seo"] img, .tile[data-craft="email"] img {
  object-fit: contain; object-position: top center;
  /* AND NO OVERSCAN. Every other picture on this wall rests at scale(1.03),
     which is headroom: it lets the pointer tilt lean the image without
     uncovering an edge, and on a photograph nobody can tell that 3% is
     missing. On a screenshot they can — with the padding gone, that 3% ate
     the first character of every dashboard and "Performance" rendered as
     "erformance". A contained screenshot is shown to be READ, so it rests at
     1 and stays at 1 on hover; the lift and the border carry the hover
     instead, and a tilt over a white ground uncovers white. */
  --z: 1;
}
.tile[data-craft="paid"]:hover img, .tile[data-craft="seo"]:hover img, .tile[data-craft="email"]:hover img,
.tile[data-craft="paid"]:focus-visible img, .tile[data-craft="seo"]:focus-visible img, .tile[data-craft="email"]:focus-visible img {
  --z: 1;
}
.tile[data-craft="content"] { aspect-ratio: 16 / 10; grid-column: span 2; }
/* THE FILM CRAFTS. One shape, one span, one fit — the design wall's rule,
   applied to a poster instead of a post. Scoped by craft and not by
   .tile--vid: the class marks "this tile opens a film", which is about
   behaviour, and the shape belongs to the craft. */
.tile[data-craft="video"], .tile[data-craft="animation"] {
  aspect-ratio: 4 / 5; grid-column: auto;
}
.tile[data-craft="video"] img, .tile[data-craft="animation"] img { object-fit: cover; }
/* The ground still matters even though a covered poster never shows it: it
   is what a tile paints while its poster is still decoding. */
.tile--vid { background: #05053f; }
.tile:hover img, .tile:focus-visible img { --z: 1.08; }
.tile__meta {
  position: absolute; inset: auto 0 0 0; padding: 2.6rem .9rem .8rem;
  display: flex; align-items: flex-end; justify-content: space-between; gap: .8rem;
  background: linear-gradient(180deg, transparent, rgba(5, 5, 58, .92));
  opacity: 0; transform: translateY(10px);
  transition: opacity .35s var(--ease), transform .45s var(--ease);
}
.tile:hover .tile__meta, .tile:focus-visible .tile__meta { opacity: 1; transform: none; }
.tile__name { font-family: var(--display); font-weight: 700; font-stretch: 86%; font-size: 1.02rem; line-height: 1.1; color: var(--bone); letter-spacing: -.01em; }
.tile__craft { font-family: var(--mono); font-size: .58rem; letter-spacing: .16em; text-transform: uppercase; color: var(--beam); white-space: nowrap; }
/* A CLIP IN THE WALL (2026-09-09). Video editing and 3D animation are films,
   and a still off a film is not the work. These tiles show the poster with a
   play badge over it and open the clip in the preview. The badge is drawn,
   not an image: one element, a white disc and a triangle made of borders. */
.tile--vid .tile__play {
  position: absolute; inset: 0; margin: auto;
  width: 3.2rem; height: 3.2rem; border-radius: 50%;
  background: rgba(255, 255, 255, .92);
  box-shadow: 0 8px 26px rgba(4, 4, 40, .34);
  transition: transform .35s var(--ease), background-color .3s var(--ease);
}
.tile--vid .tile__play::after {
  content: ''; position: absolute; inset: 0; margin: auto;
  width: 0; height: 0;
  /* the triangle sits a hair right of centre: an equilateral mark centred on
     its bounding box always reads as leaning left inside a circle */
  border-left: .72rem solid var(--deep);
  border-top: .46rem solid transparent;
  border-bottom: .46rem solid transparent;
  transform: translateX(.12rem);
}
.tile--vid:hover .tile__play, .tile--vid:focus-visible .tile__play { transform: scale(1.08); background: #fff; }
@media (prefers-reduced-motion: reduce) { .tile--vid .tile__play { transition: none; } }

.tile.is-out { display: none; }
/* .tile sets display:block, which outranks the UA [hidden] rule */
.tile[hidden] { display: none; }

/* ---- the grid holds back until you ask -----------------------------------
   Collapsed, the grid is clipped to a row count and a veil sits over its lower
   edge: backdrop-filter blurs whatever shows through, and a mask ramp makes
   that blur arrive gradually, so the work dissolves into the ground instead of
   being sliced off. Same trick as the hero lens, run the other way round.
   Tiles fully past the fold are hidden by JS, so nothing invisible holds
   focus; the half-seen row is left in place as the thing being dissolved. */
.work__hold { position: relative; }
/* height comes from the tiles left on show, never a vh cap: a cap that
   disagrees with the tile count hides rows we meant to keep visible */
.work__hold.is-clipped { overflow: hidden; }

.work__fade {
  position: absolute; left: 0; right: 0; bottom: 0; height: clamp(190px, 26vh, 320px);
  pointer-events: none; opacity: 0; visibility: hidden;
  transition: opacity .5s var(--ease), visibility .5s;
  backdrop-filter: blur(11px) saturate(.7);
  -webkit-backdrop-filter: blur(11px) saturate(.7);
  /* the ramp is the whole effect: sharp at the top edge, gone at the bottom */
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 62%);
          mask-image: linear-gradient(180deg, transparent 0%, #000 62%);
  background: linear-gradient(180deg, rgba(238, 240, 255, 0) 0%, var(--ground, var(--bone)) 78%);
}
.work__hold.is-clipped .work__fade { opacity: 1; visibility: visible; }

.work__more { display: flex; justify-content: center; margin-top: clamp(1rem, 2.4vw, 1.9rem); }
.work__more-btn { gap: .6rem; }
.work__more-count {
  font-family: var(--mono); font-size: .6rem; font-weight: 500;
  letter-spacing: .14em; opacity: .62;
}

@media (prefers-reduced-motion: reduce) {
  .work__fade { transition: none; }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  /* no backdrop-filter: the gradient alone still reads as "more below" */
  .work__fade { background: linear-gradient(180deg, rgba(238, 240, 255, 0) 0%, var(--ground, var(--bone)) 62%); }
}

/* The claim and the evidence share the same screen, so the caveat rides
   with them. It used to be a footnote under the grid; the grid is gone and
   the preview is now the only place a brand name is put in front of anyone. */
.lb__note {
  margin: 0.5rem 0 0;
  max-width: 46ch;
  font-size: 0.72rem; line-height: 1.45;
  opacity: 0.62;
}

/* The act that used to live here — the shell breaking into service cards,
   drawn by gl.js on the passage's stage — is gone, and so are .spiral,
   .svc__rail, .svc__stage, .svc__brow, .svc__title and .svc__note, which
   nothing had used since. Its `.svc { height: 145vh }` was the reason the
   services page ran to fifteen thousand pixels: those rows are `.svc` too,
   so every craft stood a screen and a half tall, nearly all of it empty. */

/* ============================================================ RECEIPTS == */
.figs { display: grid; grid-template-columns: repeat(2, 1fr); gap: clamp(1.2rem, 2.4vw, 2rem); }
@media (min-width: 860px) { .figs { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1240px) { .figs { grid-template-columns: repeat(6, 1fr); } }
.fig { border-top: 1px solid var(--line); padding-top: 1.1rem; }
.fig:first-child { border-top-color: var(--beam); border-top-width: 2px; }
.fig b {
  display: block; font-family: var(--display); font-weight: 400; font-stretch: 80%;
  font-size: clamp(2.4rem, 5.4vw, 4.6rem); line-height: 1; letter-spacing: -.045em;
}
.fig span { display: block; margin-top: .5rem; color: var(--on-ink-3); }

.logos { margin-top: clamp(3rem, 6vw, 5rem); }
.logos__rows { display: flex; flex-direction: column; gap: clamp(.55rem, .9vw, .8rem); margin-top: 1.4rem; }
.logos__row { display: flex; overflow: hidden; }
.logos__track { display: flex; flex: none; gap: clamp(.55rem, .9vw, .8rem); padding-right: clamp(.55rem, .9vw, .8rem); animation: marquee 58s linear infinite; will-change: transform; }
.logos__row--rev .logos__track { animation-name: marquee-rev; animation-duration: 66s; }
.logo {
  flex: none; display: grid; place-items: center;
  width: clamp(122px, 13vw, 172px); height: clamp(64px, 6.6vw, 88px);
  border-radius: 10px; background: rgba(255, 255, 255, .93);
  overflow: hidden;
  transition: background-color .4s var(--ease), transform .4s var(--ease);
}
/* dock proximity — the wall responds to the pointer before you reach it */
.logo { transform: scale(var(--p, 1)); }
.logo:hover { background: #fff; }
.logo img {
  width: 100%; height: 100%; object-fit: contain; padding: 8px 14px;
  filter: grayscale(1) contrast(1.06); opacity: .78;
  transition: filter .4s var(--ease), opacity .4s var(--ease);
}
.logo:hover img { filter: none; opacity: 1; }

.receipts { margin-top: clamp(3rem, 6vw, 5rem); }
.receipts__strip { display: flex; gap: clamp(.6rem, 1vw, .9rem); overflow-x: auto; padding-bottom: 1rem; margin-top: 1.4rem; scrollbar-width: none; scroll-snap-type: x proximity; }
.receipts__strip::-webkit-scrollbar { height: 0; }
.receipt {
  flex: none; width: clamp(230px, 27vw, 330px);
  border-radius: 12px; overflow: hidden; border: 1px solid var(--line);
  background: var(--ink-2); scroll-snap-align: start;
}
.receipt img { width: 100%; aspect-ratio: 16/10; object-fit: cover; object-position: top left; opacity: .82; transition: opacity .4s var(--ease); transform: translate3d(0, var(--par, 0px), 0) scale(var(--parz, 1)); }
.receipt:hover img { opacity: 1; }
.receipt figcaption { padding: .7rem .85rem .85rem; color: var(--on-ink-2); font-size: .82rem; }
.receipt figcaption b { display: block; font-family: var(--mono); font-size: .6rem; letter-spacing: .16em; text-transform: uppercase; color: var(--beam); margin-bottom: .3rem; font-weight: 500; }

/* ============================================================== REASONS == */
.stack { position: relative; }
.reason {
  position: sticky;
  display: grid; gap: clamp(1.2rem, 2.6vw, 2.4rem);
  grid-template-columns: 1fr;
  align-items: center;
  margin-bottom: clamp(1rem, 1.8vw, 1.6rem);
  padding: clamp(1.6rem, 3vw, 2.8rem);
  border-radius: 18px;
  background: #fff; border: 1px solid rgba(7, 11, 24, .1);
  box-shadow: 0 24px 60px rgba(7, 11, 24, .1);
  transform-origin: 50% 0%;
  will-change: transform;
}
@media (min-width: 980px) { .reason { grid-template-columns: 88px 1fr 300px; } }
.reason__no { font-family: var(--display); font-weight: 400; font-stretch: 80%; font-size: clamp(2.4rem, 4.4vw, 3.9rem); line-height: 1; color: var(--signal); }
.reason__copy p { margin-top: .9rem; max-width: 58ch; color: var(--on-bone-2); text-wrap: pretty; }
.reason__art { position: relative; border-radius: 12px; overflow: hidden; aspect-ratio: 16/10; background: #E6EDFB; border: 1px solid rgba(29, 78, 216, .16); }
.reason__art img {
  width: 100%; height: 100%; object-fit: cover;
  transform: translate3d(0, var(--par, 0px), 0) scale(var(--parz, 1));
}
.reason--dark { background: var(--ink); border-color: rgba(255, 255, 255, .17); color: var(--bone); }
.reason--dark .reason__no { color: var(--beam); }
.reason--dark .reason__copy p { color: var(--on-ink-2); }
.reason--dark .reason__art { background: rgba(255, 255, 255, .07); border-color: rgba(127, 178, 255, .22); }
.clock { display: grid; place-items: center; height: 100%; text-align: center; }
.clock b { font-family: var(--display); font-weight: 400; font-stretch: 80%; font-size: clamp(1.8rem, 3.4vw, 2.9rem); line-height: 1.02; color: var(--beam); display: block; }
.clock span { display: block; margin-top: .6rem; color: var(--on-ink-3); }

/* ============================================================== VOICES == */
.voices { display: grid; gap: clamp(.9rem, 1.6vw, 1.4rem); grid-template-columns: 1fr; }
@media (min-width: 780px)  { .voices { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1180px) { .voices { grid-template-columns: repeat(3, 1fr); } }
.voice {
  display: flex; flex-direction: column; justify-content: space-between; gap: 2rem;
  padding: clamp(1.5rem, 2.4vw, 2.1rem);
  border-radius: 16px; background: #fff; border: 1px solid rgba(7, 11, 24, .1);
  transition: border-color .3s var(--ease), transform .5s var(--ease);
}
.voice:hover { border-color: rgba(29, 78, 216, .55); transform: translateY(-4px); }
.voice__mark { font-family: var(--display); font-size: 3rem; line-height: .5; color: var(--signal); }
.voice blockquote { margin-top: 1.1rem; font-size: clamp(1.05rem, 1.5vw, 1.24rem); line-height: 1.45; text-wrap: pretty; }
.voice__who { display: flex; align-items: center; gap: .8rem; }
.voice__who img { width: 44px; height: 44px; border-radius: 100px; object-fit: cover; flex: none; }
.voice__who b { display: block; font-size: .92rem; }
.voice__who span { display: block; font-size: .8rem; color: var(--on-bone-2); }

/* ================================================================= CTA == */
.close { position: relative; overflow: hidden; padding-bottom: 0; }
.close__ring {
  position: absolute; left: 50%; top: 24%; z-index: 0;
  width: min(1000px, 130%); aspect-ratio: 1; transform: translate(-50%, -50%);
  border-radius: 50%;
  background: conic-gradient(from 90deg, transparent, rgba(29, 78, 216, .52), rgba(127, 178, 255, .34), transparent 74%);
  -webkit-mask-image: radial-gradient(closest-side, transparent 46%, #000 62%, #000 72%, transparent 90%);
          mask-image: radial-gradient(closest-side, transparent 46%, #000 62%, #000 72%, transparent 90%);
  filter: blur(76px);
  animation: spin 60s linear infinite;
}
@keyframes spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
/* the generative backdrop: above the blurred ring, below the content */
/* Bounded to the top of the section on purpose: the figure is widest at its
   own vertical centre, and on a phone the footer stacks tall enough that
   centring on the whole section drags the widest part across the address. */
.waves {
  position: absolute; left: 0; top: 0;
  /* A canvas is a replaced element, so an absolutely positioned one with
     width:auto takes its INTRINSIC width from the width attribute and ignores
     right:0 entirely. The box has to be sized explicitly or it just follows
     the backing store the script sets from the box — which never settles. */
  width: 100%;
  height: min(100%, 860px);
  z-index: 0; display: block; pointer-events: none;
}
.close__inner { position: relative; z-index: 1; }
.close__grid { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: clamp(2rem, 4vw, 3.5rem); }
.close__title { font-size: var(--t-mega); }
.close__acts { display: flex; flex-wrap: wrap; gap: .7rem; margin-top: clamp(1.6rem, 3vw, 2.4rem); }

.badge { position: relative; flex: none; width: clamp(112px, 11vw, 148px); aspect-ratio: 1; }
.badge__ring { position: absolute; inset: 0; animation: spin-cw 24s linear infinite; }
@keyframes spin-cw { to { transform: rotate(360deg); } }
.badge__ring text { font-family: var(--mono); font-size: 10.5px; letter-spacing: .2em; fill: rgba(239, 234, 225, .6); text-transform: uppercase; }
.badge__hub {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: grid; place-items: center; width: 58%; aspect-ratio: 1; border-radius: 100px;
  background: var(--bone); color: var(--ink); font-weight: 600; font-size: .84rem;
  transition: background-color .3s var(--ease), color .3s var(--ease);
}
.badge__hub:hover { background: var(--signal); color: #fff; }

/* -------------------------------------------------------------- footer -- */
.foot {
  position: relative; z-index: 1;
  margin-top: clamp(4rem, 8vw, 6.5rem);
  padding-top: clamp(2rem, 3vw, 2.6rem);
  border-top: 1px solid var(--line);
  display: grid; gap: clamp(1.6rem, 3vw, 2.4rem);
  grid-template-columns: 1fr;
}
@media (min-width: 720px)  { .foot { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .foot { grid-template-columns: 1.6fr 1fr 1fr 1fr; } }
/* Sized by HEIGHT, not width: the mark is a monogram, so its cap height is
   what has to agree with the wordmark beneath it. (The old asset was a bad
   crop of a larger lockup — ink ran off all four edges of its own canvas —
   so no amount of CSS could have made it whole; mark-lockup.png is cut from
   the untrimmed source with its own margin.) */
.foot__mark img { width: auto; height: 64px; opacity: .9; }
.foot__name {
  display: block; margin-top: 1rem;
  font-family: var(--display); font-weight: 700; font-stretch: 80%;
  font-size: 1.15rem; letter-spacing: -.02em; text-transform: uppercase;
}
.foot p { margin-top: 1rem; color: var(--on-ink-3); font-size: .86rem; line-height: 1.6; }
.foot a { color: var(--on-ink-2); font-size: .88rem; }
.foot a:hover { color: var(--bone); }
.foot__col ul { display: flex; flex-direction: column; gap: .55rem; margin-top: .9rem; list-style: none; padding: 0; }
.foot__legal { margin-top: 1.4rem; font-size: .76rem; color: var(--on-ink-3); }

.stamp { position: relative; z-index: 1; display: flex; align-items: center; height: 56px; margin-inline: calc(var(--gut) * -1); border-top: 1px solid var(--line); overflow: hidden; }

/* <main> is the positioning context for absolutely-placed section decoration. */
main { position: relative; }

/* ==================================================================== GL ==
   Three.js layers. Each one sits behind or replaces a DOM fallback, so the
   page is complete before a single shader compiles.
   ========================================================================== */
.gl-canvas {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: block; z-index: 0;
}
/* The strip stays laid out even when WebGL paints it: its boxes are the
   geometry and its scrollLeft is the scroll position. Only the pixels go.
   Hiding it with display:none would take the layout away with them. */
.gl-on .shelf__fallback img { opacity: 0; }
/* snapping fights the programmatic scrollLeft that drag and wheel set */
.gl-on .shelf__fallback { scroll-snap-type: none; }
/* the canvas is decoration; the strip underneath keeps native touch panning */
.shelf__stage .gl-canvas { pointer-events: none; }
.shelf.is-grabbing .shelf__stage { cursor: grabbing; }

/* --- the product shelf --------------------------------------------------- */
.shelf { overflow: hidden; padding-bottom: clamp(2.5rem, 5vw, 4rem); }
.shelf__head {
  display: flex; flex-wrap: wrap; gap: clamp(1.5rem, 3vw, 3rem);
  align-items: flex-end; justify-content: space-between;
  padding-bottom: clamp(2rem, 4vw, 3rem);
}
.shelf__stage {
  position: relative;
  height: clamp(380px, 39vw, 500px);
  margin-inline: calc(var(--gut) * -1);
  /* the strip pans sideways, the page still scrolls down through it */
  touch-action: pan-x pan-y;
  cursor: grab;
}
.shelf__fallback {
  display: flex; gap: clamp(.6rem, 1vw, 1rem); height: 100%;
  padding-inline: var(--gut);
  overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none;
}
.shelf__fallback::-webkit-scrollbar { height: 0; }
.shelf__fallback img {
  flex: none; height: 100%; width: auto; border-radius: 14px;
  object-fit: cover; scroll-snap-align: center;
}

/* --- the screening room --------------------------------------------------
   Was a plain 16:9 <video> in a rounded box. It is now a set: two halves of
   one render (room-back / room-floor) with the showreel standing in the empty
   panel that was rendered into the back wall.

   Geometry is quoted as percentages of the PLATE, never of the viewport —
   .studio__dolly is locked to the plate's own 2752x1536 aspect and sized to
   cover, so a percentage inside it lands on the same brick at every window
   size. The numbers below were measured off room-master.webp; if the plates
   are ever regenerated, re-measure rather than nudge.
   ------------------------------------------------------------------------ */
/* `overflow: hidden` here would be quietly fatal: it makes this element a
   scroll container, and a scroll container ancestor cancels `position: sticky`
   on everything inside it — the stage would simply scroll past instead of
   pinning, with no error to explain why. `clip` does the same clipping
   without becoming one. For the same reason nothing between here and the
   stage may take a `transform`, which would make it a containing block. */
.studio { overflow: clip; }

/* The title card. It rides over the dark at the top of the pin and clears as
   the room resolves, which is the whole point: with #facts gone the passage
   above ends on the same ink this section opens on, so a heading block
   standing between them would be the only thing left to read as a cut. */
.studio__title {
  position: absolute; inset: 0; z-index: 3;
  display: flex; align-items: center;
  padding-inline: var(--gut);
  pointer-events: none;
  opacity: var(--title, 1);
  will-change: opacity, transform;
  transform: translateY(calc(var(--title-lift, 0) * -1.4rem));
}
.studio__title-in {
  width: 100%;
  display: flex; flex-wrap: wrap; gap: clamp(1.5rem, 3vw, 3rem);
  align-items: flex-end; justify-content: space-between;
}
/* Out through the section's own gutter, so the room is full-bleed while the
   heading above it stays on the page's grid. Negative margins rather than
   `100vw`, which would be wider than the viewport whenever a scrollbar is
   present, and rather than a transform, which would break the sticky. */
.studio__track { height: 340vh; margin-inline: calc(-1 * var(--gut)); }
.studio__stage {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
  background: transparent;   /* the section's ground carries the dim */
}

/* The house lights going down. The passage above this section ends on BONE —
   --ground runs to 1 across the strip — so the studio does not inherit a dark
   room to appear in, it has to make one. The ground crosses from bone to deep
   over the head of the pin, and only then does the room fade up out of it. A
   screening room is the one place where that reads as the subject rather than
   as a transition. Specificity has to clear `:root .sec--deep`. */
:root .sec--deep.studio {
  background: color-mix(in oklab, var(--deep) calc(var(--dim, 1) * 100%), var(--bone));
}

/* Cover, but for a fixed-aspect box: wider viewport than the plate and the
   width leads; narrower and the height does. object-fit cannot be used here —
   the panel has to stay findable in percentages, and cover would crop the
   coordinate space out from under it. */
/* Over the passage's CANVAS, which sits at z-index 3 — neither .passage nor
   .studio makes a stacking context (position: relative, z-index auto), so the
   canvas and this veil are compared in the root context and 0 would lose. It
   has to win: the canvas ends at the passage's bottom edge and cuts whatever
   hexagons are crossing it clean in half, and a veil underneath would darken
   the ground while leaving that bright clipped edge on top of it. Under the
   room (6) and well under the nav (120).

   Fixed elements are not clipped by an ancestor's overflow — their containing
   block is the viewport — so `.studio { overflow: clip }` does not trap it. */
.studio__veil {
  position: fixed; inset: 0; z-index: 5;
  background: var(--deep); opacity: var(--veil, 0);
  pointer-events: none;
}
.studio__track { position: relative; z-index: 6; }
.studio__room { position: absolute; inset: 0; overflow: hidden; }

/* --enter and --fade are the arrival, written by studio(). The room comes up
   out of the same ink the hexagon field above was drifting in and settles the
   last tenth of its scale, so the two rooms share a ground and there is no
   edge between them to see. */
.studio__dolly {
  position: absolute; top: 50%; left: 50%;
  width: 100%; aspect-ratio: 2752 / 1536;
  transform: translate(-50%, -50%) scale(var(--enter, 1));
  opacity: var(--fade, 1);
}
@media (max-aspect-ratio: 2752 / 1536) {
  .studio__dolly { width: auto; height: 100%; aspect-ratio: 2752 / 1536; }
}

.studio__far, .studio__near, .studio__cast {
  position: absolute; inset: 0;
  /* the vanishing point of the room, so growth reads as approach */
  transform-origin: 50.15% 49.64%;
  will-change: transform;
}
.studio__plate { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* the panel rendered into the back wall */
.studio__panel {
  position: absolute;
  left: 40.334%; top: 38.737%; width: 19.622%; height: 21.810%;
  overflow: hidden; background: #05070d;
}
/* The plate's panel is 540x335 — near enough 16:10, not 16:9. Fitting the
   footage to the panel WIDTH leaves a sliver of the rendered panel showing
   above and below, which reads as bezel. Filling the box instead would crop
   a tenth off both sides of every shot. */
.studio__video {
  position: absolute; left: 0; width: 100%;
  top: 50%; height: 90.67%; transform: translateY(-50%);
  object-fit: cover; display: block;
}
.studio__grain {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background-image: radial-gradient(circle, rgba(255,255,255,.5) 0 1px, transparent 1.1px);
  background-size: 3px 3px; opacity: .05; mix-blend-mode: overlay;
}

/* Light off the screen. The panel was rendered near-black, so without spill
   the footage reads as a sticker rather than as the only lit thing in the
   room. Opacity is driven from the scrub. */
.studio__bloom {
  position: absolute;
  left: 26%; top: 24%; width: 48%; height: 52%;
  pointer-events: none; mix-blend-mode: screen; opacity: 0;
  background: radial-gradient(ellipse at 50% 50%,
              rgba(120,160,255,.34) 0%, rgba(60,90,220,.16) 42%, transparent 72%);
  filter: blur(28px);
}

/* Thrown onto the floor. scaleY(-1) on the video, with the fade on the
   wrapper so the mask runs away from the screen rather than toward it. */
.studio__cast {
  --cast: .3;
}
.studio__cast > .studio__castvid {
  position: absolute;
  left: 40.334%; top: 62.2%; width: 19.622%; height: 19.775%;
  object-fit: cover; transform: scaleY(-1);
  opacity: var(--cast); filter: blur(7px) saturate(1.25);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 78%);
          mask-image: linear-gradient(to bottom, #000 0%, transparent 78%);
}

/* Anyone who asked for less motion gets the room still, at any width: no pin,
   no travel, no arrival, and the title back in normal flow above it rather
   than overlaid on a screen it would sit on top of. */
@media (prefers-reduced-motion: reduce) {
  :root .sec--deep.studio { background: var(--deep); }
  .studio__veil { display: none; }
  .studio__track { height: auto !important; }
  .studio__stage {
    position: relative !important; top: auto !important; height: auto;
    display: block;
  }
  .studio__title {
    position: static; inset: auto; opacity: 1 !important; transform: none !important;
    padding: 0 0 clamp(2rem, 4vw, 3rem);
  }
  .studio__room { position: relative; inset: auto; }
  .studio__far, .studio__near, .studio__cast { transform: none !important; }
  .studio__dolly { opacity: 1 !important; }
  .studio__cast { display: none; }
  .studio__bloom { opacity: .55 !important; }
}

/* Still, but the whole room. A desktop reader who asked for less motion has
   the width for the wide shot and should not be handed the phone's crop.
   The dolly fills the room outright: `height: 100%` cannot resolve against a
   parent whose height comes from aspect-ratio — that computed height is still
   `auto`, so the percentage has nothing to resolve against and the dolly
   collapses, taking the panel out of the room with it. */
@media (prefers-reduced-motion: reduce) and (min-width: 701px) {
  .studio__room { aspect-ratio: 2752 / 1536; }
  .studio__dolly {
    position: absolute; inset: 0;
    width: auto; height: auto; aspect-ratio: auto; transform: none !important;
  }
}
@media (prefers-reduced-motion: reduce) and (max-width: 700px) {
  .studio__room { aspect-ratio: 4 / 5; }
}

/* Phones take the same walk, from a closer mark. Fitting the whole room on a
   375px stage puts the panel at 19.6% of it — a 66px screen, which is not a
   showreel, it is a thumbnail. Stepping in costs the ceiling and the far
   corners and buys a screen you can actually watch.

   The stage is shorter than the viewport and sticks near the middle of it
   rather than at the top, so the room holds the eye's centre for the whole
   pin instead of riding up under the nav. */
@media (max-width: 700px) {
  .studio__track { height: 280vh; }
  .studio__stage {
    position: sticky;
    /* height must be released or the base rule's 100vh wins and aspect-ratio
       runs backwards, deriving the WIDTH from it — a 675px stage on a 390px
       phone. The track is full-bleed, so the stage is 100vw across and
       1.25x that tall; centre what is left of the viewport. */
    height: auto;
    top: calc((100svh - 100vw * 1.25) / 2);
    aspect-ratio: 4 / 5;
  }
  .studio__dolly {
    width: 300%; height: auto; aspect-ratio: 2752 / 1536;
  }
  .studio__cast { display: none; }   /* a second decode is not worth it here */
  .studio__title-in { flex-direction: column; align-items: flex-start; }
}


@media (prefers-reduced-motion: reduce) {
  .shelf__stage { touch-action: auto; }
}

/* ============================================== motion, section by section =
   Techniques adapted from the Codrops hub, one per section that needed one.
   ========================================================================== */

/* Stagger Reveal Animations for Text (p14) — every section head now opens
   the way the hero does, so the page reads as one system. */
.rl { display: block; overflow: hidden; padding-bottom: .06em; }
.js .rl > span { display: block; transform: translateY(106%); transition: transform .95s var(--ease); }
.js .rl.is-in > span { transform: none; }
.js .rl:nth-child(2) > span { transition-delay: .08s; }
.js .rl:nth-child(3) > span { transition-delay: .16s; }

/* Gooey Text Reveal — ported from bleibtgleich.dev; see gooHeads() in
   site.js. The line-roll is off for these two reasons: the paint has to
   bleed well past the line box, and `overflow: hidden` would slice it flat;
   and a line cannot roll and come into focus at once without reading as two
   effects fighting. Only headings the script actually armed carry the
   attribute, so anything it skipped keeps the roll untouched. */
.js [data-goo] .rl { overflow: visible; }
.js [data-goo] .rl > span { transform: none; transition: none; }

/* Blurry Text Reveal on Scroll (p8) — the manifesto pulls into focus word by
   word instead of just fading. One class toggle per word, not a per-frame
   filter, so the blur stays cheap. */
.manifesto__text w {
  opacity: .13;
  filter: blur(7px);
  transition: opacity .5s var(--ease), filter .5s var(--ease);
}
.manifesto__text w.on { opacity: 1; filter: blur(0); }
.manifesto__text w.hi.on { color: var(--beam); }

/* Gradient Mask Hover from Evervault (p9) + Tilt Hover (p17) — the quote
   cards were the last dead surface on the page. */
.voice { position: relative; overflow: hidden; }
.voice::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(240px circle at var(--gx, 50%) var(--gy, 50%),
              rgba(29, 78, 216, .13), transparent 60%);
  opacity: 0; transition: opacity .4s var(--ease);
}
.voice:hover::before { opacity: 1; }
.voice > * { position: relative; }

/* Draggable Image Strip (p15) — the receipts read as a physical stack of
   printouts you can shove along. */
.receipts__strip { cursor: grab; }
.receipts__strip.is-dragging { cursor: grabbing; scroll-snap-type: none; user-select: none; }
.receipts__strip.is-dragging .receipt { pointer-events: none; }

/* Animate SVG Shapes on Scroll (p11) — a spine drawn down the reason stack
   so five sticky cards read as one sequence. */
.stack { position: relative; }
/* sits above the sticky cards, in their left padding, so it reads as one
   thread running through all five */
.spine {
  position: absolute; left: clamp(1.05rem, 2.1vw, 1.75rem); top: 0; bottom: 0;
  width: 2px; pointer-events: none; z-index: 3;
  background: rgba(7, 11, 24, .12);
  border-radius: 2px; overflow: hidden;
}
.spine i {
  display: block; width: 100%; height: var(--fill, 0%);
  background: linear-gradient(180deg, var(--signal), var(--beam));
}
@media (max-width: 979px) { .spine { display: none; } }


/* Footer Bounce Based on Scroll Speed (p13) */
.foot, .stamp { transform: translateY(var(--bounce, 0px)); }

/* Simple CSS Line Hover Animations for Links (p13) */
.foot a, .menu__foot a {
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size .38s var(--ease), color .25s var(--ease);
}
.foot a:hover, .menu__foot a:hover { background-size: 100% 1px; }

/* Letter Shuffle Animation for a Menu (p11) */
.menu__link .sh { display: inline-block; }

/* K72 Marquee Hover (p12) — the ticker leans away from the pointer */

.ticker__set { transition: transform .35s var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .js .rl > span { transform: none !important; }
  .manifesto__text w { opacity: 1 !important; filter: none !important; }
  .foot, .stamp { transform: none !important; }
}

/* ------------------------------------------------------------- reveals -- */
/* The same bell the home page's staged items ride: --p is written per item
   by the scroll loop, rises as the item comes up the frame and falls again
   as it leaves the top. It replaced a one-way observer whose class, once
   added, was never taken off — so an inner page's copy revealed and then
   scrolled away at full strength while the home page's let go. The stagger
   that data-rise-d used to buy with transition delays is in the geometry
   now: two items at different heights are at different points of the same
   bell. */
.js [data-rise] {
  opacity: calc(.06 + var(--p) * .94);
  transform: translateY(calc((1 - var(--p)) * 22px));
}

/* ============================================================== REVIEWS ==
   The score carries the section — a 5.0 that draws its own stars — and the
   quotes are a physical deck you deal through rather than a row of cards
   nobody reads past the first.
   ========================================================================== */
.reviews__grid {
  display: grid; gap: clamp(2rem, 4vw, 3.5rem);
  grid-template-columns: 1fr;
  align-items: start;
}
@media (min-width: 980px) { .reviews__grid { grid-template-columns: minmax(240px, 30%) 1fr; } }

.score__num {
  font-family: var(--display); font-weight: 400; font-stretch: 80%;
  font-size: clamp(4.5rem, 11vw, 9rem); line-height: .82;
  letter-spacing: -.05em; color: var(--signal);
}
.score__stars {
  display: block; width: min(100%, 260px); height: auto;
  margin-top: clamp(.8rem, 1.6vw, 1.2rem);
  color: var(--signal);
}
/* each star traces itself, then fills */
.score__stars path {
  stroke-dasharray: 130;
  stroke-dashoffset: 130;
  fill: transparent;
}
.js .score.is-in .score__stars path {
  animation: starDraw .7s var(--ease) forwards, starFill .4s var(--ease) forwards;
}
.score__stars path:nth-child(1) { animation-delay: 0s, .55s; }
.score__stars path:nth-child(2) { animation-delay: .1s, .65s; }
.score__stars path:nth-child(3) { animation-delay: .2s, .75s; }
.score__stars path:nth-child(4) { animation-delay: .3s, .85s; }
.score__stars path:nth-child(5) { animation-delay: .4s, .95s; }
@keyframes starDraw { to { stroke-dashoffset: 0; } }
@keyframes starFill { to { fill: currentColor; } }

.score__meta { margin-top: clamp(1rem, 2vw, 1.5rem); color: var(--on-bone-2); }
.score__meta b { font-weight: 500; color: var(--on-bone-1); }
.score__src {
  display: inline-flex; align-items: center; gap: .4rem;
  margin-top: .7rem; font-size: .9rem; color: var(--signal);
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat; background-position: 0 100%; background-size: 100% 1px;
  transition: background-size .35s var(--ease);
}
.score__src:hover { background-size: 0% 1px; }

/* --- the deck ------------------------------------------------------------ */
.deck { position: relative; }
.deck__cards {
  position: relative; list-style: none; margin: 0; padding: 0;
  height: clamp(300px, 34vw, 360px);
  /* headroom for the fanned cards behind the live one */
  margin-top: 34px; margin-right: 40px;
}
.rev {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; justify-content: space-between; gap: 1.4rem;
  padding: clamp(1.5rem, 2.6vw, 2.4rem);
  border-radius: 18px;
  background: #fff;
  border: 1px solid rgba(10, 10, 45, .10);
  box-shadow: 0 18px 44px rgba(10, 10, 60, .10);
  /* --i is the depth in the stack, written by the script. The cards behind
     fan up and to the right so the pile is visible without the top card
     having to shrink much. */
  transform:
    translate(calc(var(--i, 0) * 18px), calc(var(--i, 0) * -14px))
    rotate(calc(var(--i, 0) * 1.6deg))
    scale(calc(1 - var(--i, 0) * 0.03));
  transform-origin: 0% 100%;
  z-index: calc(20 - var(--i, 0));
  opacity: 1;
  transition: transform .55s var(--ease), opacity .45s var(--ease),
              box-shadow .45s var(--ease);
}
.rev[data-hidden] { opacity: 0; }
/* only the live card takes the pointer, so clicks never hit the stack behind */
.rev { pointer-events: none; overflow: hidden; }
.rev.is-live { pointer-events: auto; cursor: pointer; }
/* the card being dealt away lifts and slides off before rejoining the back */
.rev.is-dealt {
  transform: translate(-14%, -8%) rotate(-9deg) scale(.94);
  opacity: 0;
}
.rev__stars { color: var(--signal); letter-spacing: .22em; font-size: .95rem; }
.rev blockquote {
  font-size: clamp(1.05rem, 1.8vw, 1.45rem);
  line-height: 1.4; text-wrap: pretty;
  /* a real Google review can run long; the deck stays uniform */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 6;
  overflow: hidden;
}
.rev__who { display: flex; align-items: center; gap: .85rem; }
.rev__who img { width: 46px; height: 46px; border-radius: 100px; object-fit: cover; flex: none; }
.rev__who b { display: block; font-size: .95rem; }
.rev__who span { display: block; font-size: .82rem; color: var(--on-bone-2); }

.deck__ctl {
  display: flex; align-items: center; gap: .6rem;
  margin-top: clamp(1.4rem, 2.4vw, 2rem);
}
.deck__count { margin-right: auto; color: var(--on-bone-3); }
.deck__btn {
  display: grid; place-items: center; width: 44px; height: 44px;
  border-radius: 100px; border: 1px solid rgba(10, 10, 45, .18);
  color: var(--on-bone-1); font-size: 1.05rem; line-height: 1;
  transition: background-color .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease);
}
.deck__btn:hover { background: var(--signal); color: #fff; border-color: var(--signal); }

@media (prefers-reduced-motion: reduce) {
  .score__stars path { stroke-dashoffset: 0; fill: currentColor; }
  .js .score.is-in .score__stars path { animation: none; }
  .rev { transition: none; }
}

/* ============================================================== PRICING ==
   Self-contained tiers in the Zyner pattern the client asked for: a billing
   toggle, a "recommended for" line, the price, both calls to action ABOVE
   the checklist, and "Everything in X" rollups so the lists stay short.
   ========================================================================== */
.pricing__head {
  max-width: 62ch;
  padding-bottom: clamp(2.2rem, 4vw, 3.4rem);
}
.pricing__head .h2 { margin-top: .9rem; }
.pricing__head .lead { margin-top: 1.1rem; max-width: 54ch; }

/* --- the object ---------------------------------------------------------- */
.object { overflow: hidden; }
.object__head {
  display: flex; flex-wrap: wrap; gap: clamp(1.5rem, 3vw, 3rem);
  align-items: flex-end; justify-content: space-between;
  padding-bottom: clamp(1.6rem, 3vw, 2.6rem);
}
.object__stage {
  /* a faint brand halo lifts the object off the flat band */
  background: radial-gradient(58% 46% at 50% 60%, rgba(8, 8, 138, .075), transparent 72%);
  position: relative;
  height: clamp(380px, 54vh, 640px);
  touch-action: pan-y;
  cursor: grab;
}
.object.is-turning .object__stage { cursor: grabbing; }
.object__stage .gl-canvas { opacity: 0; transition: opacity 1s var(--ease); }
.object.gl-on .object__stage .gl-canvas { opacity: 1; }

/* the still holds the section until - and unless - WebGL draws */
.object__fallback {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  max-height: 100%; width: auto;
  opacity: 1; transition: opacity .8s var(--ease);
}
.object.gl-on .object__fallback { opacity: 0; }

/* --- the numbers pinned to it -------------------------------------------
   Each mark is positioned every frame from a point projected out of the 3D
   scene, so translate() is owned by the script. Everything the CSS animates
   therefore has to live on inner elements, or the two fight each other. */
.object__marks { position: absolute; inset: 0; pointer-events: none; }
.omark { position: absolute; left: 0; top: 0; will-change: transform; }
.omark__dot, .omark__line, .omark__body {
  opacity: 0; transition: opacity .5s var(--ease), transform .7s var(--ease);
  transition-delay: calc(var(--i) * 140ms);
}
.object__marks.is-in .omark__dot,
.object__marks.is-in .omark__line,
.object__marks.is-in .omark__body { opacity: 1; transform: none; }

.omark__dot {
  position: absolute; left: -4px; top: -4px;
  width: 8px; height: 8px; border-radius: 100px;
  background: var(--signal); box-shadow: 0 0 0 4px rgba(8, 8, 138, .14);
  transform: scale(.2);
}
.omark__line {
  position: absolute; left: 4px; top: -1px;
  width: clamp(26px, 3vw, 46px); height: 1px;
  background: linear-gradient(90deg, var(--signal), rgba(8, 8, 138, .25));
  transform-origin: left center; transform: scaleX(0);
}
.omark__body {
  position: absolute; top: 50%; translate: 0 -50%;
  left: calc(4px + clamp(26px, 3vw, 46px) + 10px);
  display: flex; align-items: baseline; gap: .5rem;
  white-space: nowrap; transform: translateX(-6px);
}
.omark__body b {
  font-family: var(--display); font-weight: 400; font-stretch: 84%;
  font-size: clamp(1.3rem, 2.2vw, 2rem); line-height: 1;
  letter-spacing: -.02em; color: var(--on-bone-1);
}
.omark__body em {
  font-family: var(--mono); font-style: normal;
  font-size: clamp(.56rem, .8vw, .66rem); letter-spacing: .16em;
  text-transform: uppercase; color: var(--on-bone-3);
}
/* a mark whose anchor sits in the left half lays its text outward (to the
   left of the dot), so labels never run across the object */
.omark.is-flip .omark__line { left: auto; right: 4px; transform-origin: right center; }
.omark.is-flip .omark__body {
  left: auto; right: calc(4px + clamp(26px, 3vw, 46px) + 10px);
  flex-direction: row-reverse;
}

/* a mark on the far side of the object recedes rather than competing */
.omark.is-back .omark__dot,
.omark.is-back .omark__line,
.omark.is-back .omark__body { opacity: .34; }

@media (prefers-reduced-motion: reduce) {
  .omark__dot, .omark__line, .omark__body { transition: none; }
}

/* --- narrow screens ------------------------------------------------------
   The section used to be display:none here. It is not any more: the models
   are 581KB now, so every device gets the object. What does not fit on a
   phone is the pinned labels - "Crafts under one roof" beside a thumbnail is
   not a layout - so below OBJ_PIN_WIDTH the script stops projecting them and
   hands the position back to the stylesheet, which lists them underneath. */
@media (max-width: 619px) {
  .object__stage { height: clamp(300px, 50vh, 440px); }
}
.object__marks.is-listed {
  position: static; inset: auto;
  /* the stage is full-bleed, so the list has to bring its own gutters */
  max-width: var(--max); margin: clamp(1.2rem, 3vw, 1.8rem) auto 0;
  padding-inline: var(--gut);
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(.9rem, 3vw, 1.4rem);
}
.object__marks.is-listed .omark { position: static; }
.object__marks.is-listed .omark__dot,
.object__marks.is-listed .omark__line { display: none; }
.object__marks.is-listed .omark__body {
  position: static; translate: none; left: auto;
  display: block; white-space: normal;
  padding-top: .6rem; border-top: 1px solid var(--line);
}
.object__marks.is-listed .omark__body em { display: block; margin-top: .3rem; }
/* the canvas fades in only once there is something modelled to look at */
.object__stage .gl-canvas { opacity: 0; transition: opacity 1s var(--ease); }
.object.gl-on .object__stage .gl-canvas { opacity: 1; }

/* the still holds the section until - and unless - WebGL draws */
.object__fallback {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  max-height: 100%; width: auto;
  opacity: 1; transition: opacity .8s var(--ease);
}
.object.gl-on .object__fallback { opacity: 0; }


/* A 100px-radius pill cannot wrap: on a phone the third option dropped to a
   second line inside the capsule, leaving a lozenge with a ragged hole in it.
   Below the wrap point it stops pretending to be a pill and becomes an honest
   three-up segmented control, each segment stacking its own saving badge. */


/* the recommended tier is lifted and outlined rather than recoloured */


/* reserve two lines so the prices and buttons line up across all three */


/* both actions sit above the list, the way the reference does it */


.pricing__add {
  margin-top: clamp(1.8rem, 3vw, 2.6rem);
  padding-top: clamp(1.2rem, 2vw, 1.6rem);
  border-top: 1px solid var(--line);
  max-width: 60ch; color: var(--on-ink-2); font-size: .92rem;
}
.pricing__add .mono { display: block; color: var(--on-ink-3); margin-bottom: .3rem; }

/* ================================================================== FAQ ==
   Native <details>/<summary>, so keyboard and screen readers are correct by
   construction. JS only adds the one-open-at-a-time courtesy. */
.qa__list { max-width: 62rem; margin-top: clamp(1.4rem, 3vw, 2.4rem); }
.qa { border-top: 1px solid var(--line); }
.qa:last-child { border-bottom: 1px solid var(--line); }
.qa summary {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1.5rem;
  padding: clamp(1rem, 2vw, 1.4rem) 0;
  cursor: pointer; list-style: none;
  font-family: var(--display); font-weight: 400; font-stretch: 86%;
  font-size: clamp(1.05rem, 1.8vw, 1.45rem); letter-spacing: -.01em;
  color: var(--on-deep-1);
}
.qa summary::-webkit-details-marker { display: none; }
.qa summary i {
  flex: none; position: relative; width: 14px; height: 14px; align-self: center;
}
.qa summary i::before, .qa summary i::after {
  content: ''; position: absolute; left: 0; top: 6px; width: 14px; height: 2px;
  /* the section is deep in both themes, so the accent must stay light */
  background: var(--beam); transition: transform .35s var(--ease);
}
.qa summary i::after { transform: rotate(90deg); }
.qa[open] summary i::after { transform: rotate(0deg); }
.qa__a { padding: 0 0 clamp(1rem, 2vw, 1.4rem); max-width: 52ch; }
.qa__a p { color: var(--on-deep-2); line-height: 1.6; }
@media (prefers-reduced-motion: reduce) { .qa summary i::before, .qa summary i::after { transition: none; } }

/* ------------------------------------------------------- theme switch --- */
.tswitch {
  flex: none; display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: 100px;
  color: var(--on-ink-2);
  transition: background-color .25s var(--ease), color .25s var(--ease);
}
.tswitch:hover { background: rgba(255, 255, 255, .14); color: #fff; }
:root[data-theme="light"] .tswitch:hover { background: rgba(8, 8, 138, .09); color: var(--signal); }
.tswitch svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; }
/* the icon shows what you get: sun on the brand ground, moon on the light one */
.tswitch .t-moon { display: none; }
:root[data-theme="light"] .tswitch .t-moon { display: block; }
:root[data-theme="light"] .tswitch .t-sun { display: none; }

/* ================================================== LIGHT-THEME SURFACES ==
   Tokens carry most of the swap. These are the places that hard-coded a dark
   surface because, in the night palette, dark *was* the ground.
   ========================================================================== */
:root[data-theme="light"] {

  /* --- chrome ----------------------------------------------------------- */
  .nav {
    background: rgba(255, 255, 255, .74);
    border-color: rgba(10, 10, 40, .12);
    box-shadow: 0 10px 34px rgba(10, 10, 60, .10);
  }
  .nav__link:hover, .nav__link.is-current { color: var(--signal); }
  .nav__burger span { background: var(--signal); }
  .progress { background: rgba(10, 10, 40, .10); }
  .btn--ghost { border-color: rgba(10, 10, 40, .24); color: var(--on-ink-1); }
  .btn--ghost:hover { background: rgba(8, 8, 138, .07); border-color: var(--signal); }
  .boot__word { color: var(--signal); }
  .boot__mark { background: var(--signal); color: #fff; }
  .boot__name { color: rgba(10, 10, 45, .58); }
  .boot__rule { background: rgba(10, 10, 40, .14); }

  /* --- hero: the wall frosts over instead of blacking out ---------------- */
  .hero { background: #fff; }
  .veil {
    backdrop-filter: saturate(.16) brightness(1.58) blur(3px);
    -webkit-backdrop-filter: saturate(.16) brightness(1.58) blur(3px);
    background: linear-gradient(180deg,
      rgba(255,255,255,.72) 0%, rgba(255,255,255,.46) 26%,
      rgba(255,255,255,.50) 62%, rgba(255,255,255,.90) 100%);
  }
  /* the reading field on the left, plus a white floor so the stats bar and the
     ticker never sit on a bright photograph */
  .hero__scrim {
    background:
      linear-gradient(180deg, transparent 52%, rgba(255,255,255,.74) 76%, rgba(255,255,255,.96) 92%),
      linear-gradient(96deg,
        #fff 0%, #fff 50%,
        rgba(255,255,255,.94) 60%, rgba(255,255,255,.70) 68%,
        rgba(255,255,255,.30) 78%, transparent 90%);
  }
  .hero__glow {
    background: radial-gradient(50% 50% at 50% 50%, rgba(75, 75, 232, .20), transparent 68%);
    mix-blend-mode: multiply;
  }
  .lens-ring { border-color: rgba(8, 8, 138, .38); }
  .cur { background: var(--signal); }
  .cur__label { color: #fff; }
  /* --- panels ------------------------------------------------------------ */
  .receipt { background: #fff; border-color: rgba(10, 10, 40, .12); }
  .logo { background: #fff; box-shadow: 0 2px 10px rgba(10, 10, 60, .06); }
  .fig { border-top-color: rgba(10, 10, 40, .14); }
  .fig:first-child { border-top-color: var(--signal); }
  .reason { box-shadow: 0 18px 46px rgba(10, 10, 60, .08); }
  .reason__art { background: #EEF1FB; border-color: rgba(8, 8, 138, .14); }
  .voice { box-shadow: 0 12px 34px rgba(10, 10, 60, .06); }
  .voice::before { background: radial-gradient(240px circle at var(--gx,50%) var(--gy,50%), rgba(8,8,138,.10), transparent 60%); }
  .chip { border-color: rgba(10, 10, 40, .18); }
  .chip[aria-pressed="true"] { background: var(--signal); border-color: var(--signal); color: #fff; }

  /* --- lightbox ---------------------------------------------------------- */
  .lb { background: rgba(241, 243, 252, .92); }
  .lb__img { box-shadow: 0 40px 90px rgba(10, 10, 60, .22); }
  .lb__btn { border-color: rgba(10, 10, 40, .22); color: var(--on-ink-1); }
  .lb__btn:hover { background: var(--signal); color: #fff; border-color: var(--signal); }
  .lb__bar { border-top-color: rgba(10, 10, 40, .12); }
}

/* ================================================ BRAND-GROUND SURFACES ==
   On the indigo ground the primary action reads best as a white pill, and
   the accent fills need to be the paper, not the brand-on-brand.
   ========================================================================== */
:root:not([data-theme="light"]) {
  .btn--fill { background: #fff; color: #0A0A7A; }
  .btn--fill:hover { background: var(--beam); color: #0A0A7A; }
  /* ...except ON A WHITE CARD, where a white pill is no plate at all. The
     tier cards on plans.html are white glass on bone, and the primary action
     inside one has to be the brand fill. It belongs in THIS block: written
     outside it, at .sec--bone .glass .btn--fill, it still lost to the rule
     above — which is the whole reason the correction lives beside what it
     corrects. */
  .sec--bone .glass .btn--fill, .glass--white .btn--fill { background: var(--signal-ink); color: #fff; }
  .sec--bone .glass .btn--fill:hover, .glass--white .btn--fill:hover { background: var(--signal); color: #fff; }
  /* The wordmark is TYPE now, not a white badge with a span inside it —
     these were painting a filled pill and colouring an element that no
     longer exists, so the name came out white on white. The links lost
     their chips with it; hover is a rule under the word. */
  .nav__mark { color: #fff; }
  .nav__link:hover, .nav__link.is-current { color: #fff; }
  .chip[aria-pressed="true"] { background: #fff; border-color: #fff; color: #0A0A7A; }
  .badge__hub { background: #fff; color: #0A0A7A; }
  .lb__btn:hover { background: #fff; color: #0A0A7A; border-color: #fff; }
  .boot__word { color: #fff; }
  .skip { background: #fff; color: #0A0A7A; }
}

/* --------------------------------------------- deep bands (both themes) --
   A light page still needs punctuation. These three sections stay deep so
   the work, the film and the call-to-action land as events.
   -------------------------------------------------------------------------- */
:root .sec--deep {
  background: var(--deep);
  color: var(--on-deep-1);
}
:root .sec--deep .lead,
:root .sec--deep .sec-head__note { color: var(--on-deep-2); }
:root .sec--deep .footnote { color: var(--on-deep-3); }
:root .sec--deep .eyebrow { color: #9FA8FF; }
:root .sec--deep .fig { border-top-color: var(--deep-line); }
:root .sec--deep .fig:first-child { border-top-color: #9FA8FF; }
:root .sec--deep .fig span,
:root .sec--deep .mono { color: var(--on-deep-3); }
:root .sec--deep .foot p,
:root .sec--deep .foot__legal { color: var(--on-deep-3); }
:root .sec--deep .foot a { color: var(--on-deep-2); }
:root .sec--deep .foot a:hover { color: #fff; }
:root .sec--deep .foot,
:root .sec--deep .stamp { border-top-color: var(--deep-line); }
:root .sec--deep .receipt { background: rgba(255, 255, 255, .06); border-color: var(--deep-line); }
:root .sec--deep .receipt figcaption { color: var(--on-deep-2); }
:root .sec--deep .logo { background: #fff; }
:root .sec--deep .btn--ghost { border-color: rgba(255, 255, 255, .34); color: #fff; }
:root .sec--deep .btn--ghost:hover { background: rgba(255, 255, 255, .14); border-color: #fff; }
:root .sec--deep .badge__ring text { fill: rgba(255, 255, 255, .62); }
:root .sec--deep .badge__hub { background: #fff; color: var(--deep); }

/* --------------------------------------------------- graceful downgrade -- */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .veil { background: rgba(10, 10, 122, .80); }
  .wall { filter: grayscale(.85) brightness(.55); }
  .lens-ring { display: none; }
}
:root[data-theme="light"] .veil { background: rgba(255, 255, 255, .62); }
@media (hover: none) {
  .veil { --lens-r: 0px !important; background: rgba(10, 10, 122, .55); }
  .wall { filter: saturate(.34) brightness(.62); }
  .lens-ring { display: none; }
  :root[data-theme="light"] .veil { background: rgba(255, 255, 255, .58); }
  :root[data-theme="light"] .wall { filter: saturate(.5) brightness(1.2); }
}


/* ------------------------------------------------------ cookie notice -- */
/* Hidden by default and revealed by script, which is also the honest
   behaviour: with scripts off nothing is stored beyond the essentials, so
   there is nothing to consent to and no banner to dismiss. */
.cc {
  position: fixed; z-index: 300;
  left: var(--gut); right: var(--gut); bottom: var(--gut);
  max-width: 680px;
  padding: clamp(1rem, 2.2vw, 1.35rem);
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, .18);
  background: rgba(5, 5, 64, .88);
  backdrop-filter: blur(22px) saturate(1.4);
  -webkit-backdrop-filter: blur(22px) saturate(1.4);
  box-shadow: 0 18px 50px rgba(2, 2, 24, .42);
  opacity: 0; transform: translateY(14px);
  transition: opacity .45s var(--ease), transform .55s var(--ease);
}
.cc.is-in { opacity: 1; transform: none; }
:root[data-theme="light"] .cc {
  background: rgba(255, 255, 255, .93);
  border-color: rgba(10, 10, 40, .14);
  box-shadow: 0 18px 50px rgba(10, 10, 60, .16);
}
.cc__inner { display: grid; gap: clamp(.85rem, 1.8vw, 1.2rem); }
@media (min-width: 720px) { .cc__inner { grid-template-columns: 1fr auto; align-items: center; } }
.cc__title {
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: .95rem; letter-spacing: -.01em; color: var(--on-ink-1);
}
.cc__body { margin-top: .3rem; font-size: .84rem; line-height: 1.55; color: var(--on-ink-2); }
.cc__body a { color: var(--beam); text-decoration: underline; text-underline-offset: 3px; }
:root[data-theme="light"] .cc__body a { color: var(--signal); }
.cc__acts { display: flex; flex-wrap: wrap; gap: .5rem; }
.cc__acts .btn { padding: .6rem 1.1rem; font-size: .82rem; }
@media (max-width: 719px) { .cc__acts .btn { flex: 1 1 auto; justify-content: center; } }

/* ------------------------------------------------- inner-page chrome --- */
/* Every page except the homepage opens on the same masthead: eyebrow, title,
   standfirst. It sits on the ink ground under the fixed nav, so the top
   padding has to clear the nav rather than the viewport. */
.phero { padding: calc(var(--bay) + 3.2rem) var(--gut) var(--bay); position: relative; }
.phero__in { max-width: 62rem; }
/* THE ABOUT PAGE HAS NO CRUMB (2026-09-09, client's word), and every other
   page does. Without something in its place its H1 starts a whole line
   higher than the rest of the site's, which reads as a different template.
   The crumb's own height and the title's top margin, given back as padding
   so the six mastheads all begin on the same line.

   It is also what keeps the title's reveal honest. --p falls as an item
   leaves the TOP of the frame, and a one-line H1 sitting 136px down a 844px
   phone already has its bottom inside that exit band at scroll zero, so it
   could never reach full strength: it peaked at 0.985 and the pages check
   caught it. There is no scroll position that fixes that; only moving the
   line down the page does. */
/* Exactly the crumb's own line box, which is 1.6 x the mono size: measured
   at 16.4px against a 10.24px mono on a 390 frame, and the About title now
   starts within a pixel of every other page's. */
.phero__in--nocrumb { padding-top: calc(var(--t-mono) * 1.6); }
.phero__title {
  font-family: var(--display); font-weight: 800; font-stretch: 82%;
  font-size: clamp(2.4rem, 8.2vw, 5.4rem); line-height: .96;
  letter-spacing: -.035em; text-transform: uppercase; margin-top: .8rem;
}
.phero__lead { margin-top: 1.5rem; max-width: 54ch; }
.crumb { display: flex; flex-wrap: wrap; gap: .45rem; align-items: center; color: var(--on-ink-3); }
.crumb a { color: var(--on-ink-2); }
.crumb a:hover { color: var(--beam); }

/* a plain prose block, used by about and privacy */
.prose { max-width: 68ch; }
.prose h2 {
  font-family: var(--display); font-weight: 700; font-stretch: 86%;
  font-size: clamp(1.3rem, 2.6vw, 1.9rem); line-height: 1.15;
  letter-spacing: -.025em; margin-top: clamp(2.2rem, 4vw, 3rem);
}
.prose h3 {
  font-family: var(--display); font-weight: 600; font-stretch: 88%;
  font-size: clamp(1.02rem, 1.8vw, 1.18rem); letter-spacing: -.015em;
  margin-top: clamp(1.5rem, 2.6vw, 2rem);
}
.prose p, .prose li { font-size: clamp(.92rem, 1.15vw, 1rem); line-height: 1.68; color: var(--on-ink-2); }
.sec--bone .prose p, .sec--bone .prose li { color: var(--on-bone-2); }
.prose p { margin-top: .9rem; }
.prose ul, .prose ol { margin-top: .9rem; padding-left: 1.1rem; display: grid; gap: .5rem; }
.prose a { color: var(--beam); text-decoration: underline; text-underline-offset: 3px; }
:root[data-theme="light"] .prose a, .sec--bone .prose a { color: var(--signal-ink); }
.prose__note { margin-top: 1.6rem; font-size: .8rem; color: var(--on-ink-3); }

/* the value cards on about */
.pillars { display: grid; gap: clamp(.9rem, 1.8vw, 1.3rem); margin-top: clamp(1.8rem, 3.4vw, 2.6rem); }
@media (min-width: 760px) { .pillars { grid-template-columns: repeat(3, 1fr); } }
.pillar {
  padding: clamp(1.2rem, 2.2vw, 1.7rem);
  border: 1px solid var(--line); border-radius: 14px;
  background: rgba(255, 255, 255, .04);
}
:root[data-theme="light"] .pillar { background: #fff; }
.pillar__no { display: block; color: var(--on-ink-3); font-size: .66rem; letter-spacing: .2em; }
.pillar h3 {
  margin-top: .7rem;
  font-family: var(--display); font-weight: 600; font-stretch: 88%;
  font-size: clamp(1.02rem, 1.7vw, 1.2rem); letter-spacing: -.02em;
}
.pillar p { margin-top: .5rem; font-size: .89rem; line-height: 1.6; color: var(--on-ink-2); }
.sec--bone .pillar p { color: var(--on-bone-2); }
/* ON A BONE SECTION THE CARD HAS TO BE DRAWN (2026-09-14). The story block
   moved under the founder and took the ground with it, and both of the
   pillar's surfaces were written for a dark one: a 4%-white fill and a
   16%-white hairline, neither of which exists on bone. The same treatment the
   glass cards already take there — white on the pale band, with the dark
   hairline. `--line` is re-pointed on the grid rather than on .sec--bone, the
   way .svcs and .rooms do it, because the bone class rebinds the text tokens
   and deliberately leaves the rule colour alone. */
.sec--bone .pillars { --line: rgba(10, 10, 40, .13); }
.sec--bone .pillar { background: #fff; }

/* the founder card */
.lede-card {
  display: grid; gap: clamp(1.2rem, 3vw, 2.4rem);
  margin-top: clamp(1.8rem, 3.4vw, 2.6rem);
  padding: clamp(1.3rem, 2.6vw, 2rem);
  border: 1px solid var(--line); border-radius: 16px;
}
@media (min-width: 760px) { .lede-card { grid-template-columns: 200px 1fr; align-items: start; } }
.lede-card__shot { border-radius: 12px; overflow: hidden; aspect-ratio: 1; background: rgba(255, 255, 255, .06); }
.lede-card__shot img { width: 100%; height: 100%; object-fit: cover; }
.lede-card__role { color: var(--on-ink-3); font-size: .68rem; letter-spacing: .18em; }
/* ON A BONE SECTION IT IS THE OTHER SET (2026-09-06). This card carried the
   founder on about.html when that was a .sec--bone, and every one of its
   three text rules asked for the INK tokens: white at 70% on a near-white
   ground, which measured 1.09:1 and read as an empty card with a name
   floating in it. The section is on the ink ground again since the founder
   came up above the story (2026-09-14), so these three are dormant — they are
   kept because .sec--bone now rebinds the ink tokens itself and the pair
   agree, and because the card moves grounds whenever the page is re-ordered. */
.sec--bone .lede-card__role { color: var(--on-bone-3); }
.sec--bone .lede-card p { color: var(--on-bone-2); }
.sec--bone .lede-card__creds span { color: var(--on-bone-2); }
/* the same collision, on the label above every pillar */
.sec--bone .pillar__no { color: var(--on-bone-3); }
.lede-card h3 {
  margin-top: .5rem;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.3rem, 2.4vw, 1.7rem); letter-spacing: -.025em;
}
.lede-card p { margin-top: .8rem; font-size: .92rem; line-height: 1.62; color: var(--on-ink-2); }
.lede-card__creds { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: 1rem; }
.lede-card__creds span {
  padding: .3rem .7rem; border-radius: 100px; border: 1px solid var(--line);
  font-family: var(--mono); font-size: .64rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--on-ink-2);
}

/* the founder's own line, under his card. A quotation mark set large and
   faint in the brand's blue, the way the review cards carry theirs, so the
   two read as the same family; the rule under it is the card's border
   continued rather than a second box. */
.ceoq {
  position: relative; margin: clamp(1.4rem, 2.6vw, 2rem) 0 0;
  padding: clamp(1.2rem, 2.4vw, 1.9rem) clamp(1.3rem, 2.6vw, 2rem);
  border: 1px solid var(--line); border-radius: 16px;
}
.ceoq::before {
  content: '\201C'; position: absolute; top: .1rem; right: 1.2rem;
  font-family: var(--display); font-weight: 700; font-size: 5rem; line-height: 1;
  color: var(--beam); opacity: .12; pointer-events: none;
}
.sec--bone .ceoq::before, .glass--white .ceoq::before { color: var(--signal); }
.ceoq__q {
  margin: 0; max-width: 62ch;
  font-family: var(--display); font-weight: 400; font-stretch: 92%;
  font-size: clamp(1.05rem, 1.7vw, 1.4rem); line-height: 1.42; letter-spacing: -.015em;
}
/* --on-ink-3, not --on-bone-3 (2026-09-14): the card sits on the INK ground
   since the founder came up above the story, and .sec--bone rebinds the ink
   tokens to the bone ones anyway — so the token form is right on either. */
.ceoq__by { margin: 1rem 0 0; font-size: .66rem; letter-spacing: .18em; text-transform: uppercase; color: var(--on-ink-3); }

/* --- services: the nine, one row each ------------------------------------
   They were three cards to a row under three group headings until
   2026-09-07, when the client asked for the grouping to go: a group is a
   claim about which crafts belong together, and the point is that all nine
   come from the same room. So a craft is a full row now — what it is and
   what you get on the left, what it has looked like on the right — and the
   sample work is what breaks the list up instead of a heading.

   `--line` is re-pointed on the list rather than on `.sec--bone`, which does
   not carry one: the bone class rebinds the text tokens but leaves `--line`
   at its dark-ground value, and a white hairline on a pale ground is no
   hairline at all. Bone is pale in both themes, so one dark value is right
   for both. -------------------------------------------------------------- */
.svcs {
  --line: rgba(10, 10, 40, .13);
  display: grid;
  border-top: 1px solid var(--line);
}
.svc {
  display: grid; align-items: start;
  gap: clamp(1.3rem, 3vw, 3rem);
  padding: clamp(1.8rem, 3.4vw, 3rem) 0;
  border-bottom: 1px solid var(--line);
}
@media (min-width: 900px) { .svc { grid-template-columns: minmax(0, .82fr) minmax(0, 1fr); } }

.svc__copy { display: grid; align-content: start; }
.svc__no.chip { justify-self: start; animation: none; }
.svc__name {
  margin-top: .9rem;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.35rem, 2.2vw, 1.85rem); line-height: 1.08; letter-spacing: -.03em;
}
.svc__what { margin-top: .7rem; max-width: 46ch; font-size: .95rem; line-height: 1.6; color: var(--on-ink-2); }
.sec--bone .svc__what { color: var(--on-bone-2); }
.svc__deliv {
  margin-top: clamp(1rem, 1.8vw, 1.35rem); padding-top: 0; padding-left: 0;
  display: grid; gap: .5rem; list-style: none;
}
.svc__deliv li {
  position: relative; padding-left: 1.5rem;
  font-size: .9rem; line-height: 1.55; color: var(--on-ink-2);
}
.sec--bone .svc__deliv li { color: var(--on-bone-2); }
.svc__deliv li::before {
  content: ''; position: absolute; left: 0; top: .6em;
  width: 8px; height: 1.5px; background: var(--beam);
}
:root[data-theme="light"] .svc__deliv li::before, .sec--bone .svc__deliv li::before { background: var(--signal); }

/* --- the sample work ---------------------------------------------------- */
.svc__shots {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: clamp(.45rem, .9vw, .7rem);
}
.svc__shots li {
  margin: 0; overflow: hidden; border-radius: 12px;
  border: 1px solid var(--line); background: rgba(10, 10, 40, .04);
  aspect-ratio: 16 / 11;
}
/* an odd last one takes the full width rather than leaving a hole beside it */
.svc__shots li:last-child:nth-child(odd) { grid-column: 1 / -1; aspect-ratio: 32 / 11; }
.svc__shots img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }

/* A screenshot of a page is read from the top — its cover, its hero — so
   these crop upward instead of at the middle. */
.svc__shots--top img { object-position: 50% 0%; }

/* A dashboard is the evidence, and half a dashboard is not evidence: these
   are shown whole on a white slide, whatever shape they came in. */
/* every one of these is landscape, most of them wide, so the slide is wide
   too — a square box round a 2.5:1 chart is mostly margin */
.svc__shots--panel li { background: #fff; padding: clamp(.35rem, .7vw, .55rem); aspect-ratio: 16 / 9; }
.svc__shots--panel img { object-fit: contain; }
/* THE SAMPLES ARE MIXED SHAPES NOW. The catalogue behind these holds phone
   screenshots, dashboards, 1:1 campaign posts and 9:16 clips, and one crop
   rule cannot serve all of them: a dashboard cropped to a square loses the
   numbers, a campaign post letterboxed on white loses its ground. So a shot
   is CONTAINED on its own dark slide, which shows every piece whole and
   whatever shape it came in. */
.svc__shot { background: rgba(6, 6, 60, .06); }
.sec--bone .svc__shot { background: rgba(6, 6, 60, .05); }
.svc__shot img { object-fit: contain; }

/* THE SEE-MORE IS A BUTTON (2026-09-09, client's word) and it belongs to
   the copy column, under the deliverables, not floating under the samples.
   It carries .btn--ghost, so it only needs its own spacing. */
.svc__more { margin-top: clamp(1.1rem, 2vw, 1.5rem); align-self: start; justify-self: start; width: -moz-fit-content; width: fit-content; }

/* --- a sample that plays -------------------------------------------------
   Video editing and 3D animation show real clips rather than frames grabbed
   from them, at the client's word. They are `preload="none"` with a poster:
   eight clips is sixteen megabytes, and none of it is fetched until someone
   presses play. `controls` rather than autoplay because these are the work
   being shown, not decoration — a reader chooses to watch. */
.svc__shot--vid { background: #05053f; }
.svc__vid { display: block; width: 100%; height: 100%; object-fit: cover; background: #05053f; }
/* a portrait clip in a landscape box would be two black bars and a sliver:
   these sit whole, on the dark ground, the way the panel shots do on white */
.svc__shot--vid { aspect-ratio: 16 / 11; }
.svc__shots .svc__shot--vid video { object-fit: contain; }

/* --- about: the two addresses -------------------------------------------
   Under the numbers on the bone ground: the argument on the left, the two
   rooms as cards on the right. `--line` is re-pointed here for the same
   reason it is on the services list — `.sec--bone` rebinds the text tokens
   but leaves `--line` at its dark-ground white, and a white hairline on
   bone is no hairline at all. ------------------------------------------- */
.rooms {
  --line: rgba(10, 10, 40, .13);
  margin-top: clamp(2.4rem, 4.6vw, 4rem);
  padding-top: clamp(2rem, 3.8vw, 3rem);
  border-top: 1px solid var(--line);
  display: grid; gap: clamp(1.6rem, 3.4vw, 3rem);
}
@media (min-width: 940px) { .rooms { grid-template-columns: minmax(0, .86fr) minmax(0, 1.14fr); align-items: start; } }

.rooms__h {
  margin: .5rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.6rem, 3vw, 2.4rem); line-height: 1.06; letter-spacing: -.03em;
  color: var(--on-ink-1);
}
.rooms__what { margin-top: 1rem; max-width: 42ch; font-size: .95rem; line-height: 1.6; color: var(--on-ink-2); }

.rooms__grid { display: grid; gap: clamp(.8rem, 1.6vw, 1.2rem); }
@media (min-width: 620px) { .rooms__grid { grid-template-columns: repeat(2, 1fr); } }

.room { display: flex; flex-direction: column; padding: clamp(1.15rem, 2vw, 1.6rem); border-radius: 18px; }
.room__k { color: var(--on-ink-3); }
.room__name {
  margin: 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.1rem, 1.7vw, 1.35rem); line-height: 1.15; letter-spacing: -.025em;
}
/* THE COUNTRY BOLD, THE CITY NORMAL (2026-09-09, client's word). The label
   above these cards was a separate mono line saying the country and the
   heading said only the city; the client asked for both in the heading with
   the country carrying the weight. They are two lines, not one run-on: on a
   narrow card "United States Rio Rancho, New Mexico" wrapped wherever it
   happened to run out of room. */
.room__name b { display: block; font-weight: 700; }
.room__name span {
  display: block; margin-top: .18em;
  font-family: var(--body); font-weight: 400; font-stretch: 100%;
  font-size: .92em; letter-spacing: -.01em; color: var(--on-bone-2);
}
.room__addr { margin-top: .75rem; font-style: normal; font-size: .92rem; line-height: 1.55; color: var(--on-ink-2); }
.room__note { margin-top: .9rem; font-size: .86rem; line-height: 1.6; color: var(--on-ink-3); }
/* margin-top auto: the two cards put their contact line on the same baseline
   however long the note above it runs */
.room__reach { margin-top: auto; padding-top: 1.1rem; font-size: .9rem; }
/* --signal-ink, not --signal: these sit on a WHITE card, where the lit blue
   measures 4.03:1 and misses AA. It is the same token .sec--bone .prose a
   already uses for exactly this reason. */
.room__reach a { color: var(--signal-ink); text-decoration: none; border-bottom: 1px solid transparent; }
.room__reach a:hover { border-bottom-color: currentColor; }

/* contact page cards */
.ways { display: grid; gap: clamp(.9rem, 1.8vw, 1.3rem); margin-top: clamp(1.8rem, 3.4vw, 2.6rem); }
@media (min-width: 720px) { .ways { grid-template-columns: repeat(3, 1fr); } }
.way {
  display: block; padding: clamp(1.2rem, 2.4vw, 1.8rem);
  border: 1px solid var(--line); border-radius: 14px;
  transition: border-color .3s var(--ease), background-color .3s var(--ease), transform .4s var(--ease);
}
.way:hover { border-color: var(--beam); background: rgba(255, 255, 255, .05); transform: translateY(-3px); }
.way__tag { color: var(--on-ink-3); font-size: .66rem; letter-spacing: .2em; }
.way__val {
  display: block; margin-top: .7rem;
  font-family: var(--display); font-weight: 600; font-stretch: 88%;
  font-size: clamp(1rem, 1.9vw, 1.22rem); letter-spacing: -.02em;
  color: var(--on-ink-1); overflow-wrap: anywhere;
}
.way__sub { margin-top: .45rem; font-size: .84rem; color: var(--on-ink-3); }


/* --- the hero mark -------------------------------------------------------
   The hero moves off the brand blue and onto the deep ink so the monogram has
   something to be lit against: the object is nearly black and almost all of
   its colour is reflected environment, which simply does not read on a mid
   blue. The rest of the page keeps its blue/bone rhythm — this is the one
   band that goes dark, and a seam at the end of this file carries it into the
   manifesto.

   Every layer the original hero stacked on brand blue has to be re-tinted or
   the ground fights itself: the scrim painted solid #0A0A7A across the
   reading side, and the glow was a bright screen-blended bloom. Both are the
   reason the first pass came out mid-blue despite the section itself already
   being --deep.

   The image wall stays, but pushed a long way back. At full strength it and
   the mark are two busy layers competing for the same space; at this opacity
   it reads as depth behind the object rather than as pictures, and the
   pointer lens still pulls a patch of it back toward full colour. */
.hero--mark { background: transparent; }
/* NOT narrowed to clear the object: .hero__line is white-space:pre inside an
   overflow:hidden box, so shrinking the column silently CUTS the headline
   instead of reflowing it. The type keeps its width; the object is sized and
   the reading wash extended so the two overlap on purpose rather than by
   accident, with the type always on top. */
@media (min-width: 900px) { .hero--mark { --hero-edge: 58%; } }
/* The reference's own headline is modest — the object is the subject and the
   type introduces it. Ours was set for a hero with no object in it, at 8vw
   against trionn's ~3.4vw, which is why the two collided the moment the mark
   moved to centre. Reducing the type is the correct lever and the only safe
   one: .hero__line is white-space:pre in an overflow:hidden box, so narrowing
   the column would have cut the words instead of reflowing them. */
/* All THREE guards retuned together, as .hero__title's own note requires —
   it sizes itself, it does not read --t-mega. Width guard 7vw -> 4.8vw and
   height guard 10.4vh -> 8vh. The third guard is carried over unchanged and
   never binds above 900px — the phone case it exists for is handled by the
   base rule's 9.2vw. Checked 320x700 through 1920x1080. */
@media (min-width: 900px) {
  .hero--mark .hero__title { font-size: min(clamp(2.4rem, 4.8vw, 4.4rem), 8vh, 12vw); }
}

/* When the frame turns portrait the composition STACKS — copy on top, the
   hands below — so the copy belongs at the top of the frame, not centred in
   it. Centred, it sat in the middle third with dead space above it and the
   object crowded into the bottom edge underneath.

   The 4/5 threshold is not a guess: it is exactly the `camera.aspect < 0.8`
   test gl.js uses to switch the object into its stacked framing, so the type
   and the object change layout on the same frame. Two different breakpoints
   here would leave a band of sizes where the copy has moved but the hands
   have not. */
@media (max-aspect-ratio: 4/5) {
  .hero--mark .hero__body {
    /* clears the nav and nothing more */
    padding-top: clamp(4.2rem, 11vh, 6rem);
  }
  /* TOP AND CENTRED on a phone. The headline is centred in the frame on a
     wide screen because the object sits beside it; stacked, the object owns
     the middle and the lower half, so the type belongs at the top of the
     frame rather than floating into it. */
  .hero--mark .hero__title { margin-block: 0 auto; text-align: center; }
  /* AND THE CLAIM STAYS WITH IT (2026-09-15, client's word: the two lines
     under the headline are in the wrong place on a phone). The title took
     `margin-bottom: auto` here and the claim carries one of its own further
     up, so the free space was SPLIT between them: the headline sat at the
     top, the claim floated to the middle of what was left, and the middle of
     a stacked frame is exactly where the hand is. The figure was laid over a
     black glove at 70% white and the link's rule ran across the fingers.

     Killing the title's auto margin hands the whole slack to the claim's,
     which puts the pair back together at the top of the frame — the same
     relationship they have on a desk — in the clear band above the orb. The
     object does not move with them: gl.js places it off the TITLE's bottom
     (restForCopy), not the claim's, and on this frame it is already on its
     0.64 floor. Centred to match the headline above it, since on a phone
     that headline is centred and a flush-left line under it reads as a
     mistake. */
  .hero--mark .hero__title { margin-bottom: 0; }
  .hero--mark .hero__claim {
    justify-content: center;
    text-align: center;
    gap: .35rem .9rem;
  }
}

/* The wall was dimmed twice — its own opacity AND the veil's backdrop
   darkening — and the double tax made it invisible on phones. Both taxes
   are lighter now: the work reads as a real presence behind the mark,
   and the lens still lifts a patch of it further. */
.hero--mark .wall {
  opacity: .34; filter: saturate(.7) brightness(.9);
  transition: opacity .8s var(--ease);
}
.hero--mark.is-lit .wall { opacity: .5; }

.hero--mark .veil {
  backdrop-filter: saturate(.55) brightness(.6) blur(1.5px);
  -webkit-backdrop-filter: saturate(.55) brightness(.6) blur(1.5px);
  background: linear-gradient(180deg,
    rgba(10, 10, 122, .72) 0%, rgba(10, 10, 122, .32) 26%,
    rgba(10, 10, 122, .38) 62%, rgba(10, 10, 122, .9) 100%);
}
.hero--mark .hero__scrim {
  z-index: 2;
  background:
    /* a floor under the stats bar and the ticker */
    linear-gradient(180deg, transparent 58%, rgba(10,10,122,.62) 80%, rgba(10,10,122,.92) 94%),
    /* and a soft wash on the reading side only — it stops well short of the
       centre now, because the centre is where the object lives */
    linear-gradient(96deg,
      rgba(10,10,122,.94) 0%, rgba(10,10,122,.88) 26%,
      rgba(10,10,122,.62) 40%, rgba(10,10,122,.24) 50%, transparent 60%);
}
.hero--mark .hero__glow {
  background: radial-gradient(50% 50% at 50% 50%, rgba(75, 85, 235, .30), transparent 66%);
}

/* The prelude: one deep ground under hero, manifesto and strip, with the
   WebGL stage sticky across all three — the object floats in place while
   the sections scroll past it, exactly the reference's construction. The
   hero gives up its own background and isolation so the stage (z3) can sit
   between the hero's backdrops (wall 0, veil 1, scrim 2) and its text (5)
   inside the ROOT stacking context; an isolated hero would paint atomically
   and the stage could only ever be entirely above or entirely below it. */
/* The passage the object crosses: prelude (hero, manifesto, strip) and the
   room, on ONE sticky stage. They used to be two stages with two cameras,
   and through the strip's wipe both drew an orb at once. */
/* THE GROUND IS ONE SURFACE, and it turns over. --ground is written by
   site.js from the strip's own scrub: 0 while the hand is whole, 1 once it
   has gone to dust. The passage owns the colour for its whole length and the
   sections inside it are transparent, so ink becomes bone as a single move
   rather than as a cut between two sections.

   This replaced the blinds wipe. The bars existed to hand the dark ground
   over to the pale one, and the dust does that now — keeping both would
   have been two transitions competing over the same scroll. */
.passage {
  position: relative;
  --ground: 0;
  --flip: 0;
  /* --ground is the strip's one-way turn from ink to bone, written by
     site.js off the strip's own scrub. A --reground used to ride with it —
     the award took the page back to ink for the finale, written by gl.js —
     and it was a second variable rather than a rewind of the first because
     site.js keeps writing --ground for as long as the passage is on screen.
     The finale is gone and the turn is one way again. */
  background: color-mix(in oklab,
    var(--bone) calc(var(--ground) * 100%), var(--ink));
}
.prelude { position: relative; background: transparent; }
.prelude .hero { isolation: auto; }

/* An absolute rail with a sticky child, NOT a sticky first child with a
   negative bottom margin: a sticky element's travel is bounded by its
   MARGIN box, so margin-bottom:-100svh extended the range by exactly one
   viewport and the object kept floating over the section after the
   prelude instead of leaving with it. The rail takes no flow space, and
   the stage releases exactly at the prelude's bottom edge. */
.hero__rail {
  position: absolute; inset: 0; z-index: 3;
  pointer-events: none;
}
.hero__stage {
  position: sticky; top: 0;
  height: 100svh;
}
/* The prelude's text always reads over the object — but sitting ABOVE it
   in the stack is not enough when the object contains a pure white orb:
   white type over that is invisible. The manifesto carries its own soft
   field, the same trick .hero__scrim plays for the headline column, so
   the copy stays readable wherever the hands happen to have turned to. */
.prelude .manifesto__text { position: relative; z-index: 4; }
.prelude .manifesto__text::before {
  content: ''; position: absolute; inset: -16% -12% -20% -14%; z-index: -1;
  background: radial-gradient(64% 58% at 38% 50%,
    rgba(10, 10, 122, .90), rgba(10, 10, 122, .58) 50%, transparent 74%);
  pointer-events: none;
}
/* And the real fix is editorial, not a scrim: on a wide screen the copy
   takes the LEFT column and the hands own the right, the same split the
   hero already runs. Full-width text ran straight across a lit white orb,
   and no amount of backdrop rescues white type on white. */
@media (min-width: 900px) {
  .prelude .manifesto__text { max-width: 54%; margin-inline: 0; }
}
.strip__pin { z-index: 4; }
.hero__stage .gl-canvas {
  position: absolute; inset: 0; width: 100%; height: 100%;
  /* fast: the assembly starts the moment the preloader clears, and a slow
     canvas fade was hiding the first second of the flight */
  opacity: 0; transition: opacity .45s var(--ease);
}
.gl-on .hero__stage .gl-canvas { opacity: 1; }


/* --- the studio diorama ---------------------------------------------------
   The room takes over where the work carousel ends. It is sticky for its
   track's whole height, so the scroll drives the camera rather than moving
   the canvas — the same sticky-stage construction the prelude uses.

   No fade-in on this canvas: its first frame IS the carousel, blitted onto
   the screen in the room, and the carousel above it fades out instead. A
   fade here would dip to the page ground mid-handoff and give the trick
   away. See studio.js. */
/* The track carries the room's own ground, so if the canvas is ever a frame
   behind the scroll there is no bright band between the carousel and the
   studio — the seam the whole hand-over depends on hiding. */
/* No section padding. The shots are timed to a grid of nine one-screen beats
   starting at the track's top edge; `.sec`'s padding shifted every beat by
   half a screen and lit the wrong copy under the wrong installation. */
.studio-track { position: relative; padding: 0; background: #05060A; }
/* FIXED, not sticky. Sticky only pins once the element's own top reaches the
   viewport top, so the first screen of the track renders the room half below
   the fold with the previous section still showing above it — the reveal's
   opening frame arrives cropped, which is the one frame that must be perfect.
   Fixed covers the viewport from the moment the track is live; `is-live`
   fades it in and out so it never sits over the sections either side. */
.studio-gl {
  /* ABOVE the passage's rail (z-index 3), not below it. Underneath, the
     carousel paints straight over the room and only the JS opacity fade hides
     it — so any frame where that fade has not landed shows both at once, the
     work orbiting inside the studio. Above it, the room's own fade-in IS the
     crossfade, and the carousel fade below is only cleanup. */
  position: fixed; inset: 0; z-index: 4;
  opacity: 0; visibility: hidden;
  transition: opacity .18s linear, visibility 0s linear .18s;
  pointer-events: none;
}
.studio-track.is-live .studio-gl {
  opacity: 1; visibility: visible;
  transition: opacity .18s linear, visibility 0s;
}
.studio-gl__canvas {
  display: block; width: 100%; height: 100%;
}
/* The copy rides over the room, one screen per station, and only the beat
   the camera is on is lit — the rest hold at a whisper so the reader always
   knows which craft the room is showing them. */
.studio-beat {
  /* above the stage, which sits at 4. `isolation` forces its own stacking
     context: without one the beat's z-index alone let the fixed canvas
     composite over the type, and the copy rendered at about 40% brightness
     while every computed style still said opacity 1. */
  position: relative; z-index: 6; isolation: isolate;
  min-height: 100svh; display: flex; align-items: center;
  padding-inline: clamp(1.4rem, 7vw, 7rem);
  pointer-events: none;
}
/* The copy sits at the frame's edges, which is mostly the brand-blue ground
   OUTSIDE the room — so it stays WHITE, the same as every other deep section
   on the page. What it needs is protection for the stretches where it crosses
   the room itself, which is pale.

   A halo panel behind the text was the wrong tool: at these sizes it reads as
   a white blob sitting on the floor rather than as type on a page. A tight
   text-shadow hugs the glyphs instead — invisible over the blue, just enough
   of an edge to hold the letters together over the pale room. */
.studio-beat { color: var(--bone, #F4F1EA); }
.studio-beat > * { max-width: min(31rem, 44vw); pointer-events: auto; position: relative; }

/* ONE scrim, fixed to the viewport — not one per beat.

   Per-beat scrims tile vertically: each paints only its own box, so where two
   beats meet their gradients butt up and the join draws a hard horizontal line
   straight across the room, which slides with the scroll. Worse when adjacent
   beats sit on opposite sides, because then the two columns cross at the seam.

   A single fixed element has no seam to show. It lives inside the stage (so it
   fades in and out with it) and carries both columns, crossfading between them
   as the copy moves from one side to the other. */
.studio-beat h2,
.studio-beat h3,
.studio-beat p { text-shadow: 0 1px 2px rgba(6, 6, 74, .45); }

.studio-scrim { position: absolute; inset: 0; pointer-events: none; }
.studio-scrim i {
  position: absolute; inset: 0; display: block;
  opacity: 0; transition: opacity .5s var(--ease);
}
.studio-scrim .l {
  background: linear-gradient(90deg,
    rgba(6, 6, 74, .95) 0%, rgba(6, 6, 74, .88) 22%,
    rgba(6, 6, 74, .52) 48%, rgba(6, 6, 74, 0) 70%);
}
.studio-scrim .r {
  background: linear-gradient(270deg,
    rgba(6, 6, 74, .95) 0%, rgba(6, 6, 74, .88) 22%,
    rgba(6, 6, 74, .52) 48%, rgba(6, 6, 74, 0) 70%);
}
.studio-scrim.on-left .l,
.studio-scrim.on-right .r { opacity: 1; }

/* On a phone the copy cannot sit in a side column — a 62% scrim over a 390px
   screen is the whole screen, and 22ch of type has nowhere to go. The copy
   drops to the bottom instead and the scrim becomes one upward gradient, so
   the room keeps the top two thirds and the words sit under it. Both columns
   are lit so the side the beat happens to be on stops mattering. */
@media (max-width: 760px) {
  .studio-scrim .l,
  .studio-scrim .r { background: none; }
  .studio-scrim.on-left::after,
  .studio-scrim.on-right::after {
    content: ''; position: absolute; inset: auto 0 0 0; height: 62%;
    background: linear-gradient(0deg,
      rgba(6, 6, 74, .96) 0%, rgba(6, 6, 74, .88) 26%,
      rgba(6, 6, 74, .5) 58%, rgba(6, 6, 74, 0) 100%);
  }
  .studio-beat,
  .studio-beat--right {
    align-items: flex-end; justify-content: flex-start; text-align: left;
    padding-bottom: clamp(2.4rem, 12vh, 6rem);
  }
  .studio-beat > * { max-width: 30ch; }
}
.studio-beat--right { justify-content: flex-end; text-align: right; }
.studio-beat--hush > * { opacity: 0; }
.studio-beat > * {
  opacity: .26;
  transform: translateY(10px);
  transition: opacity .5s var(--ease), transform .5s var(--ease);
}
.studio-beat.is-on > * { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .studio-beat > * { opacity: 1; transform: none; transition: none; }
}

/* No WebGL: the beats have nothing behind them, so they stop being a camera
   move and become an ordinary list of the nine crafts. The reveal beats carry
   no copy at all and simply go. */
.studio-track.no-gl { padding-block: var(--bay); }
.studio-track.no-gl .studio-beat--hush { display: none; }
.studio-track.no-gl .studio-beat {
  min-height: 0; padding-block: clamp(1.4rem, 4vh, 2.6rem);
  align-items: flex-start; justify-content: flex-start; text-align: left;
}
.studio-track.no-gl .studio-beat > * { opacity: 1; transform: none; max-width: 46ch; }
.studio-track.no-gl .studio-beat::before { display: none; }


/* --- statement strip ------------------------------------------------------
   Trionn's signature carried over: three verbs the width of the screen,
   moved by the scroll position itself (site.js writes the transform), then
   a blinds wipe that closes the strip and hands straight to the room.

   The wipe finishes exactly as the pin releases. It used to close two
   thirds of the way through and leave a screen of empty ground to scroll
   past, because the figures it once uncovered had moved to the room; now
   what it uncovers is the room's own first frame, glass already in it.
   The section is
   taller than the viewport and the inner frame is sticky, so the scrub has
   somewhere to happen; without JS or with reduced motion it collapses to a
   plain, static band and the blinds never exist. */
.strip { padding: 0; }
/* 280vh, up from 200. The strip used to hold two things — the verbs
   crossing and the ground turning over — and both were done by its half way
   mark. It holds a THIRD now: once the ground has settled the orb squares
   off into a cube, and that has to follow the turn rather than share it. At
   200 the two ran together and neither read. */
.js .strip { height: 280vh; }
@media (max-aspect-ratio: 4/5) { .js .strip { height: 210vh; } }
.strip__pin {
  display: flex; align-items: center;
  padding: var(--bay) 0;
  overflow: hidden;
}
.js .strip__pin { position: sticky; top: 0; height: 100svh; padding: 0; }
/* THE VERBS DO NOT LEAVE. They used to fade out on --verbs, handing over to
   a ring of the same words wrapped around the orb — and that ring is gone,
   so what the fade handed to was nothing: three words vanishing a quarter of
   the way through the ground's turn, while the turn they were supposed to be
   part of carried on without them.

   They cross the turnover instead. The ground goes ink to bone underneath
   them and the words go bone to BLUE on --flip, so STRATEGY EXECUTE SCALE is
   the one thing on the page that is the same thing before and after — which
   is what makes the turn read as one surface rather than as a cut. */
.strip__track {
  display: flex; align-items: baseline; gap: clamp(1.6rem, 4vw, 3.6rem);
  padding-left: var(--gut);
  white-space: nowrap; will-change: transform;
  font-family: var(--display); font-weight: 700; font-stretch: 82%;
  font-size: clamp(4.2rem, 14vw, 13rem); line-height: 1; letter-spacing: -.035em;
  text-transform: uppercase;
  /* the verbs invert as the ground does — bone on ink at the start, BRAND
     BLUE on bone by the end. Left at bone they disappear into the turnover
     exactly when they are still crossing the frame; landed on --on-bone-1,
     the near-black the body copy uses, they read as a different page's type.

     --signal-ink, NOT --ink. On this theme --ink is the deep blue and would
     have been right, but the light theme redefines --ink to #FFFFFF — so
     the verbs would have finished the turn as white type on the pale ground
     they were turning onto, which is to say gone. --signal-ink is the brand
     blue held dark enough to sit on bone in BOTH themes; the light theme's
     own token block says as much where it sets it. */
  color: color-mix(in oklab, var(--signal-ink) calc(var(--flip, 0) * 100%), var(--bone));
}
.strip__track i { font-style: normal; font-weight: 300; font-stretch: 100%; font-size: .42em; color: var(--signal); transform: translateY(-.6em); }

/* The ghost rows: the verbs again, twice, as outlines, one above and one
   below the words in front, behind the object. An absolute layer with a
   sticky stage — the hero rail's construction — at z-index 2, under the
   rail's 3 and the pin's 4. The stroke turns with the ground like the words
   do, and like them it stays: an outline that leaves while the solid words
   it doubles are still there reads as a dropped layer, not as a fade. */
.strip__ghosts { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
.strip__ghostStage { position: sticky; top: 0; height: 100svh; overflow: hidden; }
.strip__ghost {
  position: absolute; left: 0;
  display: flex; align-items: baseline; gap: clamp(1.2rem, 3vw, 2.6rem);
  padding-left: var(--gut);
  white-space: nowrap; will-change: transform;
  font-family: var(--display); font-weight: 700; font-stretch: 82%;
  font-size: clamp(2.6rem, 8.5vw, 8rem); line-height: 1; letter-spacing: -.035em;
  text-transform: uppercase;
  color: transparent;
  -webkit-text-stroke: 1.5px color-mix(in oklab, rgba(10, 10, 122, .26) calc(var(--flip, 0) * 100%), rgba(238, 240, 255, .34));
  opacity: .95;
}
.strip__ghost i { font-style: normal; font-weight: 300; font-stretch: 100%; font-size: .42em; -webkit-text-stroke: 0; color: color-mix(in oklab, var(--signal) 55%, transparent); transform: translateY(-.6em); }
.strip__ghost--a { top: 9svh; }
.strip__ghost--b { bottom: 9svh; }
html:not(.js) .strip__ghosts { display: none; }
@media (prefers-reduced-motion: reduce) { .strip__ghosts { display: none; } }
@media (max-aspect-ratio: 4/5) { .strip__ghost { font-size: clamp(2.4rem, 11vw, 5rem); } }

@media (max-aspect-ratio: 4/5) {
  /* 20vw, not the 14 a wide frame gets: at this width the verbs ARE the
     screen, and the smaller step left a thin band of type adrift in a lot
     of ground. */
  .strip__track { font-size: clamp(5rem, 20vw, 9rem); }
}

@media (prefers-reduced-motion: reduce) {
  .js .strip { height: auto; }
  .js .strip__pin { position: static; height: auto; padding: var(--bay) 0; }
  .strip__track { transform: none !important; }
}


/* --- staged reveals -------------------------------------------------------
   One mechanic for both new sections: site.js writes each item's own --p
   (0..1) from its position in the viewport, and everything here is a
   function of that. Scroll-position driven rather than IntersectionObserver
   toggled, so scrolling back UNPLAYS the reveal instead of leaving it stuck
   on — the same rule the statement strip and the tally already follow.

   The no-JS and reduced-motion floor is --p: 1, declared on the item itself,
   so the content is simply present when nothing is driving it. */
[data-stage-item] { --p: 1; }
.js [data-stage] [data-stage-item] { --p: 0; }

/* --- selected work ---- */
.cases__list {
  list-style: none; margin: clamp(1.8rem, 3.4vw, 2.8rem) 0 0; padding: 0;
  display: grid; gap: clamp(2.4rem, 6vw, 5rem);
}
.case {
  display: grid; gap: clamp(1.1rem, 2.6vw, 2.4rem); align-items: center;
  opacity: calc(.2 + var(--p) * .8);
  transform: translateY(calc((1 - var(--p)) * 46px));
}
@media (min-width: 860px) {
  .case { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
  /* alternate the side so the eye zig-zags down the section */
  .case:nth-child(even) .case__shot { order: 2; }
}
.case__shot {
  margin: 0; overflow: hidden; border-radius: 16px;
  border: 1px solid var(--line); background: var(--ink-2);
  /* The frame sets the height, not the file. These are 1080x1350 portraits;
     at full column width that is a ~940px card, taller than the viewport,
     which pushed the meta column off screen entirely. */
  aspect-ratio: 4 / 3;
  /* the frame opens as the card arrives */
  clip-path: inset(calc((1 - var(--p)) * 12%) 0 calc((1 - var(--p)) * 12%) 0 round 16px);
}
.case__shot img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  /* a slow counter-drift inside the clipped frame; the resting scale is the
     headroom that keeps the shift from uncovering an edge */
  transform: translateY(calc((var(--p) - .5) * -26px)) scale(1.07);
}
.case__no { display: block; color: var(--on-ink-3); font-size: .68rem; letter-spacing: .2em; }
.case__title {
  margin-top: .7rem;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.6rem, 3.4vw, 2.4rem); line-height: 1.04;
  letter-spacing: -.03em; color: var(--on-ink-1);
}
.case__craft { margin-top: .5rem; font-size: .64rem; letter-spacing: .16em; text-transform: uppercase; color: var(--signal-2); }
.case__body { margin-top: .9rem; max-width: 40ch; font-size: clamp(.9rem, 1.15vw, .98rem); line-height: 1.62; color: var(--on-ink-2); }

@media (prefers-reduced-motion: reduce) {
  .js [data-stage] [data-stage-item] { --p: 1; }
  .case { opacity: 1; transform: none; }
  .case__shot { clip-path: none; }
  .case__shot img { transform: none; }
}

/* ============================================================ the objects ==
   Nymera's layout, in this page's own rhythm of indigo and bone: glass on the
   dark sections and white cards on the pale ones, one accent phrase per
   headline, numbered chips that float, reveals that resolve out of a blur,
   panels that lean toward the pointer. No gradients, no atmosphere: the
   grounds alternate as they did.
   ========================================================================== */
.glass {
  position: relative; border-radius: 20px;
  background: color-mix(in oklab, var(--ink-2) 62%, transparent);
  border: 1px solid color-mix(in oklab, var(--beam) 30%, transparent);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10), 0 24px 60px -30px rgba(3, 3, 40, .7);
  transform: perspective(900px) rotateX(var(--ty, 0deg)) rotateY(var(--tx, 0deg)) translateY(var(--lift, 0px));
  transition: transform .55s var(--ease), box-shadow .55s var(--ease);
}
.glass[data-tilt]:hover { --lift: -4px; }
.glass--white, .sec--bone .glass {
  background: #fff; color: var(--on-bone-1);
  border: 1px solid rgba(7, 11, 24, .10);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  box-shadow: 0 18px 48px rgba(7, 11, 24, .06);
}
.sec--bone .glass[data-tilt]:hover, .glass--white[data-tilt]:hover { box-shadow: 0 26px 60px rgba(7, 11, 24, .10); }
:root[data-theme="light"] .glass { background: rgba(255, 255, 255, .8); border-color: rgba(10, 10, 40, .12); box-shadow: 0 18px 48px rgba(7, 11, 24, .06); }

/* chips: a number as an object */
.chip {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .38rem .55rem; border-radius: 100px;
  background: color-mix(in oklab, var(--ink-2) 80%, transparent);
  border: 1px solid color-mix(in oklab, var(--beam) 32%, transparent);
  color: var(--beam); font-family: var(--mono); font-size: .62rem; letter-spacing: .14em; font-weight: 500;
  animation: chipFloat 6s ease-in-out infinite; animation-delay: calc(var(--i, 0) * -.85s);
}
.chip b { font-weight: 500; }
.chip--white, .sec--bone .chip { background: var(--bone); border-color: rgba(7, 11, 24, .10); color: var(--signal-ink); }
@keyframes chipFloat { 50% { transform: translateY(-4px); } }

/* the accent phrase: one per headline, the claim in the sentence */
.lit { font-style: normal; color: var(--beam); }
.sec--bone .lit, .glass--white .lit { color: var(--signal); }

@media (prefers-reduced-motion: reduce) {
  .chip { animation: none !important; }
  .glass { transform: none; transition: none; }
}
@media (hover: none) { .glass { transform: none; } }

/* --- the endgame: the close on the queen's stage ---------------------------
   The act's third beat: the section carries the passage's stage on, so the
   queen stays in the frame, and the copy and the call sit up the left on a
   rail above everything, on --pe. pointer-events: auto on the copy, because
   its rail lets pointers through; width 100% and no max-width of its own,
   because a narrower .wrap is centred and sat over the queen. */
.endgame { position: relative; padding: 0; }
/* Two screens: one of hold. It was 3.2, and on a phone that was 2,700px of
   scroll for a frame that does not change once the copy is up. */
.js .endgame { height: 200vh; }
@media (max-aspect-ratio: 4/5) { .js .endgame { height: 140vh; } }
.endgame__rail { position: absolute; inset: 0; z-index: 6; pointer-events: none; }

/* the copy sits up the frame, clear of the ribbons that cross its lower
   half: centred, the call landed on the work cards' left end */
/* CENTRED ON THE NAME (2026-09-06). The object here is the studio's own
   wordmark and it fills the middle of the frame, so the copy sits centred
   on top of it rather than beside it. It sat left, clear of a standing
   queen, until the queen went. */
/* --c lives on the STAGE, not on the copy. In as the beat arrives, OUT
   before the stage lets go, so the frame is gone by the time it hands over
   rather than scrolling away in it - and on the stage because the marks
   arcing either side have to leave on the same breath as the words. On the
   copy it was invisible to them: a custom property inherits down, and they
   are its siblings, so they read the fallback and never faded at all. */
.endgame__stage {
  --c: min(clamp(0, calc((var(--pe, 0) - 0.06) / 0.18), 1),
           clamp(0, calc((0.96 - var(--pe, 0)) / 0.06), 1));
  position: sticky; top: 0; height: 100svh; display: grid; align-items: center; justify-items: center; padding: 8svh var(--gut);
}
.endgame__copy {
  width: 100%; max-width: 34ch; text-align: center;
  color: var(--on-bone-1); pointer-events: auto;
  opacity: var(--c);
  filter: blur(calc((1 - var(--c)) * 8px));
}
.endgame .eyebrow { color: var(--signal); }
.endgame .lit { color: var(--signal); }
.endgame__line {
  margin: .55rem auto 0; max-width: 14ch;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(2rem, 5.2vw, 5rem); line-height: 1; letter-spacing: -.035em;
}
.endgame__note { margin: 1.2rem auto 0; max-width: 32ch; font-size: clamp(1rem, 1.35vw, 1.3rem); line-height: 1.45; opacity: .74; }
.endgame__acts { display: flex; flex-direction: column; align-items: center; gap: .9rem; margin-top: clamp(1.4rem, 2.6vw, 2rem); }
@media (max-aspect-ratio: 4/5) {
  /* the copy higher, so the call and the link clear the queen's crown */
  .endgame__stage { padding-top: 9svh; }
  .endgame__line { max-width: 11ch; font-size: clamp(2rem, 4.6vw, 4.2rem); }
}
@media (prefers-reduced-motion: reduce), (max-width: 0px) {
  .js .endgame { height: auto; }
  .endgame__rail { position: static; }
  .endgame__stage { position: static; height: auto; padding: var(--bay) var(--gut); }
  .endgame__copy { opacity: 1; transform: none; filter: none; }
}
html.no-gl .js .endgame, .no-gl .endgame { height: auto; }
.no-gl .endgame__rail { position: static; }
.no-gl .endgame__stage { position: static; height: auto; padding: var(--bay) var(--gut); }
.no-gl .endgame__copy { opacity: 1; transform: none; filter: none; }
/* --- the work, seen through the glass -----------------------------------
   The last beat of the passage. gl.js owns the picture — the ball at the
   left edge and the three bowed columns of work around it — so what is
   here is the frame around it: the copy at the top left, the panel on the
   right that names whatever the pointer is on, and the plain grid that
   stands in for all of it where there is no WebGL to draw it.

   The stage is sticky inside a tall section, the same construction the pawn
   act and the endgame use. Its children sit on --wp, the section's own
   scrub, written by site.js. */
.works { position: relative; padding: 0; }
.js .works { height: 240vh; }
@media (max-aspect-ratio: 4/5) { .js .works { height: 155vh; } }
.works__rail { position: absolute; inset: 0; z-index: 6; pointer-events: none; }

/* THE GHOST TITLE, BEHIND THE CANVAS. z-index 2 puts it under .hero__rail,
   which is 3 and carries the passage's canvas — so the pictures and the ball
   draw over these words. That is the whole point of it and it is the only
   reason this is a second rail rather than a rule on .works__copy: one
   element cannot be both over the canvas (where the real heading has to be,
   to be read) and under it. */
.works__back { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
.works__backStage {
  position: sticky; top: 0; height: 100svh;
  display: grid; place-items: center; padding: 0 var(--gut);
}
.works__ghost {
  --c: min(clamp(0, calc((var(--wp, 0) - 0.04) / 0.16), 1),
           clamp(0, calc((0.96 - var(--wp, 0)) / 0.08), 1));
  margin: 0; max-width: 11ch; text-align: center; text-wrap: balance;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(2.2rem, 6.6vw, 6rem); line-height: .94; letter-spacing: -.04em;
  /* Barely there. It is a ground for the work to pass over, not a line to
     read — the readable one is .works__title above the canvas. */
  color: color-mix(in srgb, var(--on-bone-1) 12%, transparent);
  opacity: var(--c);
}

/* THE HELD PIECE'S CAPTION. Centred under the picture the scene holds up,
   and only while it is held. Pointer events on, because the hint is the only
   thing telling a reader how to put it back. */
.works__held {
  position: absolute; z-index: 3;
  left: 50%; bottom: 8svh; transform: translateX(-50%);
  width: min(46ch, calc(100% - var(--gut) * 2));
  text-align: center; color: var(--on-bone-1);
  display: grid; gap: .35rem;
  /* A scrim, because the caption is set over whatever the corridor happens
     to be carrying past at that moment — and what it is carrying is full
     colour photography. Text alone survives an empty ground and nothing
     else; this keeps it legible without putting a card on the page. */
  padding: .7rem 1.1rem; border-radius: 14px;
  background: color-mix(in srgb, var(--bone) 82%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  animation: worksHeldIn .34s cubic-bezier(.2, .7, .3, 1) both;
}
.works__held[hidden] { display: none; }
.works__heldCraft {
  font-size: .58rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--signal); font-weight: 600;
}
.works__heldDesc { font-size: clamp(.95rem, 1.5vw, 1.15rem); line-height: 1.4; }
.works__heldHint { font-size: .62rem; letter-spacing: .14em; color: var(--on-bone-3); }
@keyframes worksHeldIn {
  from { opacity: 0; transform: translate(-50%, 12px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
@media (prefers-reduced-motion: reduce) { .works__held { animation: none; } }
.works__stage {
  position: sticky; top: 0; height: 100svh;
  padding: 11svh var(--gut) 0;
  color: var(--on-bone-1);
}
.works__copy {
  --c: min(clamp(0, calc((var(--wp, 0) - 0.06) / 0.18), 1),
           clamp(0, calc((0.96 - var(--wp, 0)) / 0.06), 1));
  max-width: 22ch;
  opacity: var(--c);
  filter: blur(calc((1 - var(--c)) * 8px));
}
.works .eyebrow { color: var(--signal); }
.works .lit { color: var(--signal); }
.works__title {
  margin: .55rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.7rem, 3.1vw, 3rem); line-height: 1; letter-spacing: -.035em;
}
/* THE PANEL MOVES TO THE LEFT (2026-09-09), under the copy. It sat on the
   right, in the middle of the frame, because the arc it belonged to was a
   band down the centre with clear ground either side. The tube that replaced
   the arc is a cylinder right of centre and its wall reaches most of the way
   to the right edge, so the type has one clear column and both blocks are in
   it. Set straight on the page; the white card it sat on went in 2026-09-05.

   Note the left inset is the STAGE's padding, not var(--gut): this is
   absolute inside .works__stage, which already carries the gutter, so a
   second one would push the panel out of line with the copy above it. */
.works__info {
  --c: min(clamp(0, calc((var(--wp, 0) - 0.14) / 0.18), 1),
           clamp(0, calc((0.96 - var(--wp, 0)) / 0.06), 1));
  position: absolute; z-index: 2;
  left: var(--gut); top: 52%;
  width: clamp(230px, 22vw, 320px);
  opacity: var(--c);
  filter: blur(calc((1 - var(--c)) * 8px));
  pointer-events: auto;
}
/* The counter, the craft name and the footnote are gone (2026-09-09) and so
   is the min-height that reserved room for a line that CHANGED under the
   pointer. One sentence, at a size that reads next to the carousel. */
.works__desc { margin: 0; font-size: 1rem; line-height: 1.45; }
.works__more {
  display: inline-block; margin-top: .9rem;
  color: var(--on-bone-1); font-size: .85rem; text-decoration: none;
  border-bottom: 1px solid rgba(10, 10, 45, .26); padding-bottom: .1em;
}
.works__more:hover { color: var(--signal); border-bottom-color: var(--signal); }
/* Portrait: the tube takes the middle of the frame and the copy sits above
   it, so the panel goes to the FOOT — there is no beside on a phone. */
@media (max-aspect-ratio: 4/5) {
  .works__stage { padding-top: 9svh; }
  .works__copy { max-width: 12ch; }
  .works__info {
    right: var(--gut); left: var(--gut); top: auto;
    bottom: max(4.6rem, calc(env(safe-area-inset-bottom, 0px) + 4.4rem));
    width: auto; transform: none;
  }
}
/* the grid stands in for the carousel wherever it cannot run */
.works__grid { display: none; list-style: none; margin: 0; padding: var(--bay) var(--gut); gap: clamp(.8rem, 1.6vw, 1.4rem); }
.works__cell { display: grid; gap: .5rem; }
.works__cell img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 12px; display: block; background: rgba(10, 10, 45, .06); }
.works__cap { margin: 0; font-size: .78rem; line-height: 1.35; color: var(--on-bone-2); }
.works__cap b { display: block; font-size: .58rem; letter-spacing: .18em; text-transform: uppercase; color: var(--on-bone-3); font-weight: 600; }
@media (prefers-reduced-motion: reduce), (max-width: 0px) {
  .js .works { height: auto; }
  .works__rail { position: static; }
  .works__stage { position: static; height: auto; padding: var(--bay) var(--gut) 0; }
  .works__copy, .works__info { opacity: 1; transform: none; filter: none; }
  .works__info { position: static; width: auto; margin-top: 1.4rem; }
  .works__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
}
.no-gl .js .works, .no-gl .works { height: auto; }
.no-gl .works__rail { position: static; }
.no-gl .works__stage { position: static; height: auto; padding: var(--bay) var(--gut) 0; }
.no-gl .works__copy, .no-gl .works__info { opacity: 1; transform: none; filter: none; }
.no-gl .works__info { position: static; width: auto; margin-top: 1.4rem; }
.no-gl .works__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }

/* --- the scroll mark -----------------------------------------------------
   ONE of these, fixed to the frame, on every page. It was the hero's and it
   left the screen with the hero; the pages are long and the acts inside the
   passage hold the frame for screens at a time, so the cue belongs to the
   whole page rather than to any section of it. Three of them used to sit on
   the held stages instead, which meant a reader saw nothing at all between
   the acts.

   It carries NO plate. The page runs indigo, bone and deep under it, so
   site.js reads what is actually behind the mark and flips its colour — the
   same reasoning as the floating nav, which paints its own controls rather
   than assume a ground. Taken away when the close section arrives: there is
   nothing below it to scroll to. */
.scrollmark {
  display: none; position: fixed; z-index: 40; margin: 0;
  left: 50%; transform: translateX(-50%);
  bottom: max(1.4rem, calc(env(safe-area-inset-bottom, 0px) + 1.2rem));
  align-items: center; gap: .7rem;
  font-size: .68rem; letter-spacing: .14em; text-transform: uppercase;
  white-space: nowrap; pointer-events: none;
  color: var(--on-ink-3);
  opacity: 0; transition: opacity .4s var(--ease), color .4s var(--ease);
}
.js .scrollmark { display: inline-flex; }
.scrollmark.is-on { opacity: 1; }
/* what is behind it, read per frame by site.js */
.scrollmark.is-light { color: var(--on-bone-3); }
.scrollmark__line {
  display: block; width: 1px; height: 26px; position: relative; overflow: hidden;
  background: currentColor; opacity: .34;
}
.scrollmark__line::after {
  content: ''; position: absolute; inset: 0;
  background: currentColor; opacity: 1;
  animation: heroScroll 2.4s var(--ease-io) infinite;
}
@media (prefers-reduced-motion: reduce), (max-width: 0px) { .scrollmark, .js .scrollmark { display: none; } }

/* the honeypot: out of sight, out of the tree, out of the tab order */
.enquire__trap {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* --- the rise: the act after the strip ---------------------------------
   The section gives the passage its length; the copy pins inside it on a
   rail of its own, above the object's stage. --pq is written by site.js
   off the section's scrub: the line arrives once the orb has started
   moving out of its way and stays until the stage lets go.

   TWO BEATS BECAME ONE (2026-09-09). The chess pieces are gone, so is the
   opening line, and 300vh of rail for one line is dead scroll: 180 here
   and 130 in portrait is the same act at the length it now needs. */
.pawn { position: relative; padding: 0; }
/* LONGER FOR THE PLANE (2026-09-11). 180vh is 80vh of actual scrub once the
   sticky stage has taken its own screen out of it, and the act that ran on
   it was one beat: the ball moving aside. The paper plane's flight is five —
   in from the corner, settle under the line, fall away to the ball, TWO
   turns around it, out through the top-right — and at 80vh each of them
   lasted a sixth of a screen, which reads as a flick rather than a flight.
   The two turns are the expensive part: they own four tenths of the scrub
   on their own, and a revolution that takes less than half a screen is a
   spin. 330vh gives the scrub 230vh, which is about 45 a beat with the
   turns taking two screens between them.

   AND THEN IT WAS TOO LONG (2026-09-15, client's word: the flag and plane
   sections take too much scrolling). Five screens of scroll for one act,
   with the reach after it, is a long time to hold a reader on one object —
   and the number above was argued from the FLIGHT's needs alone, which is
   only half of what a section length is for. 250vh is 150vh of scrub: the
   two turns still own half of it, 37vh a revolution, which is short of the
   old 46 and still reads as going round rather than spinning. The beats are
   fractions of the scrub, so nothing is retimed against anything else —
   they simply arrive closer together, and what goes is the waiting. */
.js .pawn { height: 250vh; }
/* SHORTER IN PORTRAIT (2026-09-06). Every act in the passage is a sticky
   stage with a scrub, and a scrub measured in viewport heights is the same
   number of PIXELS on a phone as on a desk — but a thumb covers far fewer
   of them per gesture, so the same act is several more swipes. The beats are
   fractions of the section, so they simply arrive closer together; what goes
   is the dead scroll between them. */
@media (max-aspect-ratio: 4/5) { .js .pawn { height: 195vh; } }
.pawn__rail { position: absolute; inset: 0; z-index: 6; pointer-events: none; }
.pawn__stage { position: sticky; top: 0; height: 100svh; display: grid; align-items: center; padding: 0 var(--gut); }
/* The copy sits left of centre rather than on the gutter, so it and the
   piece — at a fifth of the half-width right of centre, see gl.js — read
   as one composition instead of two things at opposite edges. */
.pawn__copy { padding-left: clamp(0px, 9vw, 210px); }
/* The line reads the act's number through --c. */
/* AND THEY EASE (2026-09-06). --c is a fraction of the scroll, so a flick
   that covers a third of the act in one frame took the copy from nothing to
   full in that one frame. The opacity and the defocus carry a short
   transition, which costs nothing when the reader is scrolling steadily and
   turns a flick into a fade. The custom property itself cannot be
   transitioned — it is unregistered — but what reads it can. */
.pawn__copy, .speck__copy, .endgame__copy, .works__copy, .works__info, .cue {
  transition: opacity .28s linear, filter .28s linear;
}
@media (prefers-reduced-motion: reduce) {
  .pawn__copy, .speck__copy, .endgame__copy, .works__copy, .works__info, .cue { transition: none; }
}

/* EVERY LINE ON A HELD STAGE FADES, and only fades (2026-09-05). These
   blocks used to ride 18px of translateY as well, and on a stage that is
   pinned for three screens a line sliding up reads as the PAGE moving —
   which is the one thing a pinned stage exists to prevent. Opacity and a
   defocus, no transform, on all three of them: the rise's line, the
   endgame's copy, and the work's copy and panel. */
.pawn__copy {
  --c: var(--pq, 0);
  grid-area: 1 / 1;
  width: 100%; color: var(--on-bone-1);
  opacity: var(--c);
  filter: blur(calc((1 - var(--c)) * 8px));
}
.pawn .eyebrow { color: var(--signal); }
.pawn .lit { color: var(--signal); }
.pawn__line {
  margin: .55rem 0 0; max-width: 11ch;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(2rem, 5.2vw, 5rem); line-height: 1; letter-spacing: -.035em;
}
.pawn__note { margin: 1.2rem 0 0; max-width: 28ch; font-size: clamp(1rem, 1.35vw, 1.3rem); line-height: 1.45; opacity: .74; }
/* Portrait: the copy sits ABOVE the piece, not beside it. There is no
   beside on a phone, so the copy takes the top of the stage under the
   header and gl.js stands the pawn in the height below it. */
@media (max-aspect-ratio: 4/5) {
  .pawn__stage { align-items: flex-start; padding-top: 13svh; }
  .pawn__copy { padding-left: 0; }
  .pawn__line { max-width: 9ch; font-size: clamp(2rem, 4.6vw, 4.2rem); }
}
@media (prefers-reduced-motion: reduce), (max-width: 0px) {
  .js .pawn { height: auto; }
  .pawn__rail { position: static; }
  .pawn__stage { position: static; height: auto; padding: var(--bay) var(--gut); display: block; }
  .pawn__copy { opacity: 1; transform: none; filter: none; }
}
html.no-gl .js .pawn, .no-gl .pawn { height: auto; }
.no-gl .pawn__rail { position: static; }
.no-gl .pawn__stage { position: static; height: auto; padding: var(--bay) var(--gut); display: block; }
.no-gl .pawn__copy { opacity: 1; transform: none; filter: none; }

/* --- the reach: the ribbon's own beat -----------------------------------
   (2026-09-13.) The section after the rise, on the same sticky construction
   and deliberately reusing .pawn__rail / .pawn__stage / .pawn__copy rather
   than restating them: it is the same stage, the same copy column and the
   same object, and two sets of numbers for one composition is how the two
   drift apart.

   SHORTER THAN THE RISE, because it holds one beat where the rise holds
   five. The ribbon arrives already drawn and all that happens here is that
   it turns, so the scrub only has to be long enough for a turn and a half
   to read as unhurried — 170vh is 70vh of it once the sticky stage has
   taken its own screen out. Longer than that and the reader is scrolling
   past a held frame.

   SHORTER AGAIN (2026-09-15, same client note as the rise above). 145vh is
   45vh of scrub. It is cut less than the rise because there is less here to
   cut: the rise has five beats and a long approach, this has one held frame
   and a turn, and the copy's own bell — in over the first fifth, out over
   the last — needs room to be read at all. The turn came down with it (see
   RIBBON_SPIN in gl.js) so the band travels at about the speed it did
   rather than whipping round a shorter rail.

   AND PORTRAIT IS CUT LESS AGAIN. A phone's scrub is the same number of
   vh but a far smaller number of thumb-lengths, and below about 35vh the
   line arrives and leaves inside one swipe. */
.belt { position: relative; padding: 0; }
.js .belt { height: 145vh; }
@media (max-aspect-ratio: 4/5) { .js .belt { height: 135vh; } }
/* The copy takes the LEFT, as the rise's does, because the ribbon turns
   around the ball on the right and the two must not overlap. --bq is
   written by site.js off this section's own scrub, the way --pq is. */
.belt__copy { --c: var(--bq, 0); }
/* the same two accents the rise uses; .pawn .eyebrow is scoped to .pawn, so
   this section has to say it for itself rather than inherit it */
.belt .eyebrow { color: var(--signal); }
.belt .lit { color: var(--signal); }
/* THE NOTE IS A PARAGRAPH HERE, not a line (2026-09-14, client copy). The
   rise's note is one sentence and sits in 28ch at up to 1.3rem; this one is
   the client's campaign paragraph — forty-odd words — and at those numbers it
   ran thirteen lines down a stage that is only ever one screen tall. Wider
   and a size down: seven lines on a laptop, and on a phone it stays clear of
   the belt standing in the height below it. */
.belt .pawn__note {
  max-width: 40ch;
  font-size: clamp(.94rem, 1.05vw, 1.06rem);
  line-height: 1.55;
}
@media (max-aspect-ratio: 4/5) {
  /* AND THE HEADLINE TAKES TWO LINES, not four. Portrait puts the copy at the
     top of the stage and stands the belt in the height BELOW it, so every
     line the copy spends is a line the object loses. The rise's 9ch broke
     this headline into four ("Built for / brands / beyond / borders.") and
     the paragraph's last lines then sat on the ball; 16ch holds "Built for
     brands" on one line and the copy block clears the object. */
  .belt .pawn__line { max-width: 16ch; }
  .belt .pawn__note { max-width: 34ch; font-size: .92rem; }
}
@media (prefers-reduced-motion: reduce), (max-width: 0px) {
  .js .belt { height: auto; }
  .belt .pawn__rail { position: static; }
  .belt .pawn__stage { position: static; height: auto; padding: var(--bay) var(--gut); display: block; }
  .belt__copy { opacity: 1; transform: none; filter: none; }
}
html.no-gl .js .belt, .no-gl .belt { height: auto; }
.no-gl .belt .pawn__rail { position: static; }
.no-gl .belt .pawn__stage { position: static; height: auto; padding: var(--bay) var(--gut); display: block; }
.no-gl .belt__copy { opacity: 1; transform: none; filter: none; }

/* --- the speck: the act after the strip, on home.html -------------------
   The same slot .pawn holds on index.html, and deliberately the same
   construction: a section that gives the passage its length, a rail with a
   sticky stage above the object's, and copy that arrives on --sq. What
   differs is the act inside it, and how long that act needs.

   IT IS NOT A VARIANT OF .pawn, it is a sibling. Sharing the class and
   switching behaviour on an attribute would put two acts' worth of numbers
   in one block, and the two pages would then have to be read together to
   understand either. One name per act; whichever section is in the DOM is
   the act that page runs. (gl.js does the same: #pawn present -> the plane,
   #speck present -> the fall.)

   380vh. Three beats, and the middle one is the expensive one: the pull-back
   travels about thirty times in scale, and a zoom of that depth needs to be
   read as a distance covered rather than as a cut. At 330 — the rise's
   length — the fall and the landing were fine and the pull-back went past in
   under a screen, which reads as the flower snapping into existence rather
   than as the frame drawing away from it. The extra 50vh is all in the
   third beat.

   Portrait keeps 250vh for the reason every act here does: a scrub measured
   in viewport heights is the same number of pixels on a phone as on a desk,
   but a thumb covers far fewer of them per gesture. */
.speck { position: relative; padding: 0; }
.js .speck { height: 380vh; }
@media (max-aspect-ratio: 4/5) { .js .speck { height: 250vh; } }
.speck__rail { position: absolute; inset: 0; z-index: 6; pointer-events: none; }
.speck__stage { position: sticky; top: 0; height: 100svh; display: grid; align-items: center; padding: 0 var(--gut); }
/* Left of centre, with the flower at 0.30 of the half-width right of it
   (see the speck's block in gl.js) — one composition, not two things at
   opposite edges. The same relationship the rise's copy has to the ball. */
.speck__copy { padding-left: clamp(0px, 9vw, 210px); }
/* Fades and defocuses, no transform: on a stage pinned for nearly four
   screens a line sliding up reads as the PAGE moving, which is the one
   thing a pinned stage exists to prevent. */
.speck__copy {
  --c: var(--sq, 0);
  grid-area: 1 / 1;
  width: 100%; color: var(--on-bone-1);
  opacity: var(--c);
  filter: blur(calc((1 - var(--c)) * 8px));
}
.speck .eyebrow { color: var(--signal); }
.speck .lit { color: var(--signal); }
.speck__line {
  margin: .55rem 0 0; max-width: 11ch;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(2rem, 5.2vw, 5rem); line-height: 1; letter-spacing: -.035em;
}
.speck__note { margin: 1.2rem 0 0; max-width: 30ch; font-size: clamp(1rem, 1.35vw, 1.3rem); line-height: 1.45; opacity: .74; }
/* Portrait: the copy takes the top of the stage and gl.js drops the flower
   into the height below it (narrowMode in the speck's block). */
@media (max-aspect-ratio: 4/5) {
  .speck__stage { align-items: flex-start; padding-top: 13svh; }
  .speck__copy { padding-left: 0; }
  .speck__line { max-width: 9ch; font-size: clamp(2rem, 4.6vw, 4.2rem); }
}
@media (prefers-reduced-motion: reduce), (max-width: 0px) {
  .js .speck { height: auto; }
  .speck__rail { position: static; }
  .speck__stage { position: static; height: auto; padding: var(--bay) var(--gut); display: block; }
  .speck__copy { opacity: 1; transform: none; filter: none; }
}
html.no-gl .js .speck, .no-gl .speck { height: auto; }
.no-gl .speck__rail { position: static; }
.no-gl .speck__stage { position: static; height: auto; padding: var(--bay) var(--gut); display: block; }
.no-gl .speck__copy { opacity: 1; transform: none; filter: none; }

/* --- sound familiar: before / after ---------------------------------------
   The first section on the bone ground the strip hands over. Everything that
   moves here is a function of the item's own --p, written by the staged
   reveal above — so the rows deal in sequence on the way down and unplay on
   the way back, and the reduced-motion floor (--p: 1) makes every transform
   below an identity without a second rule.

   The "after" header is set on --deep, NOT on --signal. --signal is the lit
   brand (#6E6EF5) and white body text on it is 4.0:1 — under AA. --deep is
   the deep brand in both themes (--ink goes white in the light one), and
   white on it is 15:1. The lit blue is kept for the check marks and the
   glow under the block, where it carries no text. */
.familiar__title {
  margin: .55rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: var(--t-h2); line-height: 1; letter-spacing: -.035em;
  color: var(--on-bone-1);
}
.familiar__note {
  margin: 0;
  font-size: var(--t-lead); line-height: 1.45;
  color: var(--on-bone-2);
}
@media (min-width: 781px) { .familiar__note { text-align: right; } }

.compare { display: grid; gap: clamp(1rem, 2vw, 1.6rem); }
@media (min-width: 900px) { .compare { grid-template-columns: 1fr 1fr; } }

/* the same card the reason stack uses: white on bone, hairline, 18px */
.compare__col {
  padding: clamp(.9rem, 1.6vw, 1.25rem);
  border-radius: 18px;
  background: #fff; border: 1px solid rgba(7, 11, 24, .10);
  box-shadow: 0 18px 48px rgba(7, 11, 24, .05);
}

.compare__head {
  padding: clamp(1.1rem, 2vw, 1.5rem) clamp(1.1rem, 2.2vw, 1.6rem);
  border-radius: 12px;
  border: 1px solid rgba(7, 11, 24, .10);
  color: var(--on-bone-1);
  opacity: var(--p);
  transform: translateY(calc((1 - var(--p)) * 18px));
}
.compare__h {
  margin: 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.35rem, 2.2vw, 1.75rem); line-height: 1.05; letter-spacing: -.03em;
}
.compare__sub { margin: .45rem 0 0; font-size: clamp(.9rem, 1.05vw, .98rem); color: var(--on-bone-2); }

.compare__col--after .compare__head {
  border-color: transparent;
  color: var(--on-deep-1);
  background: linear-gradient(118deg, var(--deep-2), var(--deep));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12),
              0 22px 44px -18px color-mix(in oklab, var(--signal) 70%, transparent);
  /* the fill SWEEPS in from the left as the block arrives, instead of
     rising with the quiet header beside it */
  opacity: 1; transform: none;
  clip-path: inset(0 calc((1 - var(--p)) * 100%) 0 0 round 12px);
}
.compare__col--after .compare__sub { color: var(--on-deep-2); }

.compare__list {
  list-style: none; margin: clamp(.5rem, 1vw, .8rem) 0 0;
  padding: 0 clamp(.3rem, .8vw, .6rem);
}
.compare__row {
  /* --r is this row's own share of the COLUMN's --p: the first row is
     finished by 0.55 of the column's reveal and each one after it starts a
     twentieth later, so they still deal in sequence and then hold together.
     They are not stage items of their own any more; see the note in
     index.html for why. */
  --r: clamp(0, calc((var(--p, 1) - var(--i, 0) * 0.05) / 0.55), 1);
  display: grid; grid-template-columns: auto 1fr; align-items: center;
  gap: clamp(.9rem, 1.6vw, 1.3rem);
  padding: clamp(.95rem, 1.8vh, 1.3rem) clamp(.2rem, .6vw, .5rem);
  border-top: 1px solid rgba(7, 11, 24, .09);
  font-size: clamp(.98rem, 1.15vw, 1.08rem); line-height: 1.45;
  /* friction is dim, not orange — the page's own rule for the "before" */
  color: var(--on-bone-2);
  opacity: calc(.08 + var(--r) * .92);
  transform: translateY(calc((1 - var(--r)) * 24px));
}
.compare__row:first-child { border-top: 0; }
.compare__col--after .compare__row { color: var(--on-bone-1); }

.compare__ico {
  display: grid; place-items: center;
  width: 2.75rem; height: 2.75rem; border-radius: 12px;
  background: var(--bone); border: 1px solid rgba(7, 11, 24, .10);
  color: var(--on-bone-2);
  transform: scale(calc(.6 + var(--p) * .4));
}
.compare__ico svg {
  width: 1.2rem; height: 1.2rem;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
/* RED FOR THE COST, GREEN FOR THE ANSWER (2026-09-09, client's word). The
   icons were both the page's indigo, which is correct for a page that runs
   on one hue and useless for a table whose whole job is telling two sides
   apart. Two tokens rather than raw hex so a theme change reaches them, and
   both are dark enough on the bone card to pass AA as text would. */
:root { --warn: #C21A1A; --good: #0E8A4F; }
.compare__col--before .compare__ico {
  color: var(--warn);
  border-color: color-mix(in oklab, var(--warn) 26%, transparent);
  background: color-mix(in oklab, var(--warn) 7%, #fff);
}
.compare__col--after .compare__ico {
  color: var(--good);
  border-color: color-mix(in oklab, var(--good) 30%, transparent);
  background: color-mix(in oklab, var(--good) 8%, #fff);
}

/* --- the switch on the "before" card ------------------------------------
   Two routes, one at a time. Built on the pricing switch's shape so the page
   has one switch, not two: a pill track with the selected option filled.
   Full width here, because the two labels are long and a centred pill in a
   card reads as a stray control. */
.swap {
  display: grid; grid-template-columns: 1fr 1fr; gap: .25rem;
  margin: clamp(.9rem, 1.8vw, 1.2rem) 0 0; padding: .3rem;
  border-radius: 100px;
  background: rgba(7, 11, 24, .05); border: 1px solid rgba(7, 11, 24, .10);
  opacity: var(--p); transform: translateY(calc((1 - var(--p)) * 14px));
}
.swap__opt {
  padding: .68rem .6rem; border: 0; border-radius: 100px;
  background: transparent; color: var(--on-bone-2); cursor: pointer;
  font-size: .6rem; letter-spacing: .13em; text-align: center;
  transition: background-color .35s var(--ease), color .35s var(--ease), box-shadow .35s var(--ease);
}
.swap__opt:hover { color: var(--on-bone-1); }
.swap__opt[aria-selected="true"] {
  background: #fff; color: var(--deep);
  box-shadow: 0 2px 10px rgba(7, 11, 24, .12);
}
@media (prefers-reduced-motion: reduce) { .swap__opt { transition: none; } }
/* With no script both panels stand, so they need to be told apart. */
.compare__col--before .compare__list + .compare__list { margin-top: clamp(.5rem, 1vw, .8rem); }
.js .compare__col--before .compare__list + .compare__list { margin-top: 0; }

/* --- why: the bento --------------------------------------------------------
   Seven cards, seven claims, one WebGL canvas over the whole grid (gl.js,
   `bento`). Two rules here are load-bearing and easy to break:

   1. .why__card must NOT create a stacking context. The canvas is a sibling
      of the cards at z-index 1 and the card's text has to sit ABOVE it, which
      is only possible if the text's z-index resolves in the grid's context —
      so the reveal's opacity and transform live on .why__body and .why__art,
      never on the card.
   2. .why__art is the rectangle the renderer scissors to. Its rect is read
      every frame; anything that changes its box (padding, border) moves the
      scene with it, which is the point. */
.why__title {
  margin: .55rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: var(--t-h2); line-height: 1; letter-spacing: -.035em;
  color: var(--on-bone-1);
}
.why__note { margin: 0; font-size: var(--t-lead); line-height: 1.45; color: var(--on-bone-2); }
@media (min-width: 781px) { .why__note { text-align: right; } }

.why__grid {
  position: relative;
  display: grid; grid-template-columns: 1fr;
  gap: clamp(1rem, 2vw, 1.6rem);
}
@media (min-width: 640px)  { .why__grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1024px) { .why__grid { grid-template-columns: 1fr 1fr 1fr; } }
/* the 2-3-2: columns are real flex columns only when there are three of them;
   below that they dissolve and the cards flow in document order */
.why__col { display: contents; }
@media (min-width: 1024px) {
  .why__col { display: flex; flex-direction: column; gap: inherit; }
  .why__col > .why__card { flex: 1 1 0; }
}


.why__card {
  display: flex; flex-direction: column;
  padding: clamp(.9rem, 1.6vw, 1.25rem);
  border-radius: 18px;
  background: #fff; border: 1px solid rgba(7, 11, 24, .10);
  box-shadow: 0 18px 48px rgba(7, 11, 24, .05);
}
.why__art {
  position: relative; flex: 1 1 auto;
  aspect-ratio: 16 / 9; min-height: 9rem;
  border-radius: 12px;
  background: var(--bone);
  display: grid; place-items: center;
}
.why__card--tall .why__art { aspect-ratio: 4 / 3; }
/* The middle column holds ONE card at the height of the two beside it, so
   its art is portrait rather than landscape; below the three-column break
   the columns dissolve and it is a wide card like any other. */
@media (min-width: 1024px) {
  .why__card--hero .why__art { aspect-ratio: 3 / 4; }
  .why__grid--five .why__col--mid > .why__card { flex: 1 1 0; }
}
.why__card--hero .why__art { aspect-ratio: 4 / 3; }

/* --- a card's art as a rendered clip ---------------------------------------
   Cards that carry a video instead of a live scene. tools/prep-clip.py has
   already cropped the source to a centred 4:3 — which is what takes the
   generator's watermark off the bottom-right corner — so a tall card's box
   fits it exactly and a wide card's crops top and bottom.

   The FILTER is not decoration. The generator renders "deep indigo on bone"
   as a slate-navy object on a grey-lavender ground (measured: #D7D7E0 where
   the page wants #EEF0FF), which reads as a dull grey tile inside a white
   card. Saturate/brightness/contrast walk the ground onto bone and keep the
   object dark; the values are fitted to those two measurements, not guessed. */
.why__vid {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: block; object-fit: cover;
  border-radius: 12px;
  filter: saturate(1.12) brightness(1.1) contrast(1.06);
}
/* the art box is the frame: the clip must not paint outside its corners */
.why__card .why__art { overflow: hidden; }


.why__body {
  position: relative; z-index: 2;
  padding: clamp(1rem, 1.8vw, 1.4rem) clamp(.3rem, .8vw, .6rem) clamp(.3rem, .8vw, .5rem);
  opacity: var(--p);
  transform: translateY(calc((1 - var(--p)) * 18px));
}
.why__h {
  margin: 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.25rem, 1.9vw, 1.55rem); line-height: 1.08; letter-spacing: -.03em;
  color: var(--on-bone-1);
}
.why__p { margin: .5rem 0 0; font-size: clamp(.95rem, 1.1vw, 1.02rem); line-height: 1.5; color: var(--on-bone-2); }

/* --- why, on indigo --------------------------------------------------------
   The bento sits on the brand ground so the proof after it lands on bone:
   the page alternates. The cards stay white — that is the read — and each
   card's art rectangle stays bone, so the seven scenes are untouched. */
.why { background: var(--ink); color: var(--on-ink-1); }
.why__title { color: var(--on-ink-1); }
.why__note { color: var(--on-ink-2); }

/* --- proof -------------------------------------------------------------------
   Figures, marks, and a quote row held back. .fig and .logo were drawn for
   the ink ground — on bone their hairlines and labels vanish — so they get
   their bone values here rather than being redrawn. */
.proof__title {
  margin: .55rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: var(--t-h2); line-height: 1; letter-spacing: -.035em;
  color: var(--on-bone-1);
}
.proof__note { margin: 0; font-size: var(--t-lead); line-height: 1.45; color: var(--on-bone-2); }
/* no one-word last lines on the section notes; balanced wrapping where the
   browser has it, plain wrapping where it does not */
.familiar__note, .why__note, .proof__note { text-wrap: balance; }
@media (min-width: 781px) { .proof__note { text-align: right; } }

/* The head as a lockup: the line and its note on the left, the score card
   on the right, bottoms aligned. The card is the page's white glass with
   the score set in the display face at a size that carries the section —
   one number in place of three small figures. It reveals on its own --p
   like everything staged. */
.proof__head {
  display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(300px, .75fr);
  gap: clamp(1.6rem, 4vw, 4rem); align-items: end;
  padding-bottom: clamp(2.2rem, 4vw, 3.6rem);
}
@media (max-width: 860px) { .proof__head { grid-template-columns: 1fr; } }
.proof__lead .proof__note { margin-top: 1rem; max-width: 40ch; text-align: left; }
.rating {
  position: relative; display: grid; gap: .55rem;
  padding: clamp(1.4rem, 2.4vw, 2rem);
  border-radius: 22px;
  opacity: var(--p, 1);
  transform: translateY(calc((1 - var(--p, 1)) * 22px));
}
.rating__stars { margin: 0; color: #FFC94D; font-size: 1.15rem; letter-spacing: .16em; line-height: 1; }
.rating__num {
  margin: .2rem 0 0; display: flex; align-items: baseline; gap: .7rem;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(4rem, 7.4vw, 6.6rem); line-height: .92; letter-spacing: -.04em;
  color: var(--on-bone-1);
}
.rating__of { font-size: .62rem; letter-spacing: .2em; text-transform: uppercase; color: var(--on-bone-3); }
.rating__meta { margin: 0; font-size: 1rem; line-height: 1.5; color: var(--on-bone-2); }
.rating__meta b { color: var(--on-bone-1); font-weight: 600; }
.rating .proof__link { margin-top: .4rem; }

.proof__link { margin: clamp(1.2rem, 2.4vw, 1.8rem) 0 0; font-size: .92rem; }
.proof__link a {
  display: inline-flex; align-items: center; gap: .5rem;
  color: var(--on-bone-1); text-decoration: none;
  border-bottom: 1px solid rgba(7, 11, 24, .28);
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.proof__link a:hover { color: var(--signal); border-bottom-color: var(--signal); }
.proof__link .btn__arr { transition: transform .3s cubic-bezier(.16,1,.3,1); }
.proof__link a:hover .btn__arr { transform: translateX(3px); }

.proof__logos { margin-top: clamp(2.6rem, 5vw, 4.2rem); }
.proof__eyebrow { color: var(--on-bone-3); }
.proof .logo { border: 1px solid rgba(7, 11, 24, .08); }

/* --- the wall of love --------------------------------------------------------
   Three rows of review cards drifting sideways at their own speeds, the long
   reviews on top, the one-liners at the foot — the shape zyner uses, in this
   page's colours. Each row holds its set twice and slides exactly half its
   width, so the loop has no seam; the middle row runs the other way, which is
   what stops three moving rows reading as one sheet.

   The rows sit outside `.wrap` and pull back over the section's gutter, so
   they run edge to edge WITHOUT `100vw` — that unit counts the scrollbar and
   would hand the page a horizontal scroll of its own. */
.love { margin-top: clamp(2.6rem, 5vw, 4.2rem); margin-inline: calc(var(--gut) * -1); }
.love__head {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: .5rem 1.4rem; padding-inline: var(--gut); margin-bottom: clamp(1rem, 2vw, 1.4rem);
}
/* there is no hovering on a touch screen, so the hint does not belong there */
@media (hover: none) { .love__hint { display: none; } }
.love__row {
  display: flex; overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
}
.love__row + .love__row { margin-top: clamp(.6rem, 1.1vw, .9rem); }
.love__track {
  /* Cards hug their own text. Stretching them to the row's tallest is what
     the reference does, but its quotes are all one length; these run from
     four words to a hundred, and a stretched card puts a hand's width of
     empty white between a short quote and its name. */
  display: flex; flex: none; align-items: flex-start;
  gap: clamp(.6rem, 1.1vw, .9rem); padding-right: clamp(.6rem, 1.1vw, .9rem);
  list-style: none; margin: 0;
  animation: marquee 96s linear infinite; will-change: transform;
}
.love__row--rev .love__track { animation-name: marquee-rev; animation-duration: 74s; }
.love__row:last-child .love__track { animation-duration: 58s; }
/* a reader who wants to finish a quote can stop the row it is on */
/* the rows do not stop for the pointer (the client's request); keyboard
   focus alone holds one, so a Read more can be reached */
.love__track:focus-within { animation-play-state: paused; }

/* THE CARD HUGS ITS QUOTE (revised 2026-09-06). Every card used to be one
   fixed height — six lines of quote whether the quote filled them or not —
   which kept the wall tidy and left "Good work" sitting in a box four times
   the size of its own sentence. Six lines is a CEILING now, not a height,
   the Read more's slot only exists in the cards that carry one, and the
   cards top-align so the gap between two of them is the row's gap and
   nothing else. */
/* The card, revised 2026-09-04 at the client's ask for something more
   premium: a deeper, brand-tinted shadow that lifts it off the bone; a large
   quotation mark in the brand's lit blue at the corner, faint; and it FLOATS
   — a slow bob, its phase set by its place in the row so no two neighbours
   rise together — on top of the row's drift. Nothing about it stops for the
   pointer: the rows run whether or not anyone is reading (the client's
   request), and only keyboard focus holds a row, for the Read more. */
.say {
  position: relative;
  flex: none; width: clamp(272px, 24vw, 340px);
  display: flex; flex-direction: column; gap: .8rem;
  padding: clamp(1.15rem, 1.9vw, 1.55rem);
  border-radius: 22px; background: #fff; border: 1px solid rgba(10, 10, 45, .07);
  box-shadow: 0 26px 64px rgba(10, 10, 122, .09), 0 2px 6px rgba(10, 10, 45, .04);
  animation: say-float 7s ease-in-out infinite;
  will-change: transform;
}
.say::before {
  content: '\201C'; position: absolute; top: .55rem; right: 1.15rem;
  font-family: var(--display); font-weight: 700; font-size: 5.2rem; line-height: 1;
  color: var(--signal); opacity: .13; pointer-events: none;
}
.say:nth-child(2n)   { animation-delay: -2.4s; }
.say:nth-child(3n)   { animation-delay: -4.7s; }
.say:nth-child(4n+1) { animation-delay: -1.3s; }
/* HALVED, 2026-09-06. The gap between two cards is the row's gap and is
   exactly equal everywhere (measured: 14.39px at 1440) - what read as
   uneven spacing was this float, which lifted neighbours out of phase
   far enough that the channel between them looked ragged. Still a
   float, still never stopping, just no longer loud enough to be read
   as a spacing fault. */
@keyframes say-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
.say__stars { margin: 0; color: #FFC94D; font-size: .95rem; letter-spacing: .12em; line-height: 1; }
.say__text {
  margin: 0; font-size: .96rem; line-height: 1.55; color: var(--on-bone-1);
  /* six lines at MOST: a long quote is clipped there and offers a Read
     more, a short one is only as tall as it is */
  max-height: calc(1.55em * 6); overflow: hidden;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 6;
}
/* The slot the Read more lands in. It is only there when there is a button
   in it — an empty one used to be held open at a fixed height so a card
   carrying a button was no taller than its neighbours, and with the cards
   free to differ there is nothing left to keep level. */
.say__act { margin: 0; display: flex; align-items: center; }
.say__act:empty { display: none; }
.say__more {
  font: inherit; font-size: .84rem; line-height: 1.2; color: var(--signal-ink);
  background: none; border: 0; padding: 0; cursor: pointer;
  border-bottom: 1px solid color-mix(in oklab, var(--signal) 45%, transparent);
  transition: border-color .3s var(--ease);
}
.say__more:hover { border-bottom-color: var(--signal); }
.say__more:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; }

/* the review at full length, over the page */
.revdlg { border: 0; padding: 0; background: none; max-width: min(38rem, 92vw); }
.revdlg::backdrop { background: rgba(5, 5, 64, .55); }
.revdlg__card {
  position: relative; display: grid; gap: 1rem;
  padding: clamp(1.5rem, 3vw, 2.1rem); border-radius: 20px; background: #fff;
  box-shadow: 0 40px 90px rgba(5, 5, 64, .35);
}
.revdlg__text { margin: 0; font-size: clamp(1rem, 1.3vw, 1.08rem); line-height: 1.62; color: var(--on-bone-1); }
.revdlg__x {
  position: absolute; top: .6rem; right: .6rem; width: 2.1rem; height: 2.1rem;
  display: grid; place-items: center; border-radius: 50%;
  border: 1px solid rgba(7, 11, 24, .12); background: #fff;
  color: var(--on-bone-2); font-size: 1.25rem; line-height: 1; cursor: pointer;
  transition: background-color .3s var(--ease), color .3s var(--ease);
}
.revdlg__x:hover { background: var(--signal); border-color: var(--signal); color: #fff; }
.say__who { display: flex; align-items: center; gap: .65rem; margin-top: auto; padding-top: .3rem; }
/* no client photographs exist for these, and a stock face on a real review
   would be a lie — the initial stands in for one */
.say__mono {
  flex: none; display: grid; place-items: center; width: 2.4rem; height: 2.4rem;
  border-radius: 50%; background: linear-gradient(135deg, var(--signal), var(--ink));
  box-shadow: 0 8px 18px rgba(110, 110, 245, .28);
  color: #fff; font-family: var(--display); font-weight: 700; font-size: 1rem;
}
.say__id { display: grid; gap: .15rem; min-width: 0; }
.say__id b {
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: .97rem; line-height: 1.1; letter-spacing: -.01em; color: var(--on-bone-1);
}
.say__id .mono { font-size: .56rem; letter-spacing: .16em; text-transform: uppercase; color: var(--on-bone-3); font-style: normal; }

/* Still: the rows stop and become one scrollable strip each, with the
   duplicate set gone so nothing is announced or read twice. */
@media (prefers-reduced-motion: reduce) {
  .love__row { overflow-x: auto; }
  .say { animation: none; }
  /* All three selectors, not just the bare one: the per-row rules that set
     the direction and the speed are MORE SPECIFIC than `.love__track`, so a
     lone `.love__track { animation: none }` left the middle row drifting
     under reduced motion however late it was written. */
  .love__track,
  .love__row--rev .love__track,
  .love__row:last-child .love__track { animation: none; }
  .love__track[aria-hidden="true"] { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .proof .logos__track { animation: none; }
  .proof .fig { opacity: 1; transform: none; }
}

/* --- clip bands: a line of the page cut out of the work ------------------
   Full bleed, so they belong to the section and not to its column: a direct
   child of the <section>, taken back out through its own gutter. Their
   height is written by clipbands() in site.js and is the cap height of the
   line and nothing more — the ribbon is only ever seen through the letters,
   so any band taller than they are is dead space.

   Everything above `.is-live` is the fallback, and it is the whole band with
   no JS and under reduced motion: the line, set large, in the section's own
   colour. -------------------------------------------------------------- */
.clipband { position: relative; overflow: hidden; margin: clamp(3rem, 7vw, 6rem) calc(var(--gut) * -1) 0; }
.clipband--head { margin: 0 calc(var(--gut) * -1) clamp(3rem, 7vw, 6rem); }
.clipband__words {
  margin: 0; padding-inline: var(--gut);
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(2.6rem, 12vw, 11rem); line-height: .84;
  letter-spacing: -.032em; text-transform: uppercase; text-align: center;
  color: var(--on-ink-1);
}
/* live: the words go to the accessibility tree, and the clip takes the page */
.clipband.is-live .clipband__words {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* Neither SVG is ever painted. The first only carries the <clipPath> the
   plate points at; the second is the measuring probe, parked off to the
   left — it has to stay laid out, because getComputedTextLength() and
   getBBox() need real text layout and display:none has none. */
.clipband__def { position: absolute; left: 0; top: 0; width: 1px; height: 1px; overflow: visible; pointer-events: none; }
.clipband__probe { position: absolute; left: -200vw; top: 0; width: 1px; height: 1px; overflow: visible; pointer-events: none; }
.clipband__def text, .clipband__probe text {
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  letter-spacing: 0;   /* the x positions carry the tracking — see CB_TRACK */
}

.clipband__plate { position: relative; width: 100%; height: 100%; }
.clipband__media {
  display: block; width: 100%; height: 100%; object-fit: cover;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  /* clipbands() never runs, so this is only the fallback line — hold it to a
     size that does not shout on a page that has stopped moving */
  .clipband__words { font-size: clamp(2.2rem, 8vw, 6rem); }
}

/* --- crafts: nine cards on indigo ----------------------------------------
   Indigo after the bone proof, and now flat: a card is the name, the lede
   and an arrow, and the whole card is the link.

   Two things this block used to carry are gone on the client's word, and
   the rules for them went with them: the SPLITTING CRYSTAL that filled the
   section behind the cards (a `.crafts__rail` canvas at z-index 0, with the
   copy lifted above it), and the GLYPH tile at the head of each card. There
   is nothing behind the cards now, so neither the rail nor the lift has
   anything left to do.
   -------------------------------------------------------------------------- */
.crafts { background: var(--ink); color: var(--on-ink-1); position: relative; }
.crafts__title {
  margin: .55rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: var(--t-h2); line-height: 1; letter-spacing: -.035em;
  color: var(--on-ink-1);
}

/* NINE CARDS, FIVE THEN FOUR (2026-09-09, client revision).
   This was a full-row list, and before that a WebGL projector with a chip
   bar and a counter. Both are gone; the client asked for nine cards in a
   fixed order, five on the first row and four on the second.

   The 5/4 break is done with a twenty-column grid rather than two lists:
   five cards spanning four columns fill a row exactly, four spanning five
   fill the next, and the nine stay ONE list in the document. Below the wide
   breakpoint the spans reset and the grid falls to three, then two, then
   one, because 5-up is unreadable on anything narrow. */
.crafts__cards {
  list-style: none; margin: clamp(2rem, 4vw, 3rem) 0 0; padding: 0;
  display: grid; gap: clamp(.7rem, 1.4vw, 1.1rem);
  grid-template-columns: 1fr;
  counter-reset: none;
}
@media (min-width: 560px)  { .crafts__cards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 860px)  { .crafts__cards { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1180px) {
  .crafts__cards { grid-template-columns: repeat(20, 1fr); }
  .crafts__cards > .craft:nth-child(-n+5) { grid-column: span 4; }
  .crafts__cards > .craft:nth-child(n+6)  { grid-column: span 5; }
}

.craft {
  opacity: calc(.06 + var(--p) * .94);
  transform: translateY(calc((1 - var(--p)) * 22px));
}
.craft__link {
  /* Three rows: name, lede, arrow. The lede's row takes the slack, so the
     arrow sits on the FLOOR of every card whatever the copy runs to and the
     nine line up along one edge instead of nine different ones. */
  display: grid; gap: .7rem; height: 100%;
  grid-template-rows: auto 1fr auto;
  padding: clamp(1.1rem, 2vw, 1.5rem);
  border-radius: 16px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, .045);
  color: inherit; text-decoration: none;
  transition: border-color .35s var(--ease), background-color .35s var(--ease), transform .35s var(--ease);
}
.craft__link:hover, .craft__link:focus-visible {
  border-color: color-mix(in srgb, var(--signal-2) 55%, transparent);
  background: rgba(255, 255, 255, .075);
  transform: translateY(-3px);
}
.craft__name {
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.15rem, 1.55vw, 1.4rem); line-height: 1.1; letter-spacing: -.02em;
}
/* The five-up row is narrow enough that a long name takes two lines while
   its neighbours take one, and the ledes then start on different lines.
   With the glyph gone there is nothing above to absorb it, so the name
   holds two lines' worth of height (line-height is 1.1) on that row only.
   Not on the four-up row below, where the cards are wide and every name
   fits on one line, and not on the narrow grids, where they all do. */
@media (min-width: 1180px) {
  .crafts__cards > .craft:nth-child(-n+5) .craft__name { min-height: 2.2em; }
}
.craft__lede { font-size: clamp(.86rem, .95vw, .94rem); line-height: 1.5; color: var(--on-ink-2); }
.craft__arr {
  justify-self: end;
  color: var(--on-ink-3); font-size: 1.15rem; line-height: 1;
  transition: transform .35s var(--ease), color .3s var(--ease);
}
.craft__link:hover .craft__arr, .craft__link:focus-visible .craft__arr { color: var(--on-ink-1); transform: translateX(5px); }

@media (prefers-reduced-motion: reduce) {
  .craft { opacity: 1; transform: none; }
  .craft__link:hover { transform: none; }
  .craft__link:hover .craft__arr { transform: none; }
}

/* --- plans: one flat rate, three cards on indigo -------------------------
   The client's structure — name, who it is for, price, both calls above the
   checklist, the rollup — in the page's language: dark glass panels with
   hairlines on the brand ground, Premium lifted and outlined in --signal and
   never recoloured. plans() in site.js drives the switch off data-mult and
   writes the amounts and the billed lines; nothing here is JS-styled. */
/* Indigo, and now sitting directly under the crafts list's indigo: removing
   the process section from between them inverted the ground parity of
   everything downstream, and putting it back would mean repainting the
   pricing, the FAQ and the enquiry form. A rule marks the seam instead
   (.sec--rule on the section), which is the device the page already uses
   where two sections share a ground. */
.plans { background: var(--ink); color: var(--on-ink-1); }
/* On plans.html the tiers follow the masthead, which is already the ink
   ground, so they take the bone instead — and .sec--bone on the same
   section is what turns the tier cards' glass white and rebinds the text
   tokens with it. */
.plans--page { background: var(--bone); color: var(--on-bone-1); }
.plans__title {
  margin: .55rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: var(--t-h2); line-height: 1; letter-spacing: -.035em;
  color: var(--on-ink-1);
}
.plans__note { margin: 0; font-size: var(--t-lead); line-height: 1.45; color: var(--on-ink-2); text-wrap: balance; }
@media (min-width: 781px) { .plans__note { text-align: right; } }

/* the switch: a segmented pill; the selected option is the white one */
.cycle {
  display: inline-flex; gap: .25rem; padding: .3rem;
  margin-bottom: clamp(1.6rem, 3vw, 2.4rem);
  border-radius: 100px; background: var(--ink-2); border: 1px solid var(--line);
}
.cycle__opt {
  display: inline-flex; align-items: center; gap: .55rem;
  padding: .62rem 1.05rem; border-radius: 100px; border: 0;
  background: transparent; color: var(--on-ink-2); cursor: pointer;
  font-size: .62rem; letter-spacing: .14em;
  transition: background-color .35s var(--ease), color .35s var(--ease);
}
.cycle__opt:hover { color: var(--on-ink-1); }
.cycle__opt[aria-selected="true"] { background: #fff; color: var(--deep); }
.cycle__off {
  padding: .18rem .5rem; border-radius: 100px;
  background: rgba(255, 255, 255, .10); color: inherit; font-size: .55rem; letter-spacing: .1em;
}
.cycle__opt[aria-selected="true"] .cycle__off { background: color-mix(in oklab, var(--signal) 18%, transparent); }
/* a narrow phone cannot fit three options and their badges in one row: the
   switch becomes three equal cells with the badge under the label */
@media (max-width: 479px) {
  .cycle { display: grid; grid-template-columns: repeat(3, 1fr); width: 100%; border-radius: 18px; }
  .cycle__opt { flex-direction: column; justify-content: center; gap: .3rem; padding: .6rem .3rem; border-radius: 14px; text-align: center; }
}
:root[data-theme="light"] .cycle { background: var(--ink-2); border-color: rgba(10, 10, 40, .12); }
:root[data-theme="light"] .cycle__opt[aria-selected="true"] { background: var(--signal); color: #fff; }
:root[data-theme="light"] .cycle__opt[aria-selected="true"] .cycle__off { background: rgba(255, 255, 255, .22); }

.tiers { display: grid; gap: clamp(1rem, 2vw, 1.6rem); }
@media (min-width: 900px) { .tiers { grid-template-columns: repeat(3, 1fr); align-items: start; } }

.tier {
  position: relative;
  padding: clamp(1.4rem, 2.4vw, 2rem);
  border-radius: 20px; background: var(--ink-2); border: 1px solid var(--line);
  opacity: calc(.08 + var(--p) * .92);
  transform: translateY(calc((1 - var(--p)) * 26px));
}
.tier--popular {
  border-color: var(--signal);
  box-shadow: 0 30px 80px -30px color-mix(in oklab, var(--signal) 55%, transparent),
              inset 0 0 0 1px color-mix(in oklab, var(--signal) 40%, transparent);
}
@media (min-width: 900px) { .tier--popular { transform: translateY(calc((1 - var(--p)) * 26px - 1rem)); } }
:root[data-theme="light"] .tier { background: #fff; border-color: rgba(10, 10, 40, .12); }
:root[data-theme="light"] .tier--popular { border-color: var(--signal); }

.tier__flag {
  position: absolute; top: -.85rem; left: 50%; transform: translateX(-50%);
  padding: .45rem .85rem; border-radius: 100px;
  /* the darker step: white on --signal measured 4.03:1 at this size */
  background: var(--signal-ink); color: #fff; font-size: .56rem; letter-spacing: .16em; white-space: nowrap;
}
.tier__tag { margin: 0; color: var(--on-ink-3); font-size: .58rem; letter-spacing: .12em; text-transform: none; }
.tier__name {
  margin: .9rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.5rem, 2.4vw, 2rem); line-height: 1.05; letter-spacing: -.03em; color: var(--on-ink-1);
}
.tier__for { margin: .5rem 0 0; min-height: 2.9em; font-size: .95rem; line-height: 1.45; color: var(--on-ink-2); }
.tier__price { margin: 1.4rem 0 0; display: flex; align-items: baseline; gap: .4rem; }
.tier__amount {
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(2.4rem, 3.6vw, 3.2rem); line-height: 1; letter-spacing: -.04em; color: var(--on-ink-1);
}
.tier__per { color: var(--on-ink-3); font-size: .62rem; }
.tier__save { margin: .55rem 0 0; color: var(--on-ink-3); font-size: .58rem; letter-spacing: .12em; }
.tier__acts { display: grid; gap: .6rem; margin-top: 1.3rem; }
.tier__acts .btn { justify-content: center; }

/* The add-ons line, under the three cards. White on the section's own ground
   on the home page; on plans.html the section is bone, so it takes the ink
   token there instead of being hard-coded white and vanishing. */
.tier__addons {
  margin: clamp(1.4rem, 2.6vw, 2.2rem) 0 0;
  max-width: 62ch;
  font-size: clamp(.9rem, 1.05vw, 1rem); line-height: 1.55;
  color: var(--on-ink-1);
}
.plans--page .tier__addons { color: var(--on-bone-1); }
.tier__addons-fine { color: var(--on-ink-3); }
.plans--page .tier__addons-fine { color: var(--on-bone-3); }

.tier__list { list-style: none; margin: 1.4rem 0 0; padding: 0; }
.tier__list li {
  position: relative; padding: .75rem 0 .75rem 1.7rem;
  border-top: 1px solid var(--line);
  font-size: .95rem; line-height: 1.45; color: var(--on-ink-2);
}
.tier__list li::before {
  content: ''; position: absolute; left: 0; top: .95rem; width: 1rem; height: 1rem; border-radius: 50%;
  background: color-mix(in oklab, var(--signal) 22%, transparent);
}
.tier__list li::after {
  content: ''; position: absolute; left: .3rem; top: 1.18rem; width: .42rem; height: .22rem;
  border-left: 2px solid var(--signal-2); border-bottom: 2px solid var(--signal-2); transform: rotate(-45deg);
}
.tier__roll { padding-left: 0 !important; color: var(--on-ink-1); font-weight: 600; }
.tier__roll::before, .tier__roll::after { display: none; }
.tier__meta { margin-left: .5rem; color: var(--on-ink-3); font-size: .55rem; letter-spacing: .12em; }
:root[data-theme="light"] .tier__list li::after { border-color: var(--signal); }

@media (prefers-reduced-motion: reduce) {
  .tier { opacity: 1; transform: none; }
  @media (min-width: 900px) { .tier--popular { transform: translateY(-1rem); } }
}


/* --- faq: asked, answered ----------------------------------------------------
   The head sticks on the left; the rows are white cards that open in place.

   The rows are `.ask`, NOT `.qa`. The inner pages carry a `.qa` accordion of
   their own on the deep ground — `.qa summary` (0,1,1) outranks a bare
   `.qa__q` (0,1,0), so those rules were winning here: the summary came out
   as a space-between flex row with no side padding, which centred the
   question and pushed the control off the card's edge, and `.qa__a p` painted
   the answer in the deep ground's near-white. On a white card that is an
   answer nobody can read. A separate name is the fix; the old block is left
   alone for the pages that still use it. */
.faq { color: var(--on-bone-1); }
.faq__grid { display: grid; gap: clamp(1.6rem, 3vw, 2.6rem); }
@media (min-width: 900px) {
  .faq__grid { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); align-items: start; }
  .faq__head { position: sticky; top: clamp(5rem, 12vh, 7rem); }
}
.faq__title {
  margin: .55rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: var(--t-h2); line-height: 1; letter-spacing: -.035em; color: var(--on-bone-1);
}
.faq__note { margin: 1rem 0 0; max-width: 34ch; font-size: var(--t-lead); line-height: 1.45; color: var(--on-bone-2); text-wrap: balance; }
/* the sticky column used to run empty under the note for most of the section */
.faq__more { margin: clamp(1.2rem, 2.4vw, 1.8rem) 0 0; font-size: .92rem; }
.faq__more a {
  display: inline-flex; align-items: center; gap: .5rem;
  color: var(--on-bone-1); text-decoration: none;
  border-bottom: 1px solid rgba(7, 11, 24, .28);
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.faq__more a:hover { color: var(--signal); border-bottom-color: var(--signal); }
.faq__more .btn__arr { transition: transform .3s cubic-bezier(.16, 1, .3, 1); }
.faq__more a:hover .btn__arr { transform: translateX(3px); }

.faq__list { display: grid; gap: .6rem; }
.ask {
  border-radius: 18px; background: #fff; border: 1px solid rgba(7, 11, 24, .10);
  box-shadow: 0 14px 40px rgba(7, 11, 24, .04);
  opacity: calc(.06 + var(--p) * .94);
  transform: translateY(calc((1 - var(--p)) * 18px));
  filter: blur(calc((1 - var(--p)) * 8px));
  transition: border-color .3s var(--ease), box-shadow .3s var(--ease);
}
.ask:hover { border-color: color-mix(in oklab, var(--signal) 40%, transparent); }
.ask[open] { border-color: color-mix(in oklab, var(--signal) 55%, transparent); box-shadow: 0 22px 56px rgba(7, 11, 24, .08); }
/* the whole row is the control: one target, from the card's edge in */
.ask__q {
  list-style: none; display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: clamp(.8rem, 1.6vw, 1.15rem);
  padding: clamp(.95rem, 1.7vw, 1.25rem) clamp(1rem, 1.8vw, 1.35rem);
  cursor: pointer;
}
.ask__q::-webkit-details-marker { display: none; }
.ask__q:focus-visible { outline: 2px solid var(--signal); outline-offset: -3px; border-radius: 18px; }
.ask__no { color: var(--on-bone-3); font-size: .6rem; }
.ask__t {
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.02rem, 1.45vw, 1.22rem); line-height: 1.25; letter-spacing: -.02em;
  color: var(--on-bone-1); text-align: left; text-wrap: pretty;
}
/* a real 2.3rem target, inset from the edge, that turns from + to − */
.ask__x {
  position: relative; flex: none; width: 2.3rem; height: 2.3rem; border-radius: 50%;
  background: color-mix(in oklab, var(--signal) 10%, transparent);
  transition: background-color .3s var(--ease);
}
.ask__x::before, .ask__x::after {
  content: ''; position: absolute; left: 50%; top: 50%; width: .8rem; height: 2px; border-radius: 2px;
  background: var(--signal); transform: translate(-50%, -50%);
  transition: transform .35s var(--ease), background-color .3s var(--ease);
}
.ask__x::after { transform: translate(-50%, -50%) rotate(90deg); }
.ask[open] .ask__x { background: var(--signal); }
.ask[open] .ask__x::before, .ask[open] .ask__x::after { background: #fff; }
.ask[open] .ask__x::after { transform: translate(-50%, -50%) rotate(0); }
/* the answer starts under the question, not under the number */
.ask__a {
  padding: 0 clamp(1rem, 1.8vw, 1.35rem) clamp(1rem, 1.8vw, 1.3rem);
  padding-left: calc(clamp(1rem, 1.8vw, 1.35rem) + 2.1rem + clamp(.8rem, 1.6vw, 1.15rem));
}
.ask__a p {
  margin: 0; max-width: 60ch; padding-top: clamp(.8rem, 1.4vw, 1rem);
  border-top: 1px solid rgba(7, 11, 24, .09);
  /* stated, not inherited: the row is white and the page's default body
     colour here is written for the deep ground */
  color: var(--on-bone-2); font-size: .97rem; line-height: 1.6;
}
@media (max-width: 560px) {
  .ask__a { padding-left: clamp(1rem, 1.8vw, 1.35rem); }
}
@supports (interpolate-size: allow-keywords) {
  .ask { interpolate-size: allow-keywords; }
  .ask::details-content { height: 0; overflow: hidden; transition: height .4s var(--ease), content-visibility .4s allow-discrete; }
  .ask[open]::details-content { height: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .ask { opacity: 1; transform: none; filter: none; transition: none; }
  .ask__x::before, .ask__x::after { transition: none; }
  .ask::details-content { transition: none; }
}

/* --- vision and mission, as two cards ---------------------------------------
   On the studio page. Two statements set side by side, in the page's own
   card language, so they read as claims the studio is making rather than as
   two more subheadings inside an argument. */
.creed { display: grid; gap: clamp(.9rem, 1.8vw, 1.3rem); margin-top: clamp(2rem, 4vw, 3rem); }
@media (min-width: 760px) { .creed { grid-template-columns: 1fr 1fr; } }
.creed__card { padding: clamp(1.3rem, 2.4vw, 1.9rem); border-radius: 20px; }
/* The name of the card is the accent (2026-09-14). It was --on-ink-3, the
   dimmest text token on the page, which made the label the last thing read on
   a card whose whole job is to say which of the two statements this is. It
   takes the eyebrow's colours — the brand lit on a dark ground, the darker
   ink of it on white — and a little more of the mono's weight. */
.creed__k { color: var(--beam); font-weight: 600; }
.sec--bone .creed__k, .glass--white .creed__k { color: var(--signal-ink); }
.creed__line {
  margin: .9rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: clamp(1.3rem, 2.2vw, 1.8rem); line-height: 1.14; letter-spacing: -.03em;
  color: var(--on-ink-1); text-wrap: pretty;
}
.creed__note { margin: .9rem 0 0; font-size: .95rem; line-height: 1.6; color: var(--on-ink-2); }
.lede-card__link { margin: 1rem 0 0; font-size: .92rem; }
.lede-card__link a {
  /* the same move as .ceoq__by: tokens, so the link reads on either ground */
  color: var(--on-ink-1); text-decoration: none;
  border-bottom: 1px solid color-mix(in oklab, currentColor 40%, transparent);
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.lede-card__link a:hover { color: var(--signal); border-bottom-color: var(--signal); }
.lede-card__link .btn__arr { display: inline-block; margin-left: .4rem; transition: transform .3s cubic-bezier(.16, 1, .3, 1); }
.lede-card__link a:hover .btn__arr { transform: translateX(3px); }

/* --- the enquiry: the form on the home page ------------------------------------
   Indigo between the bone FAQ and the deep close, so the page keeps
   alternating. The copy sits left with the three other ways to reach us; the
   form is one glass card on the right, in the same language as the plans.

   Every field has a real label above it — a placeholder is not a label: it
   leaves the field unnamed the moment anything is typed, and it is the first
   thing a screen reader loses. The one placeholder here is an example, on a
   field that already has its own label. */
/* The ground is the modifier, not the block: the same form sits on indigo on
   the home page and on bone on the contact page, where it is a white card
   with dark type — the shape the FAQ's rows already use there. */
.enquire--ink { background: var(--ink); color: var(--on-ink-1); }
.enquire__grid { display: grid; gap: clamp(1.8rem, 4vw, 3rem); align-items: start; }
@media (min-width: 900px) { .enquire__grid { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); } }
.enquire__copy {
  opacity: calc(.06 + var(--p) * .94);
  transform: translateY(calc((1 - var(--p)) * 18px));
  filter: blur(calc((1 - var(--p)) * 8px));
}
.enquire__title {
  margin: .55rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: var(--t-h2); line-height: 1; letter-spacing: -.035em; color: var(--on-ink-1);
}
.enquire__lead { margin: 1.1rem 0 0; max-width: 38ch; font-size: var(--t-lead); line-height: 1.5; color: var(--on-ink-2); }
.reach { list-style: none; margin: clamp(1.6rem, 3vw, 2.4rem) 0 0; padding: 0; display: grid; gap: .9rem; }
.reach li { display: grid; gap: .15rem; }
.reach__k { color: var(--on-ink-3); }
.reach a {
  color: var(--on-ink-1); text-decoration: none; font-size: 1rem;
  border-bottom: 1px solid color-mix(in oklab, var(--beam) 35%, transparent);
  justify-self: start; transition: color .3s var(--ease), border-color .3s var(--ease);
}
.reach a:hover { color: var(--beam); border-bottom-color: var(--beam); }
.reach .btn__arr { display: inline-block; margin-left: .4rem; transition: transform .3s cubic-bezier(.16, 1, .3, 1); }
.reach a:hover .btn__arr { transform: translateX(3px); }

.enquire__form {
  display: grid; gap: clamp(.9rem, 1.6vw, 1.15rem);
  padding: clamp(1.3rem, 2.4vw, 1.9rem);
  opacity: calc(.06 + var(--p) * .94);
  transform: translateY(calc((1 - var(--p)) * 22px));
  filter: blur(calc((1 - var(--p)) * 8px));
}
@media (min-width: 620px) { .enquire__form { grid-template-columns: 1fr 1fr; } }
.enquire__field { display: grid; gap: .4rem; grid-column: 1 / -1; }
@media (min-width: 620px) { .enquire__field--half { grid-column: span 1; } }
.enquire__field label {
  font-family: var(--mono); font-size: var(--t-mono); font-weight: 500;
  letter-spacing: .16em; text-transform: uppercase; color: var(--on-ink-2);
}
.enquire__opt { color: var(--on-ink-3); }
.enquire__field input, .enquire__field select, .enquire__field textarea {
  font: inherit; font-size: .97rem; color: var(--on-ink-1);
  width: 100%; padding: .75rem .9rem; border-radius: 12px;
  background: rgba(255, 255, 255, .06);
  border: 1px solid color-mix(in oklab, var(--beam) 22%, transparent);
  transition: border-color .25s var(--ease), background-color .25s var(--ease);
}
.enquire__field textarea { resize: vertical; min-height: 7.5rem; line-height: 1.5; }
.enquire__field input::placeholder, .enquire__field textarea::placeholder { color: var(--on-ink-3); }
.enquire__field input:hover, .enquire__field select:hover, .enquire__field textarea:hover { border-color: color-mix(in oklab, var(--beam) 40%, transparent); }
.enquire__field input:focus-visible, .enquire__field select:focus-visible, .enquire__field textarea:focus-visible {
  outline: 0; border-color: var(--beam); background: rgba(255, 255, 255, .10);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--signal) 40%, transparent);
}
/* the native arrow is drawn in the system's colours, which on this ground is
   a dark mark on a dark field */
.enquire__field select {
  appearance: none; -webkit-appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--beam) 50%), linear-gradient(135deg, var(--beam) 50%, transparent 50%);
  background-position: calc(100% - 1.15rem) 1.15rem, calc(100% - .8rem) 1.15rem;
  background-size: .35rem .35rem, .35rem .35rem;
  background-repeat: no-repeat;
  padding-right: 2.4rem;
}
.enquire__field select option { background: var(--deep); color: #fff; }
.enquire__field [aria-invalid="true"] { border-color: #FF9C9C; }
.enquire__err { margin: 0; font-size: .82rem; line-height: 1.4; color: #FFB4B4; }
.enquire__send { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: .9rem 1.2rem; }
.enquire__note { margin: 0; font-size: .84rem; color: var(--on-ink-3); }
.enquire__status { grid-column: 1 / -1; margin: 0; font-size: .92rem; line-height: 1.5; color: var(--on-ink-2); }
.enquire__status:empty { display: none; }
.enquire__status.is-ok { color: var(--beam); }
.enquire__status.is-bad { color: #FFB4B4; }
/* --- the same form, on the bone ground ------------------------------------ */
.sec--bone.enquire .enquire__title { color: var(--on-bone-1); }
.sec--bone.enquire .enquire__lead { color: var(--on-bone-2); }
.sec--bone.enquire .reach__k { color: var(--on-bone-3); }
.sec--bone.enquire .reach a { color: var(--on-bone-1); border-bottom-color: rgba(7, 11, 24, .28); }
.sec--bone.enquire .reach a:hover { color: var(--signal); border-bottom-color: var(--signal); }
.sec--bone.enquire .enquire__field label { color: var(--on-bone-2); }
.sec--bone.enquire .enquire__opt { color: var(--on-bone-3); }
/* background-COLOR, not the shorthand: the shorthand resets the size and
   position of the select's arrow gradients set above, which draws them full
   size and repeating — a field that looks like a filled button. */
.sec--bone.enquire .enquire__field input,
.sec--bone.enquire .enquire__field select,
.sec--bone.enquire .enquire__field textarea {
  color: var(--on-bone-1);
  background-color: color-mix(in oklab, var(--signal) 4%, #fff);
  border-color: rgba(7, 11, 24, .16);
}
.sec--bone.enquire .enquire__field input::placeholder,
.sec--bone.enquire .enquire__field textarea::placeholder { color: var(--on-bone-3); }
.sec--bone.enquire .enquire__field input:hover,
.sec--bone.enquire .enquire__field select:hover,
.sec--bone.enquire .enquire__field textarea:hover { border-color: color-mix(in oklab, var(--signal) 45%, transparent); }
.sec--bone.enquire .enquire__field input:focus-visible,
.sec--bone.enquire .enquire__field select:focus-visible,
.sec--bone.enquire .enquire__field textarea:focus-visible { border-color: var(--signal); background-color: #fff; }
/* the arrow is drawn in the accent, not in the light one the indigo needs */
.sec--bone.enquire .enquire__field select {
  background-image: linear-gradient(45deg, transparent 50%, var(--signal) 50%), linear-gradient(135deg, var(--signal) 50%, transparent 50%);
}
.sec--bone.enquire .enquire__field select option { background: #fff; color: var(--on-bone-1); }
.sec--bone.enquire .enquire__note { color: var(--on-bone-3); }
.sec--bone.enquire .enquire__status { color: var(--on-bone-2); }
.sec--bone.enquire .enquire__status.is-ok { color: var(--signal); }
/* #B3261E on white is 6.1:1; the light red the indigo uses would be 1.6:1 */
.sec--bone.enquire .enquire__err,
.sec--bone.enquire .enquire__status.is-bad { color: #B3261E; }
.sec--bone.enquire .enquire__field [aria-invalid="true"] { border-color: #B3261E; }

@media (prefers-reduced-motion: reduce) {
  .enquire__copy, .enquire__form { opacity: 1; transform: none; filter: none; }
}

/* --- close: the last call, the waveform and the dark footer ------------------
   On the ground, under the last bloom, with the generative waveform drawn
   behind the copy. The badge is only its ring of text turning around the hub
   (no plate, 2026-09-04); the footer is the dark one with the two rooms. */
.close { color: var(--on-deep-1); padding-bottom: 0; }
.close__grid { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: clamp(2rem, 4vw, 3.5rem); }
.close__copy { opacity: calc(.06 + var(--p) * .94); transform: translateY(calc((1 - var(--p)) * 18px)); filter: blur(calc((1 - var(--p)) * 8px)); }
.close__title {
  margin: .55rem 0 0;
  font-family: var(--display); font-weight: 700; font-stretch: 84%;
  font-size: min(clamp(2.6rem, 6.4vw, 6rem), 14vh); line-height: .98; letter-spacing: -.035em; color: var(--on-deep-1);
}
.close__lead { margin: 1.4rem 0 0; max-width: 48ch; font-size: var(--t-lead); line-height: 1.5; color: var(--on-deep-2); }
.close__acts { display: flex; flex-wrap: wrap; gap: .7rem; margin-top: clamp(1.6rem, 3vw, 2.4rem); }

/* the dark footer, five columns wide here: the brand and the Sri Lanka room,
   the United States room, and the three link columns */
@media (min-width: 1040px) { .close .foot { grid-template-columns: 1.4fr 1.1fr .8fr .8fr .8fr; } }
.foot__k { margin-top: 1.1rem; color: var(--on-ink-3); }
.foot > * > .foot__k:first-child { margin-top: 0; }
.close .foot { opacity: calc(.06 + var(--p, 1) * .94); transform: translateY(calc((1 - var(--p, 1)) * 18px)); }
@media (prefers-reduced-motion: reduce) {
  .close__copy, .badge, .close .foot { opacity: 1; transform: none; filter: none; }
  .badge__ring { animation: none; }
}

/* --- motion and objects, section by section ---------------------------------
   The reveals resolve out of a blur; the chips and glass fit each section's
   own ground. Colours come from the sections' own rules above. */
.compare__head { filter: blur(calc((1 - var(--p)) * 8px)); }
.compare__row { filter: blur(calc((1 - var(--r)) * 8px)); }
.compare__col.glass { padding: clamp(1rem, 1.8vw, 1.4rem); }
.compare__col--after .compare__head.glass--white { clip-path: inset(0 calc((1 - var(--p)) * 100%) 0 0 round 12px); opacity: 1; transform: none; }
.compare__ico { width: auto; height: auto; padding: .55rem; border-radius: 100px; }
.compare__ico.chip { transform: scale(calc(.6 + var(--r) * .4)); animation: none; }

/* why: white cards with a chip in the corner; the card is positioned but is
   never a stacking context, so the canvas keeps sliding between ground and words */
.why__card.glass--white { position: relative; transform: none; transition: none; box-shadow: 0 18px 48px rgba(7, 11, 24, .06); }
.why__chip { position: absolute; top: clamp(1.2rem, 2vw, 1.6rem); right: clamp(1.2rem, 2vw, 1.6rem); z-index: 2; }
.why__body { filter: blur(calc((1 - var(--p)) * 8px)); }

/* proof: figures as white tiles; the wall's cards carry their own colours */
.proof .fig.glass { border-top: 0; padding: clamp(1.2rem, 2vw, 1.6rem); filter: blur(calc((1 - var(--p)) * 8px)); }

/* crafts: the nine cards resolve out of a defocus like every other reveal */
.craft { filter: blur(calc((1 - var(--p)) * 8px)); }

/* process: the frames lean; the copy resolves */
.frame.glass { border-radius: 14px; transform: perspective(900px) rotateX(var(--ty, 0deg)) rotateY(var(--tx, 0deg)) scale(calc(.94 + var(--k) * .06)); filter: blur(calc((1 - var(--k)) * 6px)); }
.stepf__copy { filter: blur(calc((1 - var(--k)) * 8px)); }
.stepf__no.chip { display: inline-flex; width: auto; }

/* plans: glass tiers on indigo; Premium keeps its signal edge */
.tier.glass { transform: perspective(900px) rotateX(var(--ty, 0deg)) rotateY(var(--tx, 0deg)) translateY(calc((1 - var(--p)) * 26px + var(--lift, 0px))); filter: blur(calc((1 - var(--p)) * 8px)); }
@media (min-width: 900px) { .tier--popular.glass { transform: perspective(900px) rotateX(var(--ty, 0deg)) rotateY(var(--tx, 0deg)) translateY(calc((1 - var(--p)) * 26px - 1rem + var(--lift, 0px))); } }
.tier--popular.glass { border-color: var(--signal); }
.cycle.glass { border-radius: 100px; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .compare__head, .compare__row, .why__body, .proof .fig.glass, .craft, .frame.glass, .stepf__copy, .tier.glass { filter: none; }
  .frame.glass, .tier.glass { transform: none; }
}

/* --- native scroll-driven reveals -----------------------------------------
   The sections below carried no scroll motion at all: pricing tiers, FAQ
   rows, the proof figures and the receipts strip all simply appeared.

   These use the browser's OWN scroll timeline (`animation-timeline: view()`)
   rather than the rAF + custom-property machinery the prelude runs on. The
   difference is where the work happens: a view() timeline animating only
   transform and opacity is driven on the COMPOSITOR, so it stays exactly as
   smooth as the scroll itself and costs the main thread nothing — which
   matters on a page that already runs a WebGL prelude, a pinned rail and a
   three-stage strip scrub through one rAF loop.

   Everything here is wrapped in @supports. Where the timeline is
   unavailable (~16% of browsers today) NOTHING is declared, so the elements
   keep their natural, fully-visible state — the fallback is the content
   itself, not a degraded animation. Reduced motion opts out the same way.

   Ranges deliberately COMPLETE EARLY (by `cover 30%`ish). A range that
   finishes late leaves a tall element stuck mid-fade when it is already the
   thing you are reading. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    @keyframes ds-rise {
      from { opacity: 0; transform: translateY(30px); }
      to   { opacity: 1; transform: none; }
    }
    @keyframes ds-rise-sm {
      from { opacity: 0; transform: translateY(16px); }
      to   { opacity: 1; transform: none; }
    }
    @keyframes ds-lift {
      from { opacity: 0; transform: translateY(26px) scale(.97); }
      to   { opacity: 1; transform: none; }
    }

    /* pricing tiers — staggered so the three do not land as one block */
    
    
    

    /* FAQ rows — small movement only; these are dense and a big rise reads
       as the list flinching */
    .qa {
      animation: ds-rise-sm linear both;
      animation-timeline: view();
      animation-range: entry 2% cover 22%;
    }

    /* the proof figures */
    .fig {
      animation: ds-rise linear both;
      animation-timeline: view();
      animation-range: entry 5% cover 28%;
    }
    .fig:nth-child(even) { animation-range: entry 10% cover 33%; }

    /* receipts ride a horizontal drag strip, so they rise only — a scale
       here fights the strip's own transform */
    .receipt {
      animation: ds-rise-sm linear both;
      animation-timeline: view();
      animation-range: entry 3% cover 24%;
    }

    /* NOT .rev — deck() stacks those cards with its own --i depth transform,
       and a both-fill animation ending at `transform: none` flattens it. */

    /* Section headings are deliberately NOT here. They already reveal by
       line-roll, and a :not([data-rise]) guard cannot exclude them anyway:
       splitHeads() STRIPS that attribute when it converts a heading, so at
       runtime the guard matches everything it was written to skip. One
       reveal system per element — this one leaves the headings alone. */
  }
}

/* --- proof, rebuilt as a ledger -------------------------------------------
   Was four stacked blocks of equal weight — a figures row, a logo wall, a
   receipts strip and a footnote — with nothing binding them. Worse, that
   figures row repeated 200+, 12, 9 and 1, every one of which the statement
   strip already counts out, so the section opened on numbers the reader had
   just watched tick up.

   Now it is three rows with one argument, and each row keeps its LABEL
   STICKY beside its evidence: the claim stays on screen while the thing
   backing it scrolls past, which is the whole point of a receipt. */
.ledger { list-style: none; margin: clamp(1.8rem, 3.4vw, 2.8rem) 0 0; padding: 0; }

.ledger__row {
  display: grid; gap: clamp(1.2rem, 2.6vw, 2.6rem);
  padding: clamp(2rem, 4.5vw, 3.6rem) 0;
  border-top: 1px solid var(--line);
  opacity: calc(.25 + var(--p, 1) * .75);
  transform: translateY(calc((1 - var(--p, 1)) * 26px));
}
.ledger__row:first-child { border-top: 0; padding-top: 0; }
@media (min-width: 900px) {
  .ledger__row { grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); gap: clamp(2rem, 5vw, 4.5rem); }
  /* the label rides along while its evidence passes */
  .ledger__label { position: sticky; top: clamp(6rem, 14vh, 9rem); align-self: start; }
}

.ledger__no { display: block; color: var(--signal); font-size: .64rem; letter-spacing: .18em; text-transform: uppercase; }
.ledger__title {
  margin-top: .7rem;
  font-family: var(--display); font-weight: 700; font-stretch: 86%;
  font-size: clamp(1.25rem, 2.4vw, 1.75rem); line-height: 1.12;
  letter-spacing: -.025em; color: var(--on-ink-1);
}
.ledger__note { margin-top: .6rem; max-width: 32ch; font-size: .89rem; line-height: 1.55; color: var(--on-ink-3); }

/* min-width: 0 is load-bearing. A grid item defaults to min-width: auto, so
   it refuses to shrink below its content — and the content here is a logo
   marquee several thousand pixels wide. Without this the whole PAGE scrolled
   sideways on a phone, even though the marquee's own row clips it. */
.ledger__body { min-width: 0; }

/* the ledger supplies the rhythm now, so these lose their own top margins */
.ledger .logos, .ledger .receipts { margin-top: 0; }
.ledger .logos__rows { margin-top: 0; }

/* two verifiable facts, not a six-up figures row */
.figs--pair { grid-template-columns: 1fr; gap: clamp(1.1rem, 2.2vw, 1.8rem); }
@media (min-width: 620px) { .figs--pair { grid-template-columns: repeat(2, 1fr); } }
.fig__note { margin-top: .6rem; font-size: .84rem; line-height: 1.5; color: var(--on-ink-3); }

@media (prefers-reduced-motion: reduce) {
  .ledger__row { opacity: 1; transform: none; }
  .ledger__label { position: static; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .wall__col { animation: none !important; }
  .hero__line > span { transform: none !important; }
  [data-rise] { opacity: 1 !important; transform: none !important; }
  /* the script never arms a goo heading under reduced motion; this covers a
     filter already armed before the preference changed mid-session */
  [data-goo] .rl { filter: none !important; }
  /* the JS never writes --par under reduced motion; this covers a value
     already written before the preference changed mid-session */
  .tile img, .reason__art img, .receipt img, [data-par] { --par: 0px !important; }
  .js .tile { opacity: 1 !important; transform: none !important; }
  .manifesto__text w { opacity: 1 !important; }
  /* crafts carries its own reduced-motion unpinning now, next to the deck
     rules it has to undo — the rail these lines used to reset is gone */
  .btn__sheen { display: none; }
  .boot { display: none; }
}


/* --- the hall ----------------------------------------------------------------
   A WHITE room lit blue (room.js): white walls, a white mirror floor, every
   prop a white clay, every light in the room blue. So the copy goes the
   other way from the rest of the deep sections - brand blue on the pale
   room, the site's light theme in miniature. The tokens the pricing tiers
   and the billing cycle read (`--on-ink-*`, `--line`, `--signal`) are
   re-pointed on the beat to their light values so that markup restyles
   itself; the few rules keyed on `:root[data-theme="light"]` are repeated
   for the beat below. The scrim element still mounts (studio.js does not
   know which room it has) and is not drawn on a wide screen. */
.studio-track { background: #E8ECF5; }
.studio-beat {
  color: #08088A;
  --ink: #FFFFFF; --ink-2: #EEF1FB; --signal: #08088A; --signal-2: #2A2ADB; --beam: #4B4BE8;
  --bone: #F1F3FC;
  --line: rgba(10, 10, 40, .14); --line-solid: rgba(10, 10, 40, .3);
  --on-ink-1: rgba(8, 8, 90, .96); --on-ink-2: rgba(8, 8, 90, .7); --on-ink-3: rgba(8, 8, 90, .5);
}
.studio-beat h2,
.studio-beat h3,
.studio-beat p { text-shadow: none; }
/* The scrim comes BACK for this room, on every width. The dark hall let
   white type sit straight on it; a white room lit blue gives the copy a
   ground that is sometimes pale wall and sometimes a saturated blue pool,
   and brand-blue type disappears into the second. A pale column on the
   copy's side keeps it readable without hiding the room. */
/* NO scrim. It was a pale panel behind the copy at every width, and a white
   block sitting over the room is exactly what it looked like. The room is
   dimmer instead, so brand-blue type has something to sit on. */
.studio-scrim { display: none; }
.studio-beat--wide > * { max-width: 44rem; }
.studio-beat .lead { max-width: 40ch; }

/* --- one rhythm for every beat ------------------------------------------
   Each beat is the same three things in the same order: a hairline and a
   mono label, a headline that gets a real measure, then the content. What
   changes between beats is the content, not the frame around it. */
.studio-beat .eyebrow {
  position: relative; padding-top: .9rem; margin: 0 0 .7rem;
  font-size: .7rem; font-weight: 600; letter-spacing: .16em; color: #08088A;
}
.studio-beat .eyebrow::before {
  content: ''; position: absolute; left: 0; top: 0;
  width: 2.4rem; height: 2px; background: #0B3BE8;
}
.studio-beat--right .eyebrow::before { left: auto; right: 0; }
.studio-beat h2 {
  font-size: clamp(2.1rem, 4.2vw, 3.4rem); line-height: .96;
  letter-spacing: -.022em; margin: 0;
}
.studio-beat .lead {
  margin: 1rem 0 0; max-width: 38ch;
  font-size: 1rem; line-height: 1.55; color: rgba(8, 8, 90, .82);
}

/* the services list - nine at once, hover to light */
.hall-svc__list {
  list-style: none; margin: 1.6rem 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .35rem 1.4rem;
}
.hall-svc__list a {
  display: flex; gap: .75rem; align-items: baseline;
  padding: .62rem .55rem .62rem .2rem; color: #08088A; text-decoration: none;
  font-size: .98rem; letter-spacing: -.005em;
  border-bottom: 1px solid rgba(8, 8, 138, .18);
  transition: color .25s var(--ease), border-color .25s var(--ease),
              background-color .25s var(--ease), padding-left .25s var(--ease);
}
.hall-svc__list a b {
  font: 500 .68rem/1 "IBM Plex Mono", ui-monospace, monospace;
  color: #6C7BD6; flex: none;
}
.hall-svc__list a:hover, .hall-svc__list a:focus-visible, .hall-svc__list a.is-held {
  color: #0B3BE8; border-color: #0B3BE8; padding-left: .6rem;
  background: linear-gradient(90deg, rgba(11, 59, 232, .09), rgba(11, 59, 232, 0));
}
.hall-svc__list a.is-held b { color: #0B3BE8; }
.hall-svc__hint {
  margin-top: 1.3rem; color: rgba(8, 8, 90, .7);
  font-size: .68rem; letter-spacing: .12em;
}

/* process and why-us: numbered rows on hairlines, one rhythm each */
.steps-in__list, .why-in__list { list-style: none; margin: 1.5rem 0 0; padding: 0; }
.steps-in__list li {
  display: grid; grid-template-columns: 2.2rem 1fr; gap: .9rem; align-items: baseline;
  padding: .85rem 0; border-top: 1px solid rgba(8, 8, 138, .16);
  font-size: .98rem; line-height: 1.45; color: rgba(8, 8, 90, .8);
}
.steps-in__list b { font-size: .68rem; color: #6C7BD6; letter-spacing: .08em; }
.why-in__list li {
  padding: .85rem 0; border-top: 1px solid rgba(8, 8, 138, .16);
  font-size: .95rem; line-height: 1.5; color: rgba(8, 8, 90, .72);
}
.why-in__list li b {
  display: block; margin-bottom: .15rem; color: #08088A;
  font-size: 1rem; font-weight: 600; letter-spacing: -.01em;
}

/* The board of client marks. When the room is there the marks are ON the
   wall in 3D, so printing them again in the copy puts fourteen small logos
   on top of fourteen large ones. The DOM grid is the fallback's copy of
   them - it shows only when there is no room to put them in. */
.studio-track:not(.no-gl) .brands-in__grid { display: none; }
.brands-in__note {
  margin-top: 1.1rem; max-width: 26ch;
  font-size: .92rem; line-height: 1.5; color: rgba(8, 8, 90, .68);
}
.studio-track.no-gl .brands-in__note { display: none; }
.brands-in__grid {
  list-style: none; margin: 1.4rem 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: .5rem 1rem; align-items: center;
}
.brands-in__grid img {
  display: block; width: 100%; height: auto; max-height: 3.2rem;
  object-fit: contain; mix-blend-mode: multiply;
}
@media (max-width: 760px) {
  .brands-in__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .4rem .7rem; }
  .brands-in__grid img { max-height: 2.4rem; }
}

/* the review wall copy */
/* the review wall copy. The quotes are printed on the tiles in 3D, so what
   is left in the page is the claim and where to check it. */
.revwall__src {
  display: inline-flex; align-items: center; gap: .4rem;
  margin-top: 1.4rem; padding: .6rem 1.1rem;
  border: 1px solid rgba(8, 8, 138, .3); border-radius: 100px;
  color: #08088A; font-size: .85rem; font-weight: 600; text-decoration: none;
  transition: background-color .25s var(--ease), border-color .25s var(--ease);
}
.revwall__src:hover { background: rgba(11, 59, 232, .08); border-color: #0B3BE8; }

/* The review wall copy. It is `.revwall`, NOT `.wall`: that name belongs to
   the hero's photo wall (absolute, inset -6%, a two-column grid) and the
   beat inherited every line of it. Its quotes keep `.rev` for the card look
   but must undo the deck's absolute stacking, or all three sit on one spot. */
/* The quotes are ON the wall in 3D. In the page they are the accessible
   copy and the fallback's, so they stay in the markup and stay out of the
   frame - printing them twice put a stack of cards over the tiles that
   already carry them. */
.studio-track:not(.no-gl) .revwall__quotes { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.revwall__quotes { list-style: none; margin: 0; padding: 0; display: grid; gap: .9rem; }
.revwall__quotes .rev {
  position: static; inset: auto; transform: none; opacity: 1; pointer-events: auto;
  box-shadow: none; gap: .35rem;
  padding: .9rem 1rem; border-radius: .8rem; background: rgba(255, 255, 255, .72);
  border: 1px solid rgba(8, 8, 138, .14);
}
.revwall__quotes .rev blockquote { margin: .3rem 0 .6rem; font-size: .95rem; line-height: 1.45; -webkit-line-clamp: 4; }
.revwall__quotes .rev__stars { color: #0B3BE8; font-size: .8rem; letter-spacing: .1em; }
.revwall__quotes .rev__who { display: flex; gap: .6rem; align-items: center; font-size: .78rem; }
.revwall__quotes .rev__who img { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; flex: none; }
.revwall__quotes .rev__who b { font-size: .8rem; }
.revwall__quotes .rev__who span { font-size: .72rem; color: rgba(8, 8, 90, .6); }


/* pricing inside the hall: the tiers need the width, and the light look */
.studio-beat[data-shot="pricing"] > * { max-width: min(64rem, 92vw); }


.studio-beat .btn--ghost:hover { background: rgba(8, 8, 138, .06); border-color: rgba(8, 8, 138, .5); }

@media (max-width: 760px) {
  .hall-svc__list { grid-template-columns: 1fr; }
  .studio-beat--wide > *, .studio-beat[data-shot="pricing"] > * { max-width: 100%; }
}



/* ====================================================================== */
/* CONTENT REVISION, 2026-09-09 — the client's pass over every page.      */
/* ====================================================================== */

/* --- the floating WhatsApp handle ---------------------------------------
   Fixed to the frame on every page, bottom-right, on its own z-index BELOW
   the cookie notice (300) so an answer is never covered by a chat button,
   and above everything the page itself draws. The notice is left-anchored
   and capped at 680px, so on a wide frame the two do not meet at all; on a
   narrow one the handle lifts out of the notice's way. */
.wa {
  position: fixed; z-index: 280;
  right: var(--gut); bottom: var(--gut);
  display: grid; place-items: center;
  width: 56px; height: 56px;
  border-radius: 50%;
  background: #25D366; color: #fff;
  box-shadow: 0 10px 28px rgba(2, 2, 24, .34);
  transition: transform .35s var(--ease), box-shadow .35s var(--ease);
}
.wa svg { width: 32px; height: 32px; fill: currentColor; }
.wa:hover, .wa:focus-visible { transform: translateY(-3px) scale(1.04); box-shadow: 0 16px 36px rgba(2, 2, 24, .42); }
@media (max-width: 719px) {
  /* the notice runs the full width down here, so the handle rides above it
     for as long as it is open (site.js adds .has-cc to <html>) */
  .wa { width: 50px; height: 50px; }
  .wa svg { width: 28px; height: 28px; }
  html.has-cc .wa { bottom: calc(var(--gut) + 220px); }
}
@media (prefers-reduced-motion: reduce) { .wa { transition: none; } }
/* Out of the way while something is over the page. body.is-locked is set by
   the work preview and by the mobile menu, and the handle would otherwise
   sit on top of the preview's next arrow. */
body.is-locked .wa { opacity: 0; pointer-events: none; }

/* the footer lockup is a link now; it must not pick up the link colour */
.foot__home { color: inherit; text-decoration: none; }
.foot__home:hover .foot__mark img { opacity: 1; }

/* ── service pages (/seo, /web-development, …) ───────────────────────────
   Nine pages generated by tools/build-services.py. Everything here is
   namespaced .svp-* because site.css serves every page on the site and the
   short, obvious names are all taken — .svc is the CARD on /services, and a
   .svc__deliv here would inherit that grid and collapse.

   THE BORDERS ARE WRITTEN TWICE ON PURPOSE. .sec--bone rebinds --on-ink-* to
   the dark-on-pale set, but it deliberately does NOT rebind --line, which is
   a 16%-white rule made for the indigo ground and is invisible on bone. So
   anything drawn inside a bone section states its own edge. */
/* THE BLOCK IS CAPPED, NOT THE COLUMN INSIDE IT. .wrap runs to about 1400px
   and wider still on a large monitor. Letting the prose column take 1fr and
   then capping the paragraphs at a readable measure leaves the difference as
   a hole in the middle of the composition — at 1900px that was some 460px of
   nothing between the text and the card, which read as a layout fault rather
   than as air. Capping the whole block instead keeps the two columns beside
   each other and turns the leftover width into page margin, where it belongs. */
.svp {
  display: grid;
  gap: clamp(1.6rem, 3vw, 3.4rem);
  margin-top: clamp(1.8rem, 3.4vw, 2.6rem);
  max-width: 1160px;
}
@media (min-width: 900px) {
  .svp { grid-template-columns: minmax(0, 1fr) clamp(300px, 32%, 380px); align-items: start; }
}

/* 66ch, not the 80 the rest of the site runs at: this is the only long-form
   prose on the site, and at this size the column the cap above produces is
   almost exactly this wide — so the two agree instead of fighting. */
.svp__prose { max-width: 66ch; }
.svp__prose p {
  font-size: clamp(1rem, 1.35vw, 1.09rem);
  line-height: 1.72;
  color: var(--on-ink-2);
}
.svp__prose p + p { margin-top: 1.1em; }

.svp__side {
  padding: clamp(1.3rem, 2.4vw, 1.8rem);
  border: 1px solid rgba(10, 10, 45, .14);
  border-radius: 14px;
  background: #fff;
  position: sticky; top: calc(var(--gut) + 64px);
}
:root[data-theme="light"] .svp__side { border-color: rgba(10, 10, 40, .13); }
@media (max-width: 899px) { .svp__side { position: static; } }

.svp__k {
  font-size: .66rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--on-ink-3);
}
.svp__deliv { margin-top: 1rem; list-style: none; }
.svp__deliv li {
  position: relative;
  padding: .62rem 0 .62rem 1.35rem;
  font-size: .92rem; line-height: 1.55;
  color: var(--on-ink-1);
  border-top: 1px solid rgba(10, 10, 45, .1);
}
.svp__deliv li:first-child { border-top: 0; padding-top: 0; }
.svp__deliv li:first-child::before { top: .42rem; }
/* a rule, not a tick: the list is what the engagement contains, and a tick
   reads as a claim that it is already done */
.svp__deliv li::before {
  content: ""; position: absolute; left: 0; top: 1.04rem;
  width: .62rem; height: 1px; background: var(--signal, #08088A); opacity: .55;
}
.svp__more { margin-top: 1.3rem; }

/* ── related crafts ─────────────────────────────────────────────────── */
.svp__rel {
  display: grid; gap: clamp(.9rem, 1.8vw, 1.3rem);
  margin-top: clamp(1.8rem, 3.4vw, 2.6rem);
}
@media (min-width: 760px) { .svp__rel { grid-template-columns: repeat(3, 1fr); } }
.svp__relcard {
  display: flex; flex-direction: column; gap: .55rem;
  padding: clamp(1.2rem, 2.2vw, 1.7rem);
  border: 1px solid rgba(10, 10, 45, .14);
  border-radius: 14px;
  background: #fff;
  color: inherit; text-decoration: none;
  transition: border-color .25s, transform .25s;
}
.svp__relcard:hover { border-color: rgba(10, 10, 45, .34); transform: translateY(-2px); }
.svp__relcard h3 {
  font-family: var(--display); font-weight: 600; font-stretch: 88%;
  font-size: clamp(1.02rem, 1.7vw, 1.2rem); letter-spacing: -.02em;
}
.svp__relcard p { font-size: .89rem; line-height: 1.6; color: var(--on-ink-2); }
.svp__relgo { margin-top: auto; padding-top: .5rem; font-size: .7rem; letter-spacing: .16em; color: var(--on-ink-3); }
.svp__relcard:hover .svp__relgo { color: var(--on-ink-1); }
@media (prefers-reduced-motion: reduce) { .svp__relcard { transition: none; } }

/* the service card heading is a link to that craft's own page now; it should
   read as the heading it is, and only admit to being a link on hover */
.svc__name a { color: inherit; text-decoration: none; }
.svc__name a:hover,
.svc__name a:focus-visible {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .18em;
}

/* ── blog (/blog and the article pages) ──────────────────────────────────
   Namespaced .post-* / .postcard-* for the same reason as .svp-*: site.css
   serves every page and the obvious names are taken.

   The article body arrives as plain <h2>/<h3>/<p>/<ul>/<figure> with every
   WordPress class stripped, so ALL of its typography is set here by element.
   That is deliberate — it means a future post needs no classes in its markup,
   and it is why these selectors are scoped under .post rather than global. */
.post {
  max-width: 72ch;
  margin-top: clamp(1.4rem, 3vw, 2.2rem);
  color: var(--on-ink-2);
}
.post > :first-child { margin-top: 0; }
.post p {
  font-size: clamp(1rem, 1.35vw, 1.09rem);
  line-height: 1.75;
  margin-top: 1.05em;
}
.post h2 {
  margin-top: 2.1em;
  font-family: var(--display); font-weight: 600; font-stretch: 88%;
  font-size: clamp(1.32rem, 2.5vw, 1.72rem); letter-spacing: -.02em; line-height: 1.18;
  color: var(--on-ink-1);
}
.post h3 {
  margin-top: 1.7em;
  font-family: var(--display); font-weight: 600; font-stretch: 92%;
  font-size: clamp(1.06rem, 1.8vw, 1.22rem); letter-spacing: -.015em;
  color: var(--on-ink-1);
}
/* the article's own headings were written with <strong> inside them; it is
   redundant at these weights and makes the display face look broken */
.post h2 strong, .post h3 strong { font-weight: inherit; }
.post ul, .post ol { margin-top: 1.05em; padding-left: 1.35rem; }
.post li { margin-top: .5em; font-size: clamp(1rem, 1.3vw, 1.06rem); line-height: 1.7; }
.post li::marker { color: var(--on-ink-3); }
.post strong { color: var(--on-ink-1); font-weight: 600; }
.post a { color: var(--signal, #08088A); text-underline-offset: .18em; }
.post__fig { margin: 1.9em 0 0; }
.post__fig img {
  width: 100%; height: auto; display: block;
  border-radius: 12px; border: 1px solid rgba(10, 10, 45, .12);
}
.post__meta { margin-top: 1.1rem; font-size: .72rem; letter-spacing: .16em; color: var(--on-ink-3); }
/* an article headline is a sentence, not a two-word statement, so it cannot
   carry the poster size the other mastheads use */
.phero__title--post {
  font-size: clamp(1.9rem, 5vw, 3.4rem);
  max-width: 19ch;
}

/* ── the index ───────────────────────────────────────────────────────── */
.posts { display: grid; gap: clamp(1rem, 2vw, 1.5rem); margin-top: clamp(1.8rem, 3.4vw, 2.6rem); }
.postcard {
  display: grid; gap: clamp(1rem, 2.4vw, 1.8rem);
  padding: clamp(1rem, 2vw, 1.4rem);
  border: 1px solid rgba(10, 10, 45, .14); border-radius: 14px;
  background: #fff; color: inherit; text-decoration: none;
  transition: border-color .25s, transform .25s;
}
@media (min-width: 760px) { .postcard { grid-template-columns: minmax(0, 300px) minmax(0, 1fr); align-items: center; } }
.postcard:hover { border-color: rgba(10, 10, 45, .34); transform: translateY(-2px); }
.postcard__shot { display: block; overflow: hidden; border-radius: 10px; }
.postcard__shot img { display: block; width: 100%; height: auto; }
.postcard__copy { display: block; }
.postcard__date { display: block; font-size: .7rem; letter-spacing: .16em; color: var(--on-ink-3); }
.postcard__title {
  margin-top: .5rem;
  font-family: var(--display); font-weight: 600; font-stretch: 88%;
  font-size: clamp(1.15rem, 2.2vw, 1.5rem); letter-spacing: -.02em; line-height: 1.2;
  color: var(--on-ink-1);
}
.postcard__ex { display: block; margin-top: .6rem; font-size: .92rem; line-height: 1.65; color: var(--on-ink-2); }
.postcard__go { display: block; margin-top: .9rem; font-size: .7rem; letter-spacing: .16em; color: var(--on-ink-3); }
.postcard:hover .postcard__go { color: var(--on-ink-1); }
@media (prefers-reduced-motion: reduce) { .postcard { transition: none; } }
