/* ============================================================================
   GEMMEIN — the film

   Six chapters, one object. The object is Gemmein's own register — the same
   instrument the detail page shows — and it is built once and never torn down.
   Its body swaps between chapters (a request resolving, the five layers, the
   first real customer, what it wrote down) and it turns over once to show the
   console on its back. The frame never changes.

   JS owns the numbers. It writes a camera (--cx/--cy/--cs/--tilt/--room)
   interpolated between per-chapter positions, plus one 0→1 value per view.
   CSS only renders them. Nothing here animates on a timer, so scrubbing
   backwards is exactly as correct as scrubbing forwards.
   ========================================================================= */

/* The scroll container is the document, so snap has to live here — on body it
   is silently ignored, which is why mobile never settled onto a chapter.
   Proximity, never mandatory: the page eases into compositions instead of
   capturing the wheel, and a fast flick still runs the whole story. */
/* Mandatory, not proximity. Every chapter is exactly one viewport, so nothing
   can be trapped — and resting between two chapters shows a frame that was
   never composed to be looked at. The footer is a snap target too, otherwise
   mandatory snap makes the bottom of the page hard to reach. */
html {
  scroll-snap-type: y mandatory;
  /* styles.css sets scroll-padding-top for index.html's sticky nav (88px).
     Inherited here it shortens the snapport, so every chapter snapped 88px
     early and no composition ever framed cleanly. This page's header floats
     over the scene and reserves nothing. */
  scroll-padding-top: 0;
  /* Keeps pull-to-refresh and rubber-band bounce from fighting the snap at the
     first and last chapter, where a mandatory deck feels worst without it. */
  overscroll-behavior-y: contain;
}
.ftr { scroll-snap-align: start; }

.film-body { background: var(--paper); }

.film-body .skip { z-index: 90; }

/* ── Persistent chrome ────────────────────────────────────────────────────── */

.film-hdr {
  position: fixed; inset: 0 0 auto 0; z-index: 60;
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px;
  padding: 14px clamp(16px, 3.4vw, 34px);
  pointer-events: none;
}
.film-hdr > * { pointer-events: auto; }
.film-hdr .brand { display: inline-flex; align-items: center; gap: 9px; font-variation-settings: "wght" 620; letter-spacing: -0.03em; color: var(--ink); }
.film-hdr .brand svg { width: 23px; height: 23px; }

/* On the dark chapters the chrome has to invert with the world. */
.film-body.is-deep .film-hdr .brand { color: #fff; }
.film-hdr .brand, .film-hdr .film-cta { transition: color 300ms var(--ease); }

/* The film's chrome retires once the footer takes the frame. The footer is the
   one part of this page that is a website — it has its own brand and its own
   call to action — and a floating header sitting on top of both read as two
   pages stacked. Scrolling back up returns it. */
.film-hdr { transition: opacity 260ms var(--ease), translate 380ms var(--ease-out); }
.film-body.is-parked .film-hdr { opacity: 0; translate: 0 -120%; }
.film-body.is-parked .film-hdr > * { pointer-events: none; }

.chapter-nav {
  position: fixed; z-index: 60;
  top: 50%; right: clamp(14px, 2vw, 26px); translate: 0 -50%;
  pointer-events: none;
}
.chapter-nav ol { list-style: none; display: flex; flex-direction: column; gap: 2px; }
/* row-reverse so the marks sit flush against the right edge and the labels
   grow leftward from them. Laid out forwards, every hidden label still takes
   its width, so each mark landed wherever its own label left it and the whole
   column was ragged. */
.chapter-nav a {
  pointer-events: auto;
  display: flex; flex-direction: row-reverse;
  align-items: center; justify-content: flex-start; gap: 10px;
  padding: 5px 2px;
  font-size: 11.5px; letter-spacing: 0.04em; color: var(--faint);
  transition: color 220ms var(--ease);
}
.chapter-nav a span {
  white-space: nowrap;
  opacity: 0; translate: -8px 0;
  transition: opacity 220ms var(--ease), translate 220ms var(--ease-out);
}
.chapter-nav a i {
  width: 16px; height: 1.5px; flex: none;
  background: currentColor; opacity: 0.4;
  transition: width 260ms var(--ease-out), opacity 220ms var(--ease);
}
.chapter-nav a:hover span,
.chapter-nav a:focus-visible span { opacity: 1; translate: 0 0; }
.chapter-nav a:hover, .chapter-nav a:focus-visible { color: var(--ink); }
.chapter-nav a[aria-current="true"] { color: var(--seal); }
.chapter-nav a[aria-current="true"] i { width: 28px; opacity: 1; }
.chapter-nav a[aria-current="true"] span { opacity: 1; translate: 0 0; }

.film-body.is-deep .chapter-nav a { color: rgba(232, 239, 234, 0.5); }
.film-body.is-deep .chapter-nav a:hover,
.film-body.is-deep .chapter-nav a:focus-visible { color: #fff; }
.film-body.is-deep .chapter-nav a[aria-current="true"] { color: var(--mint); }

.chapter-nav { transition: opacity 260ms var(--ease), translate 380ms var(--ease-out); }
.film-body.is-parked .chapter-nav { opacity: 0; translate: 18px -50%; }
.film-body.is-parked .chapter-nav a { pointer-events: none; }

@media (max-width: 1080px) { .chapter-nav { display: none; } }

/* ── Film + stage ─────────────────────────────────────────────────────────── */

.film { position: relative; }

/* dvh, not svh. svh is the SMALLEST viewport — the frame with browser chrome
   expanded — so once the chrome retracts every chapter is shorter than the
   screen and each snap point shows one chapter plus a sliver of the next. You
   can never see a single composed state, which is the whole point of snapping.
   dvh matches what is actually visible. The cost is a resize when chrome
   shows or hides; the engine reads the film's height every frame, so it
   self-corrects rather than drifting. */
/* ⚠️ svh, NOT dvh. `dvh` is the DYNAMIC viewport — it shrinks and grows as the
   mobile URL bar collapses and expands. For a scroll-linked film that is
   exactly the wrong unit: every URL-bar movement re-lays-out the whole film,
   which moves the very thing progress is measured against. Chapter heights,
   the stage and the -100_vh pull all shifted mid-scroll, so `p` jumped, the
   register's views cross-faded at random (content appearing and disappearing)
   and the panel teleported over the tool chips. `svh` is the SMALL viewport —
   stable while the URL bar moves — so the film's geometry holds still and the
   only thing that changes during a scroll is the scroll position. */
.stage {
  position: sticky; top: 0; z-index: 0;
  height: 100vh; height: 100svh;
  overflow: hidden;
  /* Interpolated by the engine, not transitioned — see story.js. A transition
     here would lag the scroll and fight scrubbing. */
  background: var(--surface, var(--paper));
}

.stage-weave {
  position: absolute; inset: -22%;
  background-color: var(--weave, rgba(15, 122, 90, 0.058));
  --oct: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='72' height='72' viewBox='0 0 72 72'%3E%3Cpolygon points='21,0 51,0 72,21 72,51 51,72 21,72 0,51 0,21' fill='none' stroke='%23000' stroke-width='1'/%3E%3C/svg%3E");
  mask-image: var(--oct); -webkit-mask-image: var(--oct);
  mask-size: 72px 72px; -webkit-mask-size: 72px 72px;
  /* Fractions of the camera, so the lattice parallaxes behind the object. */
  translate: calc(var(--cx, 0) * 0.09px) calc(var(--cy, 0) * 0.5px);
  transition: background-color 500ms var(--ease);
}
.film-body.is-deep .stage-weave { --weave: rgba(95, 208, 173, 0.1); }

/* The light in the room, and its only source: the system itself. */
.stage-light {
  position: absolute; inset: 0;
  background:
    radial-gradient(52% 48% at 72% 34%, rgba(201, 168, 106, calc(0.3 * (1 - var(--room, 0)))), transparent 70%),
    radial-gradient(46% 44% at 58% 60%, rgba(19, 163, 126, calc(0.16 + 0.34 * var(--room, 0))), transparent 72%);
  filter: blur(38px);
  transition: opacity 400ms var(--ease);
}

.chapters { position: relative; z-index: 2; margin-top: -100vh; margin-top: -100svh; }

.chapter {
  position: relative;
  /* Capped, or on an ultra-wide screen the copy flies to the edge and opens a
     canyon between it and the object — which is centred on the viewport. */
  max-width: 1560px; margin-inline: auto; width: 100%;
  min-height: 100vh; min-height: 100svh;
  scroll-snap-align: start;
  /* Desktop keeps `normal`, so a fast flick can run the whole page in one
     gesture. A phone is where you actually need one screen per state. */
  scroll-snap-stop: normal;
  display: grid;
  padding: clamp(84px, 11vh, 132px) clamp(20px, 5vw, 72px) clamp(40px, 7vh, 96px);
}

/* Room for the chapter nav. 168px was more than the nav needs and it was the
   difference between fitting and colliding at the bottom of the range; the
   hover labels expand leftward over the copy's margin, which is fine. */
@media (min-width: 1081px) { .chapter { padding-right: 112px; } }

/* ── Chapter copy ─────────────────────────────────────────────────────────── */

/* Composition varies per chapter on purpose — the alternative is eight
   identically placed text blocks, which is the template this page exists to
   get away from. */
.copy {
  position: relative; z-index: 2;
  display: flex; flex-direction: column; align-items: flex-start; gap: 16px;
  width: min(100%, 30rem);
  transition: opacity 340ms var(--ease), translate 420ms var(--ease-out);
}
/* Only hide it if the engine is actually going to reveal it. Without the `js`
   guard a failed script leaves the whole page blank — the text is in the DOM
   for a crawler, but nobody can read it. */
.js .copy { opacity: 0; translate: 0 14px; }
.chapter.is-live .copy { opacity: 1; translate: 0 0; }

.copy[data-pos="left"] { justify-self: start; align-self: center; }
.copy[data-pos="left-low"] { justify-self: start; align-self: end; }
/* Right-POSITIONED, not right-aligned. A three-line display headline can hang
   right and look deliberate, but a four-line paragraph reads badly ragged-left
   and a two-column list cannot be right-aligned at all — so a chapter with all
   three in it ends up with the heading flush right and the list flush left,
   which is what broke. The column's position carries the alternation on its
   own; nothing inside it needs to flip. */
.copy[data-pos="right"] { justify-self: end; align-self: center; }
.copy[data-pos="right-low"] { justify-self: end; align-self: end; }
.copy[data-pos="centre"] {
  justify-self: center; align-self: start;
  padding-top: clamp(8px, 4vh, 48px);
  text-align: center; align-items: center;
  width: min(100%, 40rem);
}


.ch-no {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--faint);
}
.copy h1, .copy h2 {
  font-size: clamp(2.125rem, 1.3rem + 3.4vw, 3.75rem);
  line-height: 1.02; letter-spacing: -0.042em;
  font-variation-settings: "wght" 640;
  color: var(--ink); text-wrap: balance;
}
.copy-lead h1 { font-size: clamp(2.5rem, 1.5rem + 4.2vw, 4.5rem); }
.ch-say { font-size: var(--t-lg); line-height: 1.5; color: var(--muted); max-width: 34ch; }
.ch-fine { font-size: var(--t-sm); color: var(--faint); }

.copy-end .cmd { margin-top: 6px; }
.copy-end .row { justify-content: center; }

.ch-scroll {
  margin-top: 12px;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--faint);
}
.ch-scroll span { position: relative; padding-bottom: 18px; display: inline-block; }
.ch-scroll span::after {
  content: ""; position: absolute; left: 0; bottom: 0; width: 1px; height: 13px;
  background: currentColor; transform-origin: top;
  animation: drop 2.2s var(--ease) infinite;
}
@keyframes drop {
  0%, 100% { transform: scaleY(0.25); opacity: 0.3; }
  45% { transform: scaleY(1); opacity: 1; }
}

/* Supporting lists. Never cards — they sit in the type column as part of the
   sentence, so the chapter keeps one focal object. */
.ch-tools { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.ch-tools span {
  padding: 4px 10px; border-radius: 6px;
  border: 1px solid var(--line); background: var(--card);
  font-size: 12px; color: var(--muted);
}
.ch-tools em { font-style: normal; font-size: 12.5px; color: var(--faint); }

.ch-layers { list-style: none; display: flex; flex-direction: column; gap: 7px; margin-top: 2px; }
.ch-layers li { font-size: var(--t-sm); color: var(--muted); line-height: 1.4; }
.ch-layers b {
  display: inline-block; min-width: 82px;
  color: var(--ink); font-variation-settings: "wght" 620;
}

.ch-plans { list-style: none; display: flex; flex-direction: column; gap: 9px; margin-top: 2px; }
.ch-plans li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: var(--t-sm); }
.ch-plans b { min-width: 66px; color: var(--ink); font-variation-settings: "wght" 640; }
.ch-plans span { font-family: var(--mono); font-size: 12px; color: var(--seal); }
.ch-plans em { font-style: normal; color: var(--faint); }

.film-body.is-deep .ch-layers b,
.film-body.is-deep .ch-plans b { color: #fff; }
.film-body.is-deep .ch-layers li,
.film-body.is-deep .ch-tools em,
.film-body.is-deep .ch-plans em { color: #9fb0a6; }
.film-body.is-deep .ch-tools span {
  border-color: rgba(232, 239, 234, 0.16);
  background: rgba(232, 239, 234, 0.05); color: #b6c5bc;
}

.h1-lead {
  display: block; margin-bottom: 0.18em;
  font-size: 0.52em; letter-spacing: -0.035em;
  font-variation-settings: "wght" 600;
  color: var(--ink-2);
}

.ch-say-2 { font-size: var(--t-sm); color: var(--faint); }
.ch-link { color: var(--seal); text-decoration: underline; text-underline-offset: 3px; }

.ch-qs { list-style: none; display: flex; flex-direction: column; gap: 9px; }
.ch-qs li {
  display: flex; flex-direction: column; gap: 2px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--line);
}
.ch-qs b {
  font-size: var(--t-sm); color: var(--ink);
  font-variation-settings: "wght" 580; letter-spacing: -0.018em;
}
.ch-qs span { font-size: var(--t-sm); color: var(--muted); }
.film-body.is-deep .ch-qs b { color: #fff; }
.film-body.is-deep .ch-qs span { color: #9fb0a6; }
.film-body.is-deep .ch-qs li { border-color: rgba(232, 239, 234, 0.12); }
.film-body.is-deep .h1-lead { color: #cfdcd4; }

/* Deep chapters invert the type with the room. */
.film-body.is-deep .copy h1,
.film-body.is-deep .copy h2 { color: #fff; }
.film-body.is-deep .ch-say { color: #a9bab0; }
.film-body.is-deep .ch-no,
.film-body.is-deep .ch-fine { color: #8b9a91; }
.copy h1, .copy h2, .ch-say, .ch-no, .ch-fine { transition: color 500ms var(--ease); }

/* ── The world ────────────────────────────────────────────────────────────── */

.world {
  position: absolute; top: 50%; left: 50%;
  /* 36vw, not 88vw. The copy column is a fixed 30rem and the chapter reserves
     112px on the right for the nav, so anything wider than this collides with
     the paragraph at the bottom of the desktop range. */
  width: min(36vw, 620px);
  translate: -50% -50%;
  transform-style: preserve-3d;
  perspective: 1500px;
  /* The camera. JS interpolates these between per-chapter positions, so the
     move between any two chapters is one continuous path. cx is in pixels —
     see the note on CAMERA in story.js for why neither % nor vw works. */
  transform:
    translate3d(calc(var(--cx, 0) * 1px), calc(var(--cy, 0) * 1%), 0)
    scale(var(--cs, 1));
  opacity: var(--scene-o, 1);
  will-change: transform;
  transition: opacity 400ms var(--ease);
}

/* ── The application surface ──────────────────────────────────────────────── */

/* Two faces of one object. The light side is what customers see; the dark side
   is the console you work it from. Turning it over is the whole of chapter
   eight — a new panel sliding in would have broken the continuity the page is
   built on. */
.app {
  position: relative; z-index: 3;
  box-shadow: 0 2px 6px rgba(11, 19, 16, 0.05), 0 30px 70px rgba(11, 19, 16, 0.12);
  border-radius: 16px;
  transform-style: preserve-3d;
  transform:
    rotateX(calc(var(--tilt, 0) * 34deg))
    rotateY(calc(var(--flip, 0) * 180deg));
  transition: box-shadow 400ms var(--ease);
}
/* In the dark room the panel is the only lit thing, so it gets a green cast
   under it rather than a black shadow that cannot be seen. */
.film-body.is-deep .app {
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4), 0 34px 90px rgba(19, 163, 126, 0.22);
}

.app-face {
  border-radius: 16px;
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

/* The register's own frame, lit from the same direction as the room — the
   detail page's `.rec`, rebuilt here so the film and the site are visibly the
   same instrument. */
.app-front {
  position: relative;
  border: 1px solid var(--line-2);
  background:
    linear-gradient(158deg, rgba(19, 163, 126, 0.15), rgba(201, 168, 106, 0.05) 40%, rgba(255, 255, 255, 0) 66%),
    var(--card);
}
.app-front::after {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 3px; z-index: 2;
  background: var(--ribbon-line);
}

/* The operator's side, in the dark world's material. */
.app-back {
  position: absolute; inset: 0;
  transform: rotateY(180deg);
  border: 1px solid rgba(232, 239, 234, 0.16);
  background: linear-gradient(168deg, #0d1b16, #08120e);
}

/* ── The console: the operator's side ─────────────────────────────────────── */

/* Modelled on the real Customers page, and deliberately not enriched. No
   invented sidebar, no revenue tiles, and NO REFUND CONTROL — money moves
   through the founder's own Stripe and Gemmein never touches it, so a refund
   button here would contradict the product rather than merely overstate it.
   The real actions are Details, Suspend, Sign out and Delete. */
.app-back { display: flex; flex-direction: column; }

.cns-bar {
  display: flex; align-items: center; gap: 9px;
  padding: 11px 15px;
  border-bottom: 1px solid rgba(232, 239, 234, 0.1);
  flex: none;
}
.cns-gem { width: 15px; height: 15px; flex: none; }
.cns-title { font-size: var(--t-sm); font-variation-settings: "wght" 620; letter-spacing: -0.02em; color: #e8efea; }
.cns-live {
  display: flex; align-items: center; gap: 6px;
  margin-left: auto;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--mint);
}
.cns-live i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

.cns-page { display: flex; flex-direction: column; min-height: 0; }

.cns-head { display: flex; flex-direction: column; gap: 3px; padding: 13px 15px 10px; }
.cns-head b {
  font-size: var(--t-sm); letter-spacing: -0.024em; color: #e8efea;
  font-variation-settings: "wght" 620;
}
.cns-head span { font-size: 11px; line-height: 1.4; color: #7d8c83; }

.cns-tools {
  display: flex; align-items: center; gap: 10px;
  padding: 0 15px 11px;
}
.cns-find {
  flex: 1; min-width: 0;
  padding: 6px 11px; border-radius: 999px;
  border: 1px solid rgba(232, 239, 234, 0.14);
  font-size: 11px; color: #6f8378;
}
.cns-count { flex: none; font-family: var(--mono); font-size: 10px; color: #6f8378; }

.cns-tbl { list-style: none; min-width: 0; }
.cns-tbl > li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto auto;
  align-items: center; gap: 8px;
  padding: 9px 15px;
  border-top: 1px solid rgba(232, 239, 234, 0.07);
}

.cns-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cns-who b {
  font-size: 11.5px; letter-spacing: -0.014em; color: #e8efea;
  font-variation-settings: "wght" 580;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cns-who em {
  font-style: normal; font-family: var(--mono); font-size: 9.5px; color: #6f8378;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.cns-more { font-family: var(--mono); font-size: 10px; color: var(--mint); }

.cns-act {
  padding: 4px 9px; border-radius: 7px;
  border: 1px solid rgba(232, 239, 234, 0.16);
  background: rgba(232, 239, 234, 0.05);
  font-size: 10.5px; letter-spacing: -0.01em; color: #cfdcd4;
}
/* Tertiary in the real thing, and it should stay that way — it is the one
   action here that cannot be undone. */
.cns-act-no {
  border-color: transparent; background: none; color: #6f8378;
}

/* ── The pocket cards ─────────────────────────────────────────────────────── */

/* Only where the panel does not fit. See the note in story.html. */
.pkt { display: none; }

@media (max-width: 1080px) {
  .pkt {
    position: absolute; z-index: 4;
    left: 50%; bottom: 4%;
    width: min(88vw, 420px);
    translate: -50% 0;
    display: grid;
    pointer-events: none;
    perspective: 1200px;
  }
  /* Same turn as the register: these ARE the object on these chapters, so they
     tip away at the hand-over, catch the same band of light and cast the same
     deepening shadow. A card that only cross-faded next to one that turns
     would read as two different pages. */
  .pkt-card {
    grid-area: 1 / 1; align-self: end;
    position: relative; overflow: hidden;
    transform:
      rotateX(calc(var(--beat, 0) * -52deg))
      scale(calc(1 - var(--beat, 0) * 0.035));
    padding: 13px 14px 12px;
    border-radius: 14px;
    border: 1px solid var(--line-2);
    background:
      linear-gradient(158deg, rgba(19, 163, 126, 0.14), rgba(201, 168, 106, 0.05) 42%, rgba(255, 255, 255, 0) 68%),
      var(--card);
    box-shadow:
      0 2px 6px rgba(11, 19, 16, 0.05),
      0 calc(16px + var(--beat, 0) * 34px)
        calc(40px + var(--beat, 0) * 40px)
        rgba(11, 19, 16, calc(0.09 + var(--beat, 0) * 0.1));
  }
  .pkt-card::before {
    content: ""; position: absolute; inset: -20% -40%; z-index: 3;
    pointer-events: none;
    background: linear-gradient(102deg,
      transparent 40%,
      rgba(255, 255, 255, 0.72) 50%,
      transparent 60%);
    opacity: calc(var(--beat, 0) * 0.85);
    translate: calc((var(--sweep, 0) - 0.5) * 150%) 0;
  }
  .pkt-card::after {
    content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
    background: var(--ribbon-line);
  }
  .pkt-card[data-p="pay"] { opacity: var(--v-first, 0);  translate: 0 calc((1 - var(--v-first, 0)) * 16px); }
  .pkt-card[data-p="ask"] { opacity: var(--v-record, 0); translate: 0 calc((1 - var(--v-record, 0)) * 16px); }

  .pkt-hd {
    display: block; margin-bottom: 10px;
    font-size: var(--t-sm); letter-spacing: -0.024em; color: var(--ink);
    font-variation-settings: "wght" 620;
  }
  /* The register's own grammar at a smaller size: a name on the left, its
     state on the right, in mono. */
  .pkt-rows { list-style: none; }
  .pkt-rows li {
    display: flex; align-items: baseline; gap: 12px;
    padding: 6px 0;
    border-top: 1px solid var(--line);
  }
  .pkt-rows li:first-child { border-top: 0; padding-top: 0; }
  .pkt-rows b {
    font-size: 12.5px; letter-spacing: -0.018em; color: var(--ink);
    font-variation-settings: "wght" 560;
  }
  .pkt-rows span {
    margin-left: auto; flex: none;
    font-family: var(--mono); font-size: 11px; letter-spacing: -0.02em;
    color: var(--seal);
  }

  .pkt-note { font-size: 12.5px; line-height: 1.45; color: var(--muted); }
  .pkt-foot {
    margin-top: 10px; padding-top: 9px;
    border-top: 1px solid var(--line);
    font-size: 11.5px; line-height: 1.4; color: var(--faint);
  }
}

/* ── The register's swapping body ─────────────────────────────────────────── */

/* Every view sits in the same grid cell, so the frame is as tall as its
   tallest state and never resizes while one view hands over to the next. A
   panel that grows and shrinks between chapters reads as five different
   panels, which is exactly what this page is trying not to be. */
.reg-body { display: grid; }
.reg-view {
  grid-area: 1 / 1;
  align-self: stretch;
  display: flex; flex-direction: column;
  opacity: 0;
  pointer-events: none;
}
/* Each view rises into place rather than appearing. The offset is derived from
   the view's own progress, so it is scrubbable in both directions like
   everything else here — no transition, no timer. */
.reg-view[data-view="request"] { opacity: var(--v-request, 1); translate: 0 calc((1 - var(--v-request, 1)) * 18px); }
.reg-view[data-view="uses"]    { opacity: var(--v-uses, 0);    translate: 0 calc((1 - var(--v-uses, 0)) * 18px); }
.reg-view[data-view="pillars"] { opacity: var(--v-pillars, 0); translate: 0 calc((1 - var(--v-pillars, 0)) * 18px); }
.reg-view[data-view="first"]   { opacity: var(--v-first, 0);   translate: 0 calc((1 - var(--v-first, 0)) * 18px); }
.reg-view[data-view="record"]  { opacity: var(--v-record, 0);  translate: 0 calc((1 - var(--v-record, 0)) * 18px); }

/* The detail page reveals these one at a time as the register runs. Here the
   panel is a composed still, so they are simply present. */
.reg-view .rec-checks li { opacity: 1; transform: none; }
.reg-view .rec-verdict { margin-top: auto; }

.reg-lead {
  padding: 14px 18px 10px;
  font-size: var(--t-sm); color: var(--faint);
}

/* ── ② The range ──────────────────────────────────────────────────────────── */

/* Rows, and deliberately plain. These are five unalike products, and the only
   thing they may look like they have in common is the shape of the row they
   are printed in — so the row does not decorate them. The rule is set in mono
   because it is the thing you literally say to Gemmein. */
.reg-uses { list-style: none; padding: 0 18px 4px; }
.reg-uses li {
  display: flex; flex-direction: column; gap: 2px;
  padding: 9px 0;
  border-top: 1px solid var(--line);
}
.reg-uses li:first-child { border-top: 0; }
.reg-uses b {
  font-size: var(--t-sm); color: var(--ink);
  font-variation-settings: "wght" 600; letter-spacing: -0.022em;
}
.reg-uses span { font-family: var(--mono); font-size: 11.5px; letter-spacing: -0.02em; color: var(--muted); }
.reg-uses li:last-child b { color: var(--seal); }

/* ── ③ The five layers ────────────────────────────────────────────────────── */

/* The detail page's rows, minus their paragraphs — the chapter copy is already
   carrying the detail, and repeating it inside the object would make the
   reader choose which one to read. */
.reg-pillars { list-style: none; }
.reg-pillars li {
  display: flex; align-items: center; gap: 13px;
  padding: 11px 18px;
  border-top: 1px solid var(--line);
}
.reg-pillars li:first-child { border-top: 0; }
.reg-pillars .tile { width: 30px; height: 30px; color: #fff; }
.reg-pillars .tile svg { width: 15px; height: 15px; }
.reg-pillars li:nth-child(1) .tile { background: linear-gradient(145deg, #16a578, #0a5540); }
.reg-pillars li:nth-child(2) .tile { background: linear-gradient(145deg, #3fc79f, #108966); }
.reg-pillars li:nth-child(3) .tile { background: linear-gradient(145deg, #55b6cf, #256d8a); }
.reg-pillars li:nth-child(4) .tile { background: linear-gradient(145deg, #dcbd7c, #a8813a); }
.reg-pillars li:nth-child(5) .tile { background: linear-gradient(145deg, #1fae86, #0c6349); }
.reg-pillars b {
  min-width: 0;
  font-size: var(--t-sm); line-height: 1.3; letter-spacing: -0.024em;
  font-variation-settings: "wght" 580; color: var(--ink);
}
.reg-pillars .sys {
  margin-left: auto; flex: none;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--faint);
}

/* ── ⑥ What it wrote down ─────────────────────────────────────────────────── */

.reg-log { list-style: none; padding: 0 18px; }
.reg-log li {
  display: flex; align-items: baseline; gap: 12px;
  padding: 10px 0;
  border-top: 1px solid var(--line);
  font-size: var(--t-sm);
}
.reg-log li:first-child { border-top: 0; }
.reg-log b {
  flex: none; min-width: 62px;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.06em;
  color: var(--seal); font-weight: 500;
}
.reg-log li.no b { color: var(--refuse); }
.reg-log span { min-width: 0; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reg-log time { margin-left: auto; flex: none; font-family: var(--mono); font-size: 10.5px; color: var(--faint); }

.reg-foot {
  margin-top: auto;
  padding: 14px 18px 16px;
  border-top: 1px solid var(--line);
  background: var(--card-2);
  font-size: 12px; line-height: 1.5; color: var(--faint);
}

/* ── Responsive ───────────────────────────────────────────────────────────── */

/* Not a shrunk desktop slide: the camera's horizontal travel collapses to
   vertical, the 3D flattens, and copy gets the top of the frame to itself so
   it is readable before the scene is understood. */
@media (max-width: 1080px) {
  /* Two bands, never one. Copy owns the top of the frame and the object owns
     the bottom — the camera's horizontal travel becomes vertical, and nothing
     is ever asked to share space with a paragraph. */
  .world { width: min(88vw, 420px); top: auto; bottom: 1.5%; translate: -50% 0; }

  /* A real 3D turn on a phone too — around X, not Y, because in portrait a
     card turning top-over-bottom reads and a card turning left-over-right
     does not.

     THE RULE THAT MAKES THIS SAFE: no opacity anywhere on .app or its faces.
     Any opacity below 1 forces that element to `transform-style: flat`, which
     kills backface-visibility — the first attempt cross-faded the faces and
     showed the front face MIRRORED instead of the console. The cross-fade was
     the bug, not the 3D.

     The tilt leans on --beat so the card pitches back as you scroll between
     two chapters and settles flat on arrival. */
  /* The swap is a physical turn, not a cross-fade with a wobble. The card
     tips away to a steep angle at the midpoint of every chapter transition —
     which is exactly when one view hands over to the next — and lands flat.
     Desktop gets its drama from the camera crossing the frame; a phone has no
     room for that, so the object itself has to do the work. */
  /* ⚠️ NO TRANSFORM ON THE CROSS-FADING VIEWS ON MOBILE.
     Proven on an iPhone 17 simulator: inside the card's preserve-3d subtree
     WebKit keeps an element's geometry, computed opacity and visibility all
     correct and then never paints it. The register rendered as an empty white
     frame on every iPhone — a red background and a blue outline on the same
     element were invisible too, and removing the transform brought all of it
     back instantly. ANY transform does it: the independent `translate`, an
     equivalent `transform: translateY()`, even a bare `translateZ(0)`.

     This is the same family as the note below about opacity on .app and its
     faces. The desktop rise is a nicety; on a phone the views simply
     cross-fade, which nobody will miss, and the content actually appears. */
  /* Attribute selectors, to MATCH the specificity of the desktop rules above
     ([data-view="request"] etc). A bare .reg-view loses to them and the
     override silently does nothing — media queries add no specificity. */
  .reg-view[data-view], .pkt-card[data-p] { translate: none; transform: none; }

  /* ⚠️ AND THE COPY NEVER FADES OUT ON A PHONE.
     `.js .copy` starts at opacity 0 and only `.chapter.is-live .copy` brings
     it back — and is-live is `Math.round(f)`, a HARD FLIP at the midpoint
     between two chapters. So halfway through every scroll the chapter still
     filling the screen loses its eyebrow, heading and paragraphs while the
     next one has not arrived: "HOW IT WORKS" and the text under it visibly
     vanish mid-gesture. Reported from a real phone, and the dvh jitter fixed
     above was firing it constantly rather than only during a deliberate
     scroll.

     Desktop keeps the entrance animation — there the copy columns are pinned
     beside the object and the fade is part of the composition. On a phone one
     chapter owns the screen, so the copy is simply always there. */
  .js .copy, .chapter .copy { opacity: 1; translate: none; }
  /* ⚠️ AND NOTHING INSIDE THE CARD MAY MAKE ITS OWN PAINT LAYER.
     Third and fourth instances of the same WebKit failure, found because Adil
     noticed the check marks and the verdict seal were missing. Inside this 3D
     subtree iOS reports these elements at full size, opacity 1, visibility
     visible, with their background colour applied — and paints nothing. The
     seal probe read `30x30 @27,659 op=1 bg=rgb(15,122,90)` while the screen
     showed bare white.

     Everything that triggers it is a compositing/stacking trigger:
       transform   → .reg-view          (fixed above)
       clip-path   → .ck, .tile, .seal  (the octagon glyphs)
       position:relative → .seal        (clip-path alone was not enough)

     So the rule for anything inside .app on a phone is: no transform, no
     clip-path, no stacking context. A 5px radius on a 17px glyph reads as the
     same soft-cornered mark the octagon gives at that size. Desktop keeps the
     real octagon, the shadow and the stamp ring. */
  .app .ck, .app .tile, .app .seal, .app .strip i {
    clip-path: none;
    border-radius: 5px;
  }
  .app .strip i { border-radius: 2px; }
  .app .seal, .app .seal-wrap { position: static; box-shadow: none; }
  /* The impact ring only exists for the stamp animation, which this page
     never runs — and being absolutely positioned it needs the relative
     parent we just removed. */
  .app .seal-ring { display: none; }

  .app {
    transform: rotateX(calc(
      var(--tilt, 0) * 20deg
      + var(--beat, 0) * -52deg
      + var(--flip, 0) * -180deg));
    box-shadow:
      0 2px 6px rgba(11, 19, 16, 0.05),
      0 calc(22px + var(--beat, 0) * 40px)
        calc(52px + var(--beat, 0) * 46px)
        rgba(11, 19, 16, calc(0.12 + var(--beat, 0) * 0.12));
  }
  .film-body.is-deep .app {
    box-shadow:
      0 2px 6px rgba(0, 0, 0, 0.4),
      0 calc(26px + var(--beat, 0) * 40px)
        calc(70px + var(--beat, 0) * 46px)
        rgba(19, 163, 126, calc(0.22 + var(--beat, 0) * 0.12));
  }

  /* One band of light crossing the face as it turns. Driven by --sweep, which
     runs one way across the gap, while --beat fades it in and out — so it is
     a reflection travelling over the card, not a glow pulsing on it. */
  .app-front::before {
    content: ""; position: absolute; inset: -20% -40%; z-index: 3;
    pointer-events: none;
    background: linear-gradient(102deg,
      transparent 40%,
      rgba(255, 255, 255, 0.72) 50%,
      transparent 60%);
    opacity: calc(var(--beat, 0) * 0.85);
    translate: calc((var(--sweep, 0) - 0.5) * 150%) 0;
  }
  .app-back { transform: rotateX(180deg); }

  /* The row wraps rather than compressing: the address gets its own line and
     the controls sit under it. Squeezed onto one line at 346px the email
     truncates to about eight characters, which stops being a customer list. */
  .cns-bar { padding: 9px 12px; }
  .cns-head { padding: 11px 12px 8px; }
  .cns-tools { padding: 0 12px 9px; }
  .cns-tbl > li {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: 6px; padding: 8px 12px;
  }
  .cns-who { flex: 1 0 100%; }
  .cns-more, .cns-when { display: none; }
  .cns-act { padding: 3px 8px; font-size: 10px; }
  /* The joined date goes first on a phone — "14 records · seen 2 m ago" is
     what you actually scan a customer list for — and the third row goes with
     it, because a row clipped by the panel edge reads as a bug. */
  .cns-tbl > li:nth-child(3) { display: none; }
  .cns-tbl > li { padding: 6px 12px; }

  /* The panel is the whole object now, and its height is what decides whether
     the copy above it clears it. The tallest view is the request — head plus
     five checks plus the verdict — so these numbers are set by that one, and
     every other view is shorter than the frame it inherits. */
  .rec-head { padding: 10px 14px; }
  .rec-head > svg { width: 18px; height: 18px; }
  .rec-req { padding: 10px 14px; }
  .rec-checks { padding: 2px 0; }
  .rec-checks li { padding: 6px 14px; font-size: 12.5px; gap: 10px; }
  .rec-checks li b { min-width: 72px; }
  .ck { width: 17px; height: 17px; }
  .rec-verdict { padding: 11px 14px; min-height: 0; gap: 12px; }
  .seal { width: 34px; height: 34px; }
  .seal svg { width: 17px; height: 17px; }
  .reg-lead { padding: 11px 14px 7px; }
  .reg-uses { padding: 0 14px 2px; }
  .reg-uses li { padding: 7px 0; }
  .reg-uses b { font-size: 12.5px; }
  .reg-uses span { font-size: 11px; }
  .reg-pillars li { padding: 7px 14px; gap: 11px; }
  .reg-pillars .tile { width: 24px; height: 24px; }
  .reg-pillars .tile svg { width: 12px; height: 12px; }
  .reg-pillars b { font-size: 12.5px; }
  .reg-log { padding: 0 14px; }
  .reg-log li { padding: 8px 0; font-size: 12.5px; }
  .reg-log b { min-width: 56px; }
  .reg-foot { padding: 11px 14px 13px; font-size: 11.5px; }

  /* The bottom padding is pure reserve — the copy is top-aligned, so this only
     stops it growing down into the object. 46vh made the hero 858px tall in an
     812px frame on a small phone, and a chapter taller than the viewport can
     never land flush. 38vh still clears the object at every size measured. */
  .chapter { padding: clamp(72px, 12vh, 104px) clamp(20px, 6vw, 32px) 38vh; }

  /* Chapters whose content IS the text do not reserve half the frame for an
     object that has already receded — reserving it pushed them past one
     viewport, and a chapter taller than the screen can never land flush. */
  .chapter[data-lead="copy"] { padding-bottom: 14vh; }
  /* Every data-pos, not just some. `.copy` alone (0-1-0) loses to
     `.copy[data-pos="left"]` (0-2-0), so chapters 1, 3 and 5 kept desktop's
     `align-self: center` — their copy sat in the middle of the frame and ran
     straight into the object below it. */
  .copy,
  .copy[data-pos="left"],
  .copy[data-pos="right"],
  .copy[data-pos="right-low"],
  .copy[data-pos="left-low"],
  .copy[data-pos="centre"] {
    justify-self: start; align-self: start;
    text-align: left; align-items: flex-start;
    width: min(100%, 26rem); padding-top: 0;
  }
  .copy h1, .copy h2 { font-size: clamp(1.875rem, 1.2rem + 3.6vw, 2.5rem); }
  .copy-lead h1 { font-size: clamp(2.125rem, 1.3rem + 4.4vw, 3rem); }
  .ch-say { font-size: var(--t-base); max-width: 30ch; }
  .ch-layers { gap: 5px; }
  .ch-layers li { font-size: 12.5px; }
  .ch-layers b { min-width: 70px; }

  /* One flick, one chapter. `always` forbids skipping past a snap point in a
     single gesture, so every state gets its own screen and can be looked at. */
  .chapter { scroll-snap-stop: always; }

  /* Landing on a chapter should resolve immediately, not ease in for most of a
     second — on a phone the snap IS the transition. */
  .copy { transition: opacity 190ms var(--ease), translate 240ms var(--ease-out); }
  .world { transition: opacity 220ms var(--ease); }
  .copy-end .row { justify-content: flex-start; }
}

/* ── Small phones ─────────────────────────────────────────────────────────── */

/* Under about 400px a paragraph and a five-row instrument do not both fit in
   one viewport at comfortable sizes — measured 122px short at 360×780. Rather
   than cut content or let the panel run past the fold, every part of the frame
   gives up a little: the copy loses a line, the panel loses a few pixels per
   row, and the frame loses some of its top margin. */
@media (max-width: 500px) {
  .chapter { padding: clamp(60px, 9vh, 88px) 18px 38vh; }
  .chapter[data-lead="copy"] { padding-bottom: 14vh; }
  .copy { gap: 12px; }
  .copy h1, .copy h2 { font-size: clamp(1.75rem, 1.15rem + 3.2vw, 2.25rem); }
  .copy-lead h1 { font-size: clamp(1.95rem, 1.25rem + 4vw, 2.6rem); }
  /* The 30ch cap wrapped the paragraph well before the column ran out, which
     cost a whole line on exactly the screens that could least afford one. */
  .ch-say { max-width: none; font-size: 14.5px; line-height: 1.45; }
  .ch-plans { gap: 7px; }
  .ch-qs { gap: 6px; }
  .ch-qs li { padding-bottom: 6px; }

  .world { width: min(93vw, 420px); }
  .rec-head { padding: 9px 12px; }
  .rec-req { padding: 9px 12px; font-size: 12px; gap: 7px; }
  .rec-checks li { padding: 4px 12px; font-size: 12px; }
  .rec-checks li .said { font-size: 11px; }
  .rec-verdict { padding: 9px 12px; gap: 10px; }
  .seal { width: 30px; height: 30px; }
  .seal svg { width: 15px; height: 15px; }
  .rec-said b { font-size: 11.5px; }
  .rec-said span { font-size: 12px; }
  .rec-code { font-size: 10px; }
  .rec-code b { font-size: 12px; }
  /* The table ran 8px past the panel here and `overflow: hidden` ate the last
     customer's bottom border, which reads as a rendering fault rather than a
     crop. Tightened rather than dropping a row — three customers and a header
     stops looking like a table. */
  /* Nine pixels of air between the pricing copy and the card at 360 — the
     card has to give some back, since the copy is the argument. */
  .pkt-card { padding: 11px 12px 10px; }
  .pkt-hd { margin-bottom: 6px; }
  .pkt-rows li { padding: 4px 0; }
  .pkt-rows b { font-size: 12px; }
  .pkt-note { font-size: 12px; }
  .pkt-foot { margin-top: 7px; padding-top: 7px; font-size: 11px; }

  .cns-bar { padding: 8px 12px; }
  .cns-stats { padding: 8px 12px 9px; }
  .cns-tbl > li { padding: 5px 12px; }
  .cns-tbl .cns-acts { padding: 1px 12px 8px; }

  .reg-uses { padding: 0 12px 2px; }
  .reg-uses li { padding: 5px 0; gap: 1px; }
  .reg-uses b { font-size: 12px; }
  .reg-uses span { font-size: 10.5px; }
  .reg-pillars li { padding: 6px 12px; }
  .reg-pillars b { font-size: 12px; }
  .reg-log li { padding: 7px 0; font-size: 12px; }
  .reg-foot { padding: 10px 12px 12px; font-size: 11px; }
}

/* ── Reduced motion ───────────────────────────────────────────────────────── */

/* Same eight chapters, same order, same states — as composed editorial frames.
   Every state change still happens; only the travel between them is removed. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-snap-type: none; }
  .world { transition: none; }
  .app, .reg-view { transition: opacity 200ms linear; }
  .copy { opacity: 1; translate: none; transition: opacity 200ms linear; }
  .ch-scroll span::after { animation: none; opacity: 0.6; }
  .stage-light { filter: blur(48px); }
}

.film-body.is-reduced .stage-light { filter: blur(48px); }
