/* ═══════════════════════════════════════════════════════════════════════════
   assembl · the motion layer
   ───────────────────────────────────────────────────────────────────────────
   Companion to assembl-system.css. Every primitive here obeys the design
   constitution §11: motion communicates work, never entertainment. Nothing
   fades in; things ASSEMBLE. Nothing bounces, spins, or seeks attention.

   THE RULE THAT DECIDES EVERY ARGUMENT
   If a piece of motion cannot be described as "this is becoming more
   complete", it does not ship.

   USAGE — everything is a data attribute, no classes to remember:
     data-m="rise"        an element assembles into place on entry
     data-m="lines"       child lines assemble in sequence
     data-m="type"        a headline assembles word by word
     data-m="wipe"        a photograph resolves behind a moving edge
     data-m="count"       a number materialises (data-m-to="68" data-m-suffix="%")
     data-m="magnet"      a control leans toward the cursor
     data-m="depth"       a photograph holds depth against the pointer
     data-m-delay="120"   stagger, in ms
   Scrubbed stages use data-m-scrub on a .mStage (see assembl-motion.js).
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── the base state: present but unassembled ─────────────────────────────── */
[data-m]{--m-ease:cubic-bezier(.16,.84,.28,1)}

[data-m="rise"]{opacity:0;transform:translate3d(0,26px,0);
  transition:opacity .9s var(--m-ease),transform 1.05s var(--m-ease)}
[data-m="rise"].m-in{opacity:1;transform:none}

/* lines assemble from behind a mask — the sentence builds, it does not fade */
[data-m="lines"] .m-line{display:block;overflow:hidden}
[data-m="lines"] .m-line > *{display:block;transform:translate3d(0,110%,0);
  transition:transform 1.15s var(--m-ease)}
[data-m="lines"].m-in .m-line > *{transform:none}

/* type assembles word by word, each word rising out of its own mask */
[data-m="type"] .m-w{display:inline-block;overflow:hidden;vertical-align:bottom}
[data-m="type"] .m-w > i{display:inline-block;font-style:inherit;
  transform:translate3d(0,105%,0);transition:transform 1.1s var(--m-ease)}
[data-m="type"].m-in .m-w > i{transform:none}

/* a photograph resolves behind a moving edge — the image arrives, it does not
   appear. Scale settles at the same time so it reads as material, not slide. */
[data-m="wipe"]{position:relative;overflow:hidden}
[data-m="wipe"] > img,[data-m="wipe"] > video{transform:scale(1.08);
  transition:transform 1.8s var(--m-ease);will-change:transform}
[data-m="wipe"].m-in > img,[data-m="wipe"].m-in > video{transform:none}
[data-m="wipe"]::after{content:"";position:absolute;inset:0;background:var(--sys-paper,#FCFCFA);
  transform-origin:left;transition:transform 1.25s var(--m-ease)}
[data-m="wipe"].m-in::after{transform:scaleX(0);transform-origin:right}

/* numbers materialise — tabular so the layout never jumps while counting */
[data-m="count"]{font-variant-numeric:tabular-nums}

/* a control leans toward the pointer. 6px maximum: felt, not seen. */
[data-m="magnet"]{transition:transform .5s var(--m-ease);will-change:transform}

/* ── the scrubbed stage — a section that assembles as you scroll through it ──
   The stage pins; the work inside advances with scroll position. This is the
   wait state made scrollable: nothing autoplays, the reader drives it. */
.mStage{position:relative}
.mStage__pin{position:sticky;top:0;height:100vh;display:flex;align-items:center;
  justify-content:center;overflow:hidden}
.mStage__steps{position:absolute;inset:0;pointer-events:none}
.mStage__step{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
  opacity:0;transition:opacity .5s var(--m-ease)}
.mStage__step.on{opacity:1}
/* the progress hairline — the only chrome the stage is allowed */
.mStage__rail{position:absolute;left:0;right:0;bottom:0;height:1px;
  background:rgba(20,22,26,.1)}
.mStage__fill{height:100%;width:0;background:var(--sysA,#2F4F44);
  transition:width .18s linear}

/* ── the assembly grid — pieces converging into a whole ──────────────────── */
.mGrid{display:grid;gap:2px}
.mGrid__cell{background:var(--sysA,#2F4F44);opacity:0;transform:scale(.86);
  transition:opacity .7s var(--m-ease),transform .7s var(--m-ease)}
.mGrid.m-in .mGrid__cell{opacity:1;transform:none}

/* ── the cursor, for pointer devices only ────────────────────────────────── */
@media(hover:hover) and (pointer:fine){
  .mCursor{position:fixed;top:0;left:0;width:34px;height:34px;border-radius:50%;
    border:1px solid rgba(20,22,26,.45);pointer-events:none;z-index:9999;
    transform:translate3d(-100px,-100px,0);
    transition:width .35s var(--m-ease),height .35s var(--m-ease),
               background .35s var(--m-ease),border-color .35s var(--m-ease);
    mix-blend-mode:difference;filter:invert(1)}
  .mCursor.on{width:66px;height:66px;background:rgba(255,255,255,.08);
    border-color:rgba(20,22,26,.9)}
  .mCursor__l{position:absolute;inset:0;display:grid;place-items:center;
    font:700 8.5px/1 -apple-system,system-ui,sans-serif;letter-spacing:.14em;
    text-transform:uppercase;opacity:0;transition:opacity .3s}
  .mCursor.on .mCursor__l{opacity:1}
  body.m-cursor{cursor:none}
  body.m-cursor a,body.m-cursor button,body.m-cursor [data-m-cursor]{cursor:none}
}

/* ── canvas hosts sit behind content and never intercept the pointer unless
   they are the interaction ─────────────────────────────────────────────── */
.mCanvas{position:absolute;inset:0;width:100%;height:100%;display:block}
.mCanvas--interactive{pointer-events:auto;cursor:grab}
.mCanvas--interactive:active{cursor:grabbing}

/* ── honour the setting. Everything above degrades to its finished state. ── */
@media(prefers-reduced-motion:reduce){
  [data-m]{transition:none!important}
  [data-m="rise"]{opacity:1;transform:none}
  [data-m="lines"] .m-line > *,[data-m="type"] .m-w > i{transform:none}
  [data-m="wipe"]::after{display:none}
  [data-m="wipe"] > img{transform:none}
  .mGrid__cell{opacity:1;transform:none}
  .mCursor{display:none}
  body.m-cursor{cursor:auto}
}

/* ═══════════════════════════════════════════════════════════════════════════
   PART TWO — the Swiss layer. Kate's brief, 1 August 2026:
   cream ground, black type, massive editorial scale, CSS 3D perspective,
   grain, cursor-reactive. No dependency, works on every device.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── grain: the texture that stops flat cream reading as "unfinished" ────── */
.mGrain::after{content:"";position:fixed;inset:0;z-index:9998;pointer-events:none;
  opacity:.055;mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E")}
@media(prefers-reduced-motion:reduce){.mGrain::after{opacity:.03}}

/* ── the 3D card — perspective that answers the pointer ─────────────────── */
.m3d{perspective:1400px;perspective-origin:50% 50%}
.m3d__card{position:relative;transform-style:preserve-3d;
  transition:transform .9s cubic-bezier(.16,.84,.28,1);will-change:transform}
.m3d__card > *{transform:translateZ(0)}
/* depth layers — anything inside can be pushed forward */
.m3d__lift-1{transform:translateZ(28px)}
.m3d__lift-2{transform:translateZ(56px)}
.m3d__lift-3{transform:translateZ(90px)}

/* ── the perspective gallery — cards standing in space, not a flat grid ── */
.m3dGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));
  gap:clamp(18px,2.4vw,34px);perspective:1800px}
.m3dGrid__item{transform-style:preserve-3d;
  transition:transform .85s cubic-bezier(.16,.84,.28,1),box-shadow .85s cubic-bezier(.16,.84,.28,1);
  will-change:transform;background:#fff;border-radius:20px;overflow:hidden;
  border:1px solid var(--sys-line,rgba(20,22,26,.1));
  box-shadow:0 24px 60px -44px rgba(20,22,26,.5)}
@media(hover:hover) and (pointer:fine){
  .m3dGrid__item:hover{transform:translateZ(38px) rotateX(2.5deg);
    box-shadow:0 60px 120px -60px rgba(20,22,26,.55)}
}
.m3dGrid__item img{width:100%;aspect-ratio:4/3;object-fit:cover;display:block}
.m3dGrid__body{padding:24px 26px 28px}
.m3dGrid__k{font:700 10px/1 var(--sys-sans,system-ui);letter-spacing:.24em;text-transform:uppercase;
  color:var(--sysA,#2F4F44);margin-bottom:12px}
.m3dGrid__t{font-family:var(--sys-display,Georgia,serif);font-weight:400;font-size:26px;
  line-height:1.12;letter-spacing:-.015em;color:var(--sys-ink,#14161A)}
.m3dGrid__p{font-size:14px;line-height:1.6;font-weight:300;color:var(--sys-body,#4A4F57);margin-top:12px}

/* ── the floating stack — cards suspended at different depths ───────────── */
.mStack{position:relative;transform-style:preserve-3d;perspective:1600px}
.mStack__c{position:absolute;border-radius:18px;background:#fff;
  border:1px solid var(--sys-line,rgba(20,22,26,.1));
  box-shadow:0 40px 90px -55px rgba(20,22,26,.55);
  transition:transform 1.2s cubic-bezier(.16,.84,.28,1)}

/* ── display type, Swiss scale — the one thing on the screen ────────────── */
.mDisplay{font-family:var(--sys-display,Georgia,serif);font-weight:400;
  font-size:clamp(52px,11.5vw,190px);line-height:.9;letter-spacing:-.035em;
  color:var(--sys-ink,#14161A);margin:0}
.mDisplay em{font-style:italic}
.mDisplay--sans{font-family:var(--sys-sans,system-ui);font-weight:700;letter-spacing:-.045em}
/* the hairline label that sits above Swiss display type */
.mLabel{font:700 10.5px/1 var(--sys-sans,system-ui);letter-spacing:.3em;text-transform:uppercase;
  color:var(--sysA,#2F4F44);display:flex;align-items:center;gap:14px}
.mLabel::after{content:"";flex:1;height:1px;background:var(--sys-line,rgba(20,22,26,.14));max-width:180px}

/* ── the ruled band — Swiss structure, a line that means a section ──────── */
.mRule{height:1px;background:var(--sys-line,rgba(20,22,26,.12));width:100%}
.mColumns{display:grid;grid-template-columns:repeat(12,1fr);gap:clamp(14px,1.6vw,26px)}
@media(max-width:820px){.mColumns{grid-template-columns:repeat(4,1fr)}}
