/* Kelabu Black — the marketing site's own language. Standalone: does not extend site.css.
   Governed by docs/72-marketing-design-language.md (look) and docs/73 (motion).

   TWO MODES, and only the first one is guaranteed:
     base            — normal flow, everything visible, no sticky track, cards in a grid.
                       This is what a dead coordinator, a dead GPU and reduced motion all get.
     [data-choreo]   — the sticky story stage of docs/73, opted into by kelabu.js once it has
                       measured the page AND proved the tallest composition fits. */

/* ---------- fonts ---------- */
/* ITF FFL v2.0 permits self-hosting and forbids subsetting: ship these bytes unmodified. */

@font-face{
  font-family:'Zodiak'; font-style:normal; font-weight:300; font-display:swap;
  src:url('/marketing-assets/fonts/zodiak-300.woff2') format('woff2');
}
@font-face{
  font-family:'Zodiak'; font-style:normal; font-weight:400; font-display:swap;
  src:url('/marketing-assets/fonts/zodiak-400.woff2') format('woff2');
}
@font-face{
  font-family:'Switzer'; font-style:normal; font-weight:200; font-display:swap;
  src:url('/marketing-assets/fonts/switzer-200.woff2') format('woff2');
}
@font-face{
  font-family:'Switzer'; font-style:normal; font-weight:300; font-display:swap;
  src:url('/marketing-assets/fonts/switzer-300.woff2') format('woff2');
}
@font-face{
  font-family:'Switzer'; font-style:normal; font-weight:400; font-display:swap;
  src:url('/marketing-assets/fonts/switzer-400.woff2') format('woff2');
}
@font-face{
  font-family:'Switzer'; font-style:normal; font-weight:500; font-display:swap;
  src:url('/marketing-assets/fonts/switzer-500.woff2') format('woff2');
}

/* Metric-matched fallbacks, so the swap does not reflow. Solved against the page's own strings
   in Chrome on Windows: residual width error 0.06% on the headline and 0% on the lede, from
   -3.8% and -5.4% with the bare Georgia/system-ui fallbacks. The numbers are solved for the
   FIRST local() in each list (Georgia, Arial); a platform without those falls through to a
   face with different metrics and gets less of the benefit. Re-solve if display sizes change. */
@font-face{
  font-family:'Zodiak Fallback'; font-style:normal; font-weight:300 400;
  src:local('Georgia'),local('Times New Roman'),local('Liberation Serif');
  size-adjust:104.19%; ascent-override:95.02%; descent-override:23.99%; line-gap-override:0%;
}
@font-face{
  font-family:'Switzer Fallback'; font-style:normal; font-weight:200 500;
  src:local('Arial'),local('Helvetica'),local('Liberation Sans');
  size-adjust:98.85%; ascent-override:99.14%; descent-override:25.29%; line-gap-override:0%;
}

/* ---------- registered scroll properties ---------- */
/* Every one of these is a <number>: a '%' suffix on a registered <number> is silently rejected
   and the property freezes at its last good value. CSS converts to lengths itself.

   Written by kelabu.js  : --s --dock --quality (root), --enter --leave --vis (per element).
   Written by carousel.js: --phi (on #ring).
   Authored in markup    : --x --y --w --i and the --in-*/--out-* offsets.

   Initial values ARE the arrived, at-rest pose, so a page with no coordinator is complete. */

@property --p{ syntax:'<number>'; initial-value:0; inherits:true; }
@property --pv{ syntax:'<number>'; initial-value:0; inherits:true; }

@property --s{ syntax:'<number>'; initial-value:0; inherits:true; }
@property --dock{ syntax:'<number>'; initial-value:0; inherits:true; }
@property --quality{ syntax:'<number>'; initial-value:0; inherits:true; }

@property --enter{ syntax:'<number>'; initial-value:1; inherits:true; }
@property --leave{ syntax:'<number>'; initial-value:0; inherits:true; }
@property --vis{ syntax:'<number>'; initial-value:1; inherits:true; }

/* inherits:false — an offset declared on a card must not travel into its descendants */
@property --x{ syntax:'<number>'; initial-value:0; inherits:false; }
@property --y{ syntax:'<number>'; initial-value:0; inherits:false; }
@property --w{ syntax:'<number>'; initial-value:1; inherits:false; }
@property --i{ syntax:'<number>'; initial-value:0; inherits:false; }
@property --in-x{ syntax:'<number>'; initial-value:0; inherits:false; }
@property --in-y{ syntax:'<number>'; initial-value:0; inherits:false; }
@property --in-s{ syntax:'<number>'; initial-value:1; inherits:false; }
@property --in-ry{ syntax:'<number>'; initial-value:0; inherits:false; }
@property --out-x{ syntax:'<number>'; initial-value:0; inherits:false; }
@property --out-y{ syntax:'<number>'; initial-value:0; inherits:false; }
@property --phi{ syntax:'<number>'; initial-value:0; inherits:false; }
@property --ap{ syntax:'<number>'; initial-value:1; inherits:false; }
/* the pin's own anchoring translate. REGISTERED so it does NOT inherit: an unregistered
   --ay:-50% on a centred pin reached every .anim child and shifted each by half its own
   height. */
@property --ax{ syntax:'<percentage>'; initial-value:0%; inherits:false; }
@property --ay{ syntax:'<percentage>'; initial-value:0%; inherits:false; }

/* docs/75 s5 placement, published as LENGTHS by kelabu.js. Every rule that reads one is
   scoped to [data-choreo], and the same measure() pass writes both, so an unset value can
   never reach the page. --uh is the stage height, --objb the base object frame. */
@property --pad{ syntax:'<length>'; initial-value:24px; inherits:true; }
@property --colw{ syntax:'<length>'; initial-value:0px; inherits:true; }
@property --objb{ syntax:'<length>'; initial-value:0px; inherits:true; }
@property --objenv{ syntax:'<length>'; initial-value:0px; inherits:true; }
@property --uh{ syntax:'<length>'; initial-value:0px; inherits:true; }
@property --cartop{ syntax:'<length>'; initial-value:0px; inherits:true; }
@property --carobj{ syntax:'<length>'; initial-value:0px; inherits:true; }

@property --foot-h{ syntax:'<length>'; initial-value:0px; inherits:true; }

/* the closing wordmark's fitted size and its single light sweep (docs/75 s6) */
@property --wm-size{ syntax:'<length>'; initial-value:0px; inherits:true; }
@property --wm{ syntax:'<number>'; initial-value:0; inherits:true; }

/* --vh is deliberately NOT registered: every use is var(--vh,100svh), so the svh fallback
   only survives while kelabu.js has not published its own measured viewport height. It is a
   LENGTH ('900px'), not a number, and it is the one definition all page geometry reads —
   set it to whatever the coordinator's scroll progress divides by (docs/73 §1). */

/* ---------- tokens ---------- */

:root{
  --ink:#0C0A0A;
  --ink-lift:#151211;
  --bone:#F2EFE9;
  --bone-2:#A8A19B;
  --bone-3:#6E6763;  /* 3.56:1 on --ink: non-text decoration only, never text (docs/72 s2) */
  --edge:rgba(242,239,233,.08);
  --volt:#5B5BF0;
  --volt-soft:#8C8CFF;

  --font-display:'Zodiak','Zodiak Fallback',Georgia,serif;
  --font-text:'Switzer','Switzer Fallback',Arial,sans-serif;

  --nav-h:64px;
  --gutter:clamp(20px,4vw,44px);
  --measure:1200px;
  --ease:cubic-bezier(.2,.7,.2,1);
  /* S90: the landing's copy plays timed entrances (docs/73 S90 revision) */
  --t-in:820ms;
  --t-out:380ms;

  /* width follows the (capped) height so the card stays portrait at every viewport, and the
     height is capped against the stage: an authored 400 overflowed a 714px stage,
     composesFit() refused, and the whole page silently dropped to flat. */
  --card-w:min(300px, calc(var(--card-h) * 0.78));
  --card-h:min(400px, calc((100svh - 64px) * 0.43));
  --ring-r:calc(1.125 * var(--card-w));
  --ring-p:calc(3.75 * var(--card-w));

  color-scheme:dark;
}

/* ---------- reset ---------- */

*,*::before,*::after{ box-sizing:border-box; }

html{
  background:var(--ink);
  scroll-behavior:smooth;
  -webkit-text-size-adjust:100%;
}

body{
  margin:0;
  color:var(--bone);
  font-family:var(--font-text);
  font-weight:300;
  font-size:15px;
  line-height:1.6;
  font-synthesis:none;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

img,svg,canvas,video{ display:block; max-width:100%; }
h1,h2,h3,h4,p,figure,ul,ol,dl,blockquote{ margin:0; }
ul,ol{ padding:0; list-style:none; }
a{ color:inherit; text-decoration:none; }
button{ font:inherit; color:inherit; background:none; border:0; padding:0; }
strong,b{ font-weight:500; }

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

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

.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0;
}

.skip{
  position:fixed; left:16px; top:-72px; z-index:100;
  background:var(--volt); color:#fff;
  padding:12px 16px; border-radius:4px;
  font-size:14px; letter-spacing:.01em;
  transition:top 180ms var(--ease);
}
.skip:focus{ top:12px; }

/* ---------- the two canvases and the poster ---------- */
/* Neither canvas may sit inside a transformed ancestor, so both are body-level and fixed. */

#sky,#scene{
  position:fixed; inset:0; width:100%; height:100%;
  pointer-events:none;
}
#sky{ z-index:0; }
#scene{ z-index:1; }

/* The static wash sky.js replaces. It is the whole sky under reduced motion and with no JS,
   so it carries its own dither: the gradient spans ~11 of 255 levels and bands without it. */
#sky{
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.04'/%3E%3C/svg%3E"),
    radial-gradient(58vmax 46vmax at 74% 58%, rgba(91,91,240,.11), rgba(91,91,240,0) 68%),
    radial-gradient(46vmax 38vmax at 28% 18%, rgba(140,140,255,.05), rgba(140,140,255,0) 70%);
  background-repeat:repeat,no-repeat,no-repeat;
}
html[data-sky-live] #sky{ background-image:none; }

/* Shown until scene.js proves a first frame, and again on context loss, so a machine with no
   WebGL still gets a hero. Absolute against the initial containing block in base mode, so it
   covers exactly the first viewport; fixed once the scene is choreographed. */
#scene-poster{
  position:absolute; top:0; left:0; z-index:2;
  width:100%; height:var(--vh,100svh);
  pointer-events:none;
  object-fit:contain; object-position:100% 50%;
  transition:opacity 260ms var(--ease);
}
html[data-scene-live] #scene-poster{ opacity:0; }
#scene-poster[hidden]{ display:none; }

/* The docked object's own poster, in the two compositions the quill poster cannot cover
   (it is gone by s=.160). docs/72 §3: the reveal and the close each carry a real visual, so
   this one paints whenever the WebGL object is not drawing — no context, a lost one, quality
   tier 6, reduced motion, or plain normal flow. data-scene-live is set ONLY while a first
   frame has been proved, which makes it the single condition for all of those. */
.knot{ margin-top:clamp(28px,4vw,52px); pointer-events:none; }
.knot__img{
  width:min(420px,78vw); height:auto; margin-inline:auto;
  transition:opacity 260ms var(--ease);
}
html[data-scene-live] .knot__img{ opacity:0; }

.nav,#main,.foot{ position:relative; z-index:10; }
.nav{ z-index:40; }

/* ---------- base layout: normal flow ---------- */

.story{ position:relative; }

.moment{
  position:relative;
  width:100%; max-width:var(--measure); margin-inline:auto;
  padding-block:clamp(90px,12vw,190px); padding-inline:var(--gutter);
  overflow-x:clip;
}

.moment--hero{
  display:grid; align-content:center;
  min-height:calc(var(--vh,100svh) - var(--nav-h));
  padding-block:clamp(48px,8vw,110px) clamp(90px,12vw,190px);
}

/* the markers exist for the choreographed track's arrival positions; in normal flow the
   moments carry their own ids and these have nothing to point at */
.anchor{ display:none; }

.pin > * + *{ margin-top:clamp(20px,2.6vw,32px); }

.flow > * + *{ margin-top:var(--flow,20px); }
.flow--tight{ --flow:12px; }
.flow--loose{ --flow:32px; }

.actions{ display:flex; flex-wrap:wrap; align-items:center; gap:12px; }

/* ---------- type ---------- */
/* Light weight, POSITIVE tracking, tight leading. Never weight >= 500 on display,
   never negative tracking on display. */

/* overflow-wrap is the last resort, not the first: `text-wrap:balance` and normal wrapping run
   first, and this only ever fires when a SINGLE word is wider than the line — which at 200% text
   zoom on a 320px screen is any of these headings. Sections clip horizontally, so without it the
   word is simply cut off with no way to reach it. */
.d1{
  font-family:var(--font-display); font-weight:300;
  font-size:clamp(36px,5.4vw,78px);
  letter-spacing:.025em; line-height:.92;
  text-transform:uppercase; text-wrap:balance;
  max-width:24ch; min-width:0; overflow-wrap:break-word;
}

/* the two authored lines scene.js rasterizes; the spans force the break, and a line that is
   still too wide for the viewport must be free to wrap again — .moment clips the x axis */
.d1__l{ display:block; }

/* scene.js sets this once its glyph texture has drawn at the measured position: geometry and
   the accessibility tree stay, only the paint goes (docs/73 s3) */
.d1[data-raster="1"]{ color:transparent; }

.d2{
  font-family:var(--font-display); font-weight:300;
  font-size:clamp(29px,3.3vw,46px);
  letter-spacing:.01em; line-height:1.1;
  text-wrap:balance; max-width:22ch; min-width:0; overflow-wrap:break-word;
}

.d3{
  font-family:var(--font-display); font-weight:400;
  font-size:21px; letter-spacing:0; line-height:1.3;
  text-wrap:balance;
}

.eyebrow{
  display:block;
  font-family:var(--font-text); font-weight:300;
  font-size:13px; letter-spacing:.13em; line-height:1;
  text-transform:uppercase; color:var(--bone-2);
}

.lede{
  font-weight:300; font-size:17px; line-height:1.55;
  /* px, not ch: `ch` is the 0-glyph width, which size-adjust does NOT match — 46ch measures
     450.42px in Switzer and 429.78px in the fallback, so the container moved on swap */
  color:var(--bone-2); max-width:450px;
}

/* the hero's three-step hierarchy: what it is, the brand line, one line of substance */
.hero__eyebrow{ color:var(--volt-soft); letter-spacing:.16em; }
.pin--hero .d1{ margin-top:18px; }
.pin--hero .lede{ font-size:18px; max-width:470px; text-wrap:balance; }
.pin--hero .actions{ margin-top:34px; }

/* ---------- buttons ---------- */

.btn{
  display:inline-flex; align-items:center; justify-content:center;
  font-family:var(--font-text); font-weight:400;
  font-size:15px; letter-spacing:.01em; line-height:1;
  padding:13px 16px; border-radius:4px;
  background:var(--volt); color:#fff;
  cursor:pointer; white-space:nowrap;
  transition:background 180ms var(--ease), transform 120ms var(--ease);
}
/* hover DARKENS: --volt under white is 5.00:1, and lightening it drops below the 4.5:1 floor
   (the #6E6EF4 that shipped first measured 4.04:1). #4A4ADB is 6.38:1. */
.btn:hover{ background:#4A4ADB; }
.btn:active{ transform:translateY(1px); }
.btn:focus-visible{ outline-color:var(--bone); }

.btn--quiet{ background:none; color:var(--bone); }
.btn--quiet:hover{ background:rgba(242,239,233,.07); }
.btn--quiet:focus-visible{ outline-color:var(--volt); }

/* the landing's secondary CTA: a real button outline, not a text link, so the second choice
   reads as one at a glance. Bone on ink is 16:1; the hairline is decoration. */
.btn--ghost{
  background:rgba(12,10,10,.55); color:var(--bone);
  box-shadow:inset 0 0 0 1px rgba(242,239,233,.28);
  gap:10px;
}
.btn--ghost:hover{ background:rgba(36,33,32,.85); box-shadow:inset 0 0 0 1px rgba(242,239,233,.5); }
.btn--ghost:focus-visible{ outline-color:var(--volt); }
.btn__arrow{ display:inline-block; transition:transform 180ms var(--ease); }
.btn--ghost:hover .btn__arrow{ transform:translateX(3px); }

.ctl{
  display:inline-flex; align-items:center; justify-content:center;
  font-size:14px; font-weight:300; letter-spacing:.01em; line-height:1;
  padding:11px 14px; border-radius:4px;
  color:var(--bone-2);
  box-shadow:inset 0 0 0 1px var(--edge);
  cursor:pointer;
  transition:color 160ms var(--ease), background 160ms var(--ease);
}
.ctl:hover{ color:var(--bone); background:rgba(242,239,233,.06); }

/* ---------- product imagery ---------- */
/* Real captures on a near-black ground. Depth comes from the rim and the volt light behind,
   not from a drop shadow — black on #0C0A0A reads as nothing. No transition on .shot: its
   transform is scroll-driven, and a transition would lag the pose behind the scroll. */

.shot{
  position:relative; isolation:isolate; margin:0;
  border-radius:10px;
}

.shot::before{
  content:''; position:absolute; inset:-9% -6% -12%; z-index:0;
  background:radial-gradient(58% 54% at 50% 46%, rgba(91,91,240,.26), rgba(91,91,240,0) 70%);
}

.shot__img{
  position:relative; z-index:1;
  width:100%; height:auto;
  border-radius:10px;
  background:var(--ink-lift);
  box-shadow:0 34px 72px -34px rgba(0,0,0,.95), 0 2px 12px rgba(0,0,0,.55);
}

/* a product clip reserves its 16:10 box before its poster arrives (preload="none") */
video.shot__img{ display:block; aspect-ratio:16/10; object-fit:cover; }

.shot::after{
  content:''; position:absolute; inset:0; z-index:2; pointer-events:none;
  border-radius:10px;
  box-shadow:inset 0 0 0 1px var(--edge), inset 0 1px 0 rgba(242,239,233,.07);
}

/* Every capture on this site is of a DEMONSTRATION firm: the people, matters and amounts in
   it are synthetic (docs/72 s4). The caption ships with the image, not as a page footnote,
   so a reader who only sees one screenshot still knows what they are looking at. */
.shot__cap,
.band__cap,
.ring__cap{
  position:relative; z-index:2;
  margin:10px 0 0;
  font-family:var(--font-text);
  font-size:12px; font-weight:400; letter-spacing:.02em; line-height:1.45;
  color:var(--bone-2);
}

/* ---------- the six cards ---------- */
/* Base mode is the static grid of docs/73 s7: 3 columns >=1100, 2 from 640, 1 below.
   The ring is opt-in, because an unrotated ring hides five of its six cards. */

.ring-clip{ margin-top:clamp(28px,4vw,52px); }

.ring{
  display:grid; gap:clamp(12px,1.6vw,20px);
  grid-template-columns:1fr;
}
@media (min-width:640px){ .ring{ grid-template-columns:repeat(2,1fr); } }
@media (min-width:1100px){ .ring{ grid-template-columns:repeat(3,1fr); } }

.card__face{
  display:flex; flex-direction:column; justify-content:flex-start; gap:12px;
  color:inherit; text-decoration:none;
  min-height:var(--card-h); padding:22px;
  border-radius:10px;
  background:var(--ink-lift);
  box-shadow:inset 0 0 0 1px var(--edge);
}

/* a real capture heads every card (docs/72 s4: demonstration firm, captioned under the ring).
   It bleeds to the card's edges and crops from the top-left, where each screen's title sits. */
.card__img{
  display:block; width:calc(100% + 44px); max-width:none;
  margin:-22px -22px 6px;
  aspect-ratio:16/10; height:auto;
  object-fit:cover; object-position:0 0;
  border-radius:10px 10px 0 0;
  background:var(--ink);
  box-shadow:0 1px 0 var(--edge);
}

.ring__cap{ margin-top:14px; }

.card__title{
  font-family:var(--font-display); font-weight:400;
  font-size:21px; letter-spacing:0; line-height:1.3;
}

.card__line{
  color:var(--bone-2); font-size:14px; line-height:1.5;
  flex:1 1 auto;
}

.card__link{
  align-self:flex-start;
  font-size:14px; letter-spacing:.01em; color:var(--volt-soft);
  transition:color 160ms var(--ease);
}
.card__link:hover{ color:var(--bone); }

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

.nav{
  position:sticky; top:0;
  height:var(--nav-h);
  background:none;
}

/* the scrim must paint ABOVE #main, so it cannot sit at z-index:-1 */
.nav::after{
  content:''; position:absolute; inset:0 0 auto; height:260%; z-index:0;
  background:linear-gradient(180deg,rgba(12,10,10,.97) 22%,rgba(12,10,10,.78) 52%,rgba(12,10,10,0));
  opacity:0; pointer-events:none;
  transition:opacity 320ms var(--ease);
}
.nav[data-scrolled]::after{ opacity:1; }

.nav__in{
  display:flex; align-items:center; gap:clamp(16px,2.4vw,34px);
  width:100%; max-width:var(--measure); margin-inline:auto;
  padding-inline:var(--gutter);
  height:100%;
  position:relative; z-index:1;
}

.nav__brand{
  font-family:var(--font-display); font-weight:400;
  font-size:19px; letter-spacing:.04em;
  color:var(--bone); margin-right:auto;
}

.nav__links{ display:flex; align-items:center; gap:clamp(16px,2vw,28px); }
.nav__links a{
  font-size:15px; font-weight:300; color:var(--bone-2);
  transition:color 160ms var(--ease);
}
.nav__links a:hover,.nav__links a[aria-current]{ color:var(--bone); }

.nav__cta{ display:flex; align-items:center; gap:8px; }

.nav__burger{
  display:none; position:relative;
  width:40px; height:40px; margin-right:-8px;
  cursor:pointer; border-radius:4px;
}
.nav__burger::before,.nav__burger::after{
  content:''; position:absolute; left:11px; right:11px; height:1px;
  background:var(--bone);
  transition:transform 220ms var(--ease);
}
.nav__burger::before{ top:17px; }
.nav__burger::after{ bottom:17px; }
.nav__burger[aria-expanded="true"]::before{ transform:translateY(3px) rotate(45deg); }
.nav__burger[aria-expanded="true"]::after{ transform:translateY(-3px) rotate(-45deg); }

/* ---------- mobile sheet ---------- */

.sheet{
  position:fixed; inset:0; z-index:60;
  display:grid; grid-template-rows:var(--nav-h) 1fr auto;
  background:var(--ink);
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity 240ms var(--ease), visibility 0s linear 240ms;
}
.sheet[hidden]{ display:none; }
/* value-specific: the JS parks a closed sheet at data-open="0", and a presence selector
   would leave it visible and pointer-active behind [hidden], with the fade never playing */
.sheet[data-open="1"]{
  opacity:1; visibility:visible; pointer-events:auto;
  transition:opacity 240ms var(--ease);
}

.sheet__head{
  display:flex; align-items:center; justify-content:space-between;
  padding-inline:var(--gutter);
}
.sheet__close{
  position:relative; width:40px; height:40px; margin-right:-8px;
  cursor:pointer; border-radius:4px;
}
.sheet__close::before,.sheet__close::after{
  content:''; position:absolute; left:11px; right:11px; top:19px;
  height:1px; background:var(--bone);
}
.sheet__close::before{ transform:rotate(45deg); }
.sheet__close::after{ transform:rotate(-45deg); }

.sheet__nav{
  display:flex; flex-direction:column; align-items:flex-start; gap:4px;
  padding:clamp(24px,7vh,64px) var(--gutter) 0;
  overflow-y:auto;
}
.sheet__link{
  font-family:var(--font-display); font-weight:300;
  font-size:clamp(26px,7vw,34px); letter-spacing:.015em; line-height:1.25;
  color:var(--bone); padding-block:8px;
}
.sheet__link:hover{ color:var(--volt-soft); }

.sheet__foot{
  display:flex; flex-wrap:wrap; gap:10px;
  padding:var(--gutter);
}
.sheet__foot .btn{ flex:1 1 160px; }

/* ---------- footer ---------- */

.foot{ padding-block:clamp(64px,8vw,112px) 32px; }

.foot__in,.foot__base{
  width:100%; max-width:var(--measure); margin-inline:auto;
  padding-inline:var(--gutter);
}

.foot__in{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(158px,1fr));
  gap:clamp(30px,4vw,56px);
}

.foot__col{ display:flex; flex-direction:column; align-items:flex-start; gap:12px; }
.foot__col a{
  font-size:14px; font-weight:300; color:var(--bone-2);
  transition:color 160ms var(--ease);
}
.foot__col a:hover{ color:var(--bone); }

.foot__base{
  display:flex; flex-wrap:wrap; justify-content:space-between; gap:12px 24px;
  margin-top:clamp(48px,7vw,88px);
  font-size:13px; color:var(--bone-2);
}
.foot__base a:hover{ color:var(--bone-2); }

/* ---------- the closing wordmark ---------- */
/* docs/75 s6. Literal type, never an image, and decorative repetition of the brand line
   above it - so aria-hidden, and the footer links sit OUTSIDE the band so their contrast
   never changes. Bounded: one band of its own, overflow clipped, no fullscreen layer, and
   the only animated thing is a clip position. kelabu.js fits --wm-size to the measured word
   and sweeps --wm on a timer while the band is on screen; with no JS, reduced motion, a hidden
   tab or quality tier 3 it stays put and the lit duplicate simply never appears. */

.wordmark{
  position:relative; overflow:hidden;
  pointer-events:none; user-select:none;
  width:100%;
  margin-top:clamp(24px,3.4vw,48px);
  height:calc(var(--wm-size) * .85);
}
.wordmark span{
  position:absolute; left:50%; bottom:0;
  transform:translateX(-50%);
  display:block; white-space:nowrap;
  font-family:var(--font-display); font-weight:300;
  font-size:var(--wm-size);
  letter-spacing:.025em; line-height:.85;
  text-transform:uppercase;
}
.wordmark__base{ color:var(--bone); opacity:.10; }
/* one 14%-wide diagonal band, swept left to right by --wm. The coordinator drives --wm from
   the shared CLOCK, not from scroll: a pass of 2.8s every 8s while the band is on screen, and
   at --wm 1.25 the band has left the final letter, so the rest of each period is dark. */
.wordmark__lit{
  color:var(--volt-soft);
  text-shadow:0 0 8px rgba(91,91,240,.55);
  -webkit-mask-image:linear-gradient(105deg,
    transparent calc(var(--wm) * 100% - 21%), #000 calc(var(--wm) * 100% - 17.5%),
    #000 calc(var(--wm) * 100% - 10.5%), transparent calc(var(--wm) * 100% - 7%));
  mask-image:linear-gradient(105deg,
    transparent calc(var(--wm) * 100% - 21%), #000 calc(var(--wm) * 100% - 17.5%),
    #000 calc(var(--wm) * 100% - 10.5%), transparent calc(var(--wm) * 100% - 7%));
}

/* ---------- narrow ---------- */

@media (max-width:900px){
  .nav__links{ display:none; }
  .nav__burger{ display:block; }
  :root{ --card-h:min(340px, calc((100svh - 64px) * 0.40)); --card-w:min(260px, 62vw, calc(var(--card-h) * 0.78)); }
}

@media (max-width:640px){
  .d1{ max-width:none; }
  .d2{ max-width:none; }
}

/* =====================================================================================
   CHOREOGRAPHED MODE — docs/73
   Requires all three: no motion preference, a tall enough viewport, and kelabu.js having
   measured the page and set data-choreo. Nothing below this line runs in base mode.
   ===================================================================================== */

@media (prefers-reduced-motion:no-preference) and (min-height:640px){

  /* a smooth scroll to a marker 9,000px away would animate the whole choreography */
  html[data-choreo="on"]{ scroll-behavior:auto; }

  /* the poster IS the quill, and the quill is gone by s=.100 — holding it to the close would
     lay a lit shaft across the closing headline whenever WebGL is unavailable */
  html[data-choreo="on"] #scene-poster{
    position:fixed; inset:0; height:auto;
    opacity:calc(1 - var(--dock));
    transition:none;
  }
  html[data-choreo="on"][data-scene-live] #scene-poster{ opacity:0; }

  /* S90: the stage holds for 3.5 viewports, then releases for the footer. Scroll travel is
     D = 3.5H + F, so track = D + H - N - F = 4.5H - N; kelabu.js maps the travel onto the
     docs/73 score (KNOT_Y), and the release point is a measured 3.5H, not a fraction. */
  html[data-choreo="on"] .story{
    height:calc(4.5 * var(--vh,100svh) - var(--nav-h));
  }

  html[data-choreo="on"] .stage{
    position:sticky; top:var(--nav-h);
    height:calc(var(--vh,100svh) - var(--nav-h));
  }

  html[data-choreo="on"] .moment{
    position:absolute; inset:0;
    width:auto; max-width:none; margin:0; padding:0;
    min-height:0; display:block;
  }

  /* offstage copy keeps its place in the tab order (docs/73 s5 moves focus to the marker
     instead), but an invisible link must never intercept a click meant for the live moment */
  html[data-choreo="on"] .moment[data-live="0"]{ pointer-events:none; }

  html[data-choreo="on"] .anchor{
    display:block; position:absolute; left:0;
    width:1px; height:1px;
    /* --at is in viewport heights of scroll travel, on kelabu.js's KNOT_Y scale */
    top:calc(var(--at) * var(--vh,100svh));
    scroll-margin-top:var(--nav-h);
  }

  html[data-choreo="on"] .pin{
    position:absolute;
    left:calc(var(--x) * 100%);
    top:calc(var(--y) * 100%);
    width:calc(var(--w) * 100%);
    transform:translate(var(--ax,0%),var(--ay,0%));
  }
  html[data-choreo="on"] .pin--c{ --ax:-50%; --ay:-50%; }

  /* ===== docs/75 s5: the object owns the centre =========================================
     ONE placement, measured in kelabu.js and published as lengths. The object's horizontal
     centre is the stage's at EVERY moment - the composition varies by where the copy sits,
     never by shuttling the model left and right. Two modes, chosen by kelabu.js from the
     viewport aspect and the width each side column would actually get:
       data-lay="cols"  - copy in a reserved side column, object between them
       data-lay="stack" - copy above the object's reserved row, supporting visual below   */

  html[data-choreo="on"] .pin--obj{
    left:50%; top:50%; width:var(--objb);
    --ax:-50%; --ay:-50%;
  }

  html[data-lay="cols"][data-choreo="on"] .pin--hero{
    left:var(--pad); top:50%; width:min(620px, calc(50% - var(--pad) - 24px));
    --ax:0%; --ay:-50%;
  }
  html[data-lay="cols"][data-choreo="on"] .pin--copy,
  html[data-lay="cols"][data-choreo="on"] .pin--vis{ top:50%; --ax:0%; --ay:-50%; }
  html[data-lay="cols"][data-choreo="on"] .pin--sl{ left:var(--pad); right:auto; width:var(--colw); }
  html[data-lay="cols"][data-choreo="on"] .pin--sr{ left:auto; right:var(--pad); width:var(--colw); }
  html[data-lay="cols"][data-choreo="on"] .pin--t{ top:28px; --ay:0%; }
  /* S90: the carousel heading sat at 28px, under the nav's scrim tail, and its first line read
     dimmed. It now centres on the lifted object (CAR_Y = .28 of the stage), level with it. */
  html[data-lay="cols"][data-choreo="on"] #m-carousel .pin--t{ top:calc(.28 * var(--uh)); --ay:-50%; }

  /* the hero is exempt from the reservation: the object is concealed until s=.045 and the
     hero copy has left by .050, so the first impression keeps its full measure */
  html[data-lay="stack"][data-choreo="on"] .pin--hero{
    left:var(--pad); right:var(--pad); width:auto; top:50%;
    --ax:0%; --ay:-50%;
  }
  /* bottom-anchored, not a fixed height: copy that grows pushes its own top up, and
     composesFit() sees that in offsetTop instead of it overflowing invisibly */
  html[data-lay="stack"][data-choreo="on"] .pin--copy{
    left:var(--pad); right:var(--pad); width:auto;
    top:auto; bottom:calc((var(--uh) + var(--objenv)) / 2 + 12px);
    --ax:0%; --ay:0%;
  }
  html[data-lay="stack"][data-choreo="on"] .pin--vis{
    left:var(--pad); right:var(--pad); width:auto;
    top:calc((var(--uh) + var(--objenv)) / 2 + 12px);
    --ax:0%; --ay:0%;
  }

  /* the carousel is the one moment whose object is not stage-centred: s5 lifts it so the
     ring owns everything beneath, and the card envelope starts below it plus clearance */
  html[data-choreo="on"] #m-carousel .pin--ring{
    left:var(--pad); right:var(--pad); width:auto;
    top:var(--cartop); height:calc(var(--uh) - var(--cartop) - 28px);
    --ax:0%; --ay:0%;
  }
  html[data-lay="stack"][data-choreo="on"] #m-carousel .pin--copy{
    bottom:calc(var(--uh) - var(--carobj) + 12px);
  }

  /* the product clip is 16:10; stacked, its own row is the ceiling, so the width
     follows the height that row can actually give it */
  html[data-lay="stack"][data-choreo="on"] #shot-matter{
    left:50%; right:auto; --ax:-50%;
    /* 40px of clearance plus 26px for ONE caption line and its 8px offset. The caption hangs out
       of flow, so this is what actually makes room for it; extentOf() still counts its REAL height
       so a narrow shot whose caption wraps to two lines drops the page to normal flow instead of
       overflowing. Reserving two lines here was the wrong trade -- it squeezed the shot to 158px
       at a 660px viewport, where the fallback page shows it at 813px. */
    width:min(calc(100% - 2 * var(--pad)), calc(((var(--uh) - var(--objenv)) / 2 - 66px) * 1.6));
  }

  /* docs/75 s3: the screenshot arrives through a soft vertical aperture, 12% -> 100% of its
     own width with a 12px feather, while the whole image settles 1.04 -> 1.00. A pure
     function of --enter, which S90 transitions on #shot-matter (.anim), so the aperture opens
     in time and inherits the eased value. */
  html[data-choreo="on"] #shot-matter .shot__img{
    --ap:calc(.12 + .88 * var(--enter));
    -webkit-mask-image:linear-gradient(90deg,
      transparent calc(50% - var(--ap) * 50% - 12px), #000 calc(50% - var(--ap) * 50%),
      #000 calc(50% + var(--ap) * 50%), transparent calc(50% + var(--ap) * 50% + 12px));
    mask-image:linear-gradient(90deg,
      transparent calc(50% - var(--ap) * 50% - 12px), #000 calc(50% - var(--ap) * 50%),
      #000 calc(50% + var(--ap) * 50%), transparent calc(50% + var(--ap) * 50% + 12px));
  }
  /* the rim and the glow describe a whole rectangle, so they may not paint until it is one */
  html[data-choreo="on"] #shot-matter::before,
  html[data-choreo="on"] #shot-matter::after{ opacity:var(--enter); }

  /* The caption rides under the capture without joining the pin's measured box: composesFit()
     drops the whole choreography if any pin grows past its row, and a caption line is enough
     to do that on a short laptop viewport. It is still directly under its own image. */
  html[data-choreo="on"] #shot-matter .shot__cap{
    position:absolute; left:0; right:0; top:100%;
    margin:8px 0 0;
  }

  /* The close is the one moment that does NOT reserve a column: its copy is centred OVER the
     object, which becomes its background. objBox() exempts it from the clearance test for exactly
     this reason. Same composition as /product's closing section, which the operator asked for. */
  html[data-choreo="on"] #m-close .pin--copy{
    left:var(--pad); right:var(--pad); width:auto; top:50%; bottom:auto;
    --ax:0%; --ay:-50%;
    display:grid; justify-items:center; text-align:center;
  }
  html[data-choreo="on"] #m-close .d1{ max-width:16ch; min-width:0; overflow-wrap:break-word; }

  /* a DISPLAY heading needs 3:1, so a half-strength scrim in the page's own ink is enough: the
     object still reads through the letters, which is the whole point of the composition */
  html[data-choreo="on"] #m-close .d1{ position:relative; }
  html[data-choreo="on"] #m-close .d1::before{
    content:''; position:absolute; z-index:-1; pointer-events:none;
    inset:-0.6em -1.2em;
    /* closest-side keeps the whole falloff inside the box: the old 70%/78% radii ran past its
       edges and the clip showed as a hard-edged dark rectangle across the object */
    background:radial-gradient(closest-side, rgba(12,10,10,.58) 55%, rgba(12,10,10,0) 100%);
  }

  html[data-choreo="on"] .pin .d1,
  html[data-choreo="on"] .pin .d2,
  html[data-choreo="on"] .pin .lede{ max-width:none; }

  /* docs/73 s5: opacity = enter x (1 - leave); translation = in x (1 - enter) + out x leave.
     S90 reverses s5's "no transition, ever": kelabu.js now writes --enter/--leave as 0/1
     STEPS when the score crosses a threshold, and these transitions play each step to the
     end in time. data-on="1" is an arrival (slow, after the departing copy has begun to go);
     data-on="0" a departure (quick). html[data-snap] lands a first paint, a resize or a jump
     on its poses with nothing animating. */
  html[data-choreo="on"] .anim,
  html[data-choreo="on"] .card{
    transition:--enter var(--t-out) var(--ease), --leave var(--t-out) var(--ease);
  }
  html[data-choreo="on"] .anim[data-on="1"],
  html[data-choreo="on"] .card[data-on="1"]{
    transition:--enter var(--t-in) var(--ease) var(--dl,140ms), --leave var(--t-in) var(--ease) var(--dl,140ms);
  }
  html[data-choreo="on"][data-snap] .anim,
  html[data-choreo="on"][data-snap] .card{ transition:none; }

  html[data-choreo="on"] .anim{
    --tx:calc(var(--in-x) * (1 - var(--enter)) + var(--out-x) * var(--leave));
    --ty:calc(var(--in-y) * (1 - var(--enter)) + var(--out-y) * var(--leave));
    --sc:calc(var(--in-s) + (1 - var(--in-s)) * var(--enter));
    --ry:calc(var(--in-ry) * (1 - var(--enter)));
    opacity:calc(var(--enter) * (1 - var(--leave)));
    transform:perspective(1200px)
              translate(calc(var(--ax,0%) + var(--tx) * 1px), calc(var(--ay,0%) + var(--ty) * 1px))
              scale(var(--sc))
              rotateY(calc(var(--ry) * 1deg));
  }

  html[data-choreo="on"] .knot__img{ width:100%; transition:none; }

  /* ⛑ nothing with opacity < 1, a mask, a filter or containment may sit on .ring-scene or
     .ring: any of those flattens the preserved 3D context. The clip is OUTSIDE the
     perspective, and only .card__face fades. */
  /* the clip bounds the ring HORIZONTALLY only; clipping y would cut a card whose text grew.
     Its height is the reserved ring row of docs/75 s5, set on the pin above. */
  html[data-choreo="on"] .ring-clip{ margin:0; overflow-x:clip; overflow-y:visible; }
  html[data-choreo="on"]{ --card-h:min(360px, calc(var(--uh) * .40)); }
  html[data-lay="stack"][data-choreo="on"]{ --card-w:min(260px, calc(var(--card-h) * .78)); }
  html[data-choreo="on"] .ring-scene{
    position:relative; height:100%;
    perspective:var(--ring-p); perspective-origin:50% 50%;
    transform-style:preserve-3d;
  }
  html[data-choreo="on"] .ring{
    position:absolute; inset:0; display:block; gap:0;
    transform-style:preserve-3d;
    transform:translateY(calc(var(--ring-r) * -0.1392))
              translateZ(calc(var(--ring-r) * -1)) rotateX(-8deg) rotateY(calc(var(--phi) * 1deg));
  }
  html[data-choreo="on"] .card{
    --in-y:64; --out-y:-40;
    --dl:calc(160ms + var(--i) * 70ms);
    position:absolute; left:50%; top:50%;
    width:var(--card-w); min-height:var(--card-h);
    transform-style:preserve-3d;
    backface-visibility:hidden;
    transform:translate(-50%,-50%)
              rotateY(calc(var(--i) * 60deg))
              translateZ(var(--ring-r))
              translateY(calc((var(--in-y) * (1 - var(--enter)) + var(--out-y) * var(--leave)) * 1px));
  }
  html[data-choreo="on"] .card__face{ opacity:calc(var(--enter) * (1 - var(--leave))); }
  /* the caption sits under the ring, inside its reserved row, without growing the pin */
  html[data-choreo="on"] .ring__cap{
    position:absolute; left:0; right:0; bottom:0; margin:0; text-align:center;
  }

}

/* ---------- reduced motion ---------- */

@media (prefers-reduced-motion:reduce){
  html{ scroll-behavior:auto; }
  *,*::before,*::after{
    animation-duration:1ms !important;
    animation-iteration-count:1 !important;
    transition-duration:1ms !important;
  }
}
