/* ═══════════════════════════════════════════════════════════════════════════
   MARBLELENS V3 · ACTS II–IV — EXPOSE · CONSTRUCT · HANDOVER

   Shared act scaffolding plus each act's own furniture. Every act follows
   the same grammar the hero set: paper-material sheets, mono kickers,
   Playfair statements, ONE accent owning the viewport at a time, and
   three-zone development whose boundary belongs to the instrument.

   All motion is written by js/acts.js from the pure model as CSS custom
   properties — nothing in this file owns a clock, so reduced motion and
   reverse scroll cost nothing here.
   ═══════════════════════════════════════════════════════════════════════════ */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
html:not(.js) .ho-draft { visibility: hidden; }
/* the construct demonstration is assembled by JS (the client site is cloned
   from a template); scriptless it would be an empty browser chrome with a
   slider that goes nowhere — hide the stage, the noscript note explains */
html:not(.js) .construct-stage { display: none; }
html:not(.js) .construct-bend { display: none; }   /* no track to bend onto */
html:not(.js) .feed-sheet { display: none; }

/* ── the instrument's stage — one fixed layer the lens lives on so the same
   element can travel between acts without ever re-parenting ── */
.v3-stage {
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
}

/* ═══ THE SPINE — the page's one printed margin rail ══════════════════════
   Every pinned sheet prints the same vertical rail at the same x (the
   content column's left edge), so at a section seam the outgoing sheet's
   rail bottom meets the incoming sheet's rail top and the route reads as
   ONE continuous line. The lens rides it; sections branch from it briefly
   and return. The x calc resolves against the pin (full viewport width),
   which is what keeps every segment collinear. */
/* the spine's x — declared once, resolved per element (every user is a
   full-viewport pin, so the % resolves identically and the segments stay
   collinear). 0.55 of the gutter: IN the margin, clear of the content
   column's type, still reading as the column's own rule. */
:root {
  --spine-left: calc((100% - min(var(--container), 100%)) / 2 + var(--gutter) * 0.55);
}
.v3-rail {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--spine-left);
  width: 3px;
  transform: translateX(-50%);
  border-radius: 2px;
  pointer-events: none;
  /* full strength edge to edge: during every seam slide this segment's
     ends MEET the neighbouring sheets' segments — end tapers would dim
     the one continuous line exactly at its junctions */
  background: rgba(31, 31, 28, 0.2);
}
/* the enlarger's stop ticks belong to the EXPOSE segment only */
.act-expose .v3-rail::before {
  content: "";
  position: absolute;
  left: -5px;
  right: -5px;
  top: 0;
  bottom: 0;
  background: repeating-linear-gradient(180deg,
    transparent 0 calc(25% - 1px), rgba(31, 31, 28, 0.26) calc(25% - 1px) 25%);
}

/* ── act scaffolding ── */
.act-travel { height: calc(100svh + var(--act-travel, 200vh)); }
.act-pin {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.is-static .act-travel { height: auto; min-height: 100svh; }
/* RELATIVE, not static: un-pins the sticky but keeps the pin a containing
   block — the feed's absolutely-positioned sheet otherwise resolves to the
   initial containing block and paints over the HERO at document top */
.is-static .act-pin { position: relative; top: auto; height: auto; min-height: 100svh; overflow: visible; }

.act-head {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: clamp(1.2rem, 3vh, 2.6rem) var(--gutter) 0;
}
.act-kicker {
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--accent-ink);
  margin: 0 0 0.6rem;
}
.act-headline {
  font-family: var(--serif);
  font-weight: 600;
  font-size: clamp(1.9rem, 1rem + 2.9vw, 3.3rem);
  line-height: var(--lh-head);
  color: var(--ink);
  margin: 0;
}
.act-lede {
  font-size: var(--t-lede);
  color: var(--ink-2);
  max-width: 38em;
  margin: 0.7rem 0 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ACT II — EXPOSE · the contact sheet under the enlarger head
   ═══════════════════════════════════════════════════════════════════════════ */
.act-expose .act-pin { background: none; }
.expose-stage {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: clamp(46px, 5vw, 76px) minmax(0, 1fr);
  gap: clamp(0.8rem, 2vw, 2rem);
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: clamp(0.8rem, 2.2vh, 1.8rem) var(--gutter) clamp(1rem, 2.6vh, 2rem);
}

/* the rail itself is the SPINE segment printed on this pin (.v3-rail on
   .act-pin); the grid's first column only reserves the head's clearance
   so the sheet never sits under the riding instrument */
.contact-sheet { grid-column: 2; }

/* the sheet: four specimen frames, numbered like a real contact sheet */
.contact-sheet {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(2, minmax(0, 1fr));
  gap: clamp(0.8rem, 1.6vw, 1.6rem) clamp(1rem, 2vw, 2rem);
  min-height: 0;
}
.contact-sheet.is-brochure {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: minmax(0, 1fr);
}
.cs-cell { display: flex; flex-direction: column; min-height: 0; position: relative; }
.cs-service {
  justify-content: flex-end;
  gap: clamp(0.7rem, 1.8vh, 1.2rem);
  padding: clamp(1.2rem, 3vw, 2.4rem);
  background:
    linear-gradient(145deg, transparent 0 42%, var(--accent-wash) 42% 58%, transparent 58% 100%),
    var(--paper-hi);
  border: 1px solid var(--line);
  box-shadow: var(--sh-mount);
}
.cs-service-mark {
  flex: none;
  width: clamp(3.1rem, 6vw, 5.2rem);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  border: 1px solid var(--accent-ink);
  border-radius: 50%;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.14em;
  color: var(--accent-ink);
}
.cs-service h3 {
  margin: 0;
  font-family: var(--serif);
  font-size: var(--t-h3);
  color: var(--ink);
}
.cs-service p {
  margin: 0;
  max-width: 28ch;
  color: var(--ink-2);
}
.cs-frame {
  position: relative;
  flex: 1;
  min-height: 0;
  background: var(--paper-hi);
  border: 1px solid var(--line);
  box-shadow: var(--sh-mount);
  overflow: hidden;
}
.cs-frame > img,
.cs-before, .cs-after {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.cs-before { z-index: 1; }
/* the developed image resolves behind the exposure light: a soft boundary
   sweeping across the frame FROM the rail side, driven by --dev. Not an
   opacity fade — the finished picture exists only where the light has
   passed. */
.cs-after { z-index: 2; }
html.js .cs-after {
  -webkit-mask-image: linear-gradient(90deg,
    #000 calc(var(--dev, 0) * 118% - 14%), transparent calc(var(--dev, 0) * 118% + 4%));
  mask-image: linear-gradient(90deg,
    #000 calc(var(--dev, 0) * 118% - 14%), transparent calc(var(--dev, 0) * 118% + 4%));
}
/* the moving exposure edge — a restrained warm light at the boundary,
   gone at rest (0 and 1) */
.cs-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(90deg,
    transparent calc(var(--dev, 0) * 118% - 16%),
    rgba(240, 195, 120, 0.28) calc(var(--dev, 0) * 118% - 5%),
    transparent calc(var(--dev, 0) * 118% + 5%));
  opacity: var(--edge, 0);
}
.cs-meta {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding-top: 0.55rem;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.14em;
  color: var(--graphite);
}
.cs-meta b { font-weight: 500; color: var(--ink-2); }
.cs-meta .cs-arrow { color: var(--accent-ink); }

/* tap/click compare — an explicit control, never hover-dependent (§26).
   Holding the frame shows the working state again; releasing restores. */
.cs-inspect {
  position: absolute;
  inset: 0;
  z-index: 4;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}
.cs-inspect:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 3px; }
.cs-cell.show-before .cs-after,
.cs-cell.show-before .cs-frame::after { visibility: hidden; }
/* desktop proximity — an enhancement only: the frame lifts a little */
@media (hover: hover) {
  .cs-cell:has(.cs-inspect:hover) .cs-frame { box-shadow: var(--sh-lift); }
}

/* Baba · fig. 0 — the exception. No compare control, no develop boundary,
   no lift. Just the photograph, and afterwards one quiet line. */
.cs-cell.cs-baba .cs-frame { box-shadow: var(--sh-mount); }
/* on the meta line, right-aligned — below the cell it clips against the
   pinned viewport on short screens and the line is never read */
.cs-note {
  position: absolute;
  right: 0.2rem;
  bottom: 0;
  font-family: var(--serif);
  font-style: italic;
  font-size: var(--t-small);
  color: var(--graphite);
  opacity: var(--note, 0);
}

/* ═══════════════════════════════════════════════════════════════════════════
   ACT II → III — the bridge: frame → viewport, rail → track
   ═══════════════════════════════════════════════════════════════════════════ */
.act-bridge { color: var(--ink-2); }
/* ochre recedes, architectural blue enters — the kicker reads the mixed
   accent, driven by --blue-in (the rail itself stays the house ink so the
   spine reads as ONE object across every section) */
.act-bridge .act-kicker {
  color: color-mix(in srgb,
    var(--ochre) calc(100% - var(--blue-in, 0) * 100%),
    var(--blue) calc(var(--blue-in, 0) * 100%));
}
.bridge-stage {
  flex: 1;
  min-height: 0;
  position: relative;
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: clamp(1rem, 3vh, 2rem) var(--gutter);
  display: grid;
  place-items: center;
}
/* the chosen frame, becoming a viewport */
.bridge-frame {
  position: relative;
  width: min(46vh, 78%);
  aspect-ratio: 4 / 5;
  background: var(--paper-hi);
  border: 1px solid var(--line);
  box-shadow: var(--sh-mount);
}
.bridge-frame img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* crop marks — four corners, arriving first */
.bf-crop { position: absolute; inset: -14px; pointer-events: none; opacity: var(--crop, 0); }
.bf-crop span { position: absolute; width: 22px; height: 22px; }
.bf-crop span::before,
.bf-crop span::after { content: ""; position: absolute; background: var(--ink-2); }
.bf-crop span::before { width: 100%; height: 1.5px; }
.bf-crop span::after { width: 1.5px; height: 100%; }
.bf-crop .c-tl { top: 0; left: 0; }
.bf-crop .c-tr { top: 0; right: 0; transform: scaleX(-1); }
.bf-crop .c-bl { bottom: 0; left: 0; transform: scaleY(-1); }
.bf-crop .c-br { bottom: 0; right: 0; transform: scale(-1); }
/* layout guides — thirds + a margin frame, arriving second */
.bf-guides {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: calc(var(--guides, 0) * 0.65);
  background:
    linear-gradient(90deg, transparent calc(33.3% - 0.5px), rgba(55, 85, 107, 0.5) 33.3%, transparent calc(33.3% + 0.5px)),
    linear-gradient(90deg, transparent calc(66.6% - 0.5px), rgba(55, 85, 107, 0.5) 66.6%, transparent calc(66.6% + 0.5px)),
    linear-gradient(180deg, transparent calc(50% - 0.5px), rgba(55, 85, 107, 0.5) 50%, transparent calc(50% + 0.5px)),
    linear-gradient(rgba(55, 85, 107, 0.35), rgba(55, 85, 107, 0.35)) 0 0 / 100% 1px no-repeat,
    linear-gradient(rgba(55, 85, 107, 0.35), rgba(55, 85, 107, 0.35)) 0 100% / 100% 1px no-repeat;
}
/* browser chrome — drawn, last to arrive; the frame has become a viewport */
.bf-chrome {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 34px;
  transform: translateY(calc((var(--chrome, 0) - 1) * 100%));
  opacity: var(--chrome, 0);
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 0 12px;
}
.bf-chrome i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1px solid var(--graphite-2);
}
.bf-chrome .bf-url {
  flex: 1;
  height: 14px;
  margin-left: 10px;
  border: 1px solid var(--line);
  border-radius: 7px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ACT III — CONSTRUCT · the drafting carriage over the client viewport
   ═══════════════════════════════════════════════════════════════════════════ */
/* the spine's ONE bend: the full-height margin rail runs THROUGH this pin
   (the spine continues below the junction — the release rides it out), and
   the rounded quarter corner switches the instrument onto the drafting
   track like a points junction. --track-y (the track's centreline,
   in-sheet px) is written by js/acts.js; --bend-r by js/hero.js. The drawn
   pieces and the model's bendPointAt share this geometry, so the lens
   rides exactly the printed line. */
.construct-bend {
  position: absolute;
  left: var(--spine-left);
  top: calc(var(--track-y, 150px) - var(--bend-r, 32px));
  width: var(--bend-r, 32px);
  height: var(--bend-r, 32px);
  border-left: 3px solid rgba(31, 31, 28, 0.2);
  border-bottom: 3px solid rgba(31, 31, 28, 0.2);
  border-bottom-left-radius: 100%;
  transform: translate(-1.5px, 1.5px);
  pointer-events: none;
}

.construct-stage {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  width: 100%;
  /* the stage keeps to --container so the track's left end IS the spine —
     the bend reconnects to the exact margin every other section prints */
  max-width: var(--container);
  margin-inline: auto;
  padding: clamp(0.7rem, 2vh, 1.4rem) var(--gutter) clamp(0.9rem, 2.4vh, 1.8rem);
}

/* the drafting track the carriage rides — grabbable: dragging the carriage
   IS scrolling the narrative (one writer) */
.draft-track {
  position: relative;
  height: 34px;
  margin-bottom: 0.6rem;
  cursor: grab;
  touch-action: pan-y;
}
.draft-track:active { cursor: grabbing; }
.draft-track:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.draft-track::before {
  content: "";
  position: absolute;
  /* the line begins where the bend's arc lands tangent on the track — the
     track's box starts at the content edge, 0.45 gutter right of the spine */
  left: calc(var(--bend-r, 32px) - var(--gutter) * 0.45 - 2px);
  right: 0;
  top: 50%;
  height: 3px;
  transform: translateY(-50%);
  border-radius: 2px;
  background: linear-gradient(90deg,
    rgba(31, 31, 28, 0.22), rgba(31, 31, 28, 0.22) 96%, rgba(31, 31, 28, 0.06));
}
.draft-track::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 11px;
  transform: translateY(-50%);
  background: repeating-linear-gradient(90deg,
    transparent 0 calc(10% - 1px), rgba(31, 31, 28, 0.2) calc(10% - 1px) 10%);
}
.track-hint {
  position: absolute;
  right: 0;
  top: -1.1rem;
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  color: var(--graphite-2);
}

/* the client viewport — a drawn browser, its width THE proof variable */
.mock-fit { position: relative; flex: 1; min-height: 0; }
.mock {
  width: calc(var(--mock-w, 1240) * 1px);
  /* NO max-height: the fit transform handles the vertical budget; a layout
     clamp here would clip the client site's lower modules BEFORE the scale
     and the proof would silently lose its menu and footer */
  /* NOT margin auto: auto margins on an overflowing box resolve to 0, so a
     1240px mock in a phone column left-aligns and the centred scale origin
     throws the visual off the right edge. The driver centres it explicitly:
     --tx = (available − mock·fit) / 2, origin top LEFT. */
  background: var(--paper-hi);
  border: 1px solid var(--line);
  box-shadow: var(--sh-mount);
  transform: translate3d(calc(var(--tx, 0) * 1px), 0, 0) scale(var(--fit, 1));
  transform-origin: top left;
  contain: layout paint;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.mock-chrome {
  flex: none;
  height: 30px;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 0 11px;
}
.mock-chrome i { width: 7px; height: 7px; border-radius: 50%; border: 1px solid var(--graphite-2); }
.mock-chrome .mock-url {
  flex: 1;
  max-width: 260px;
  margin-left: 10px;
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  color: var(--graphite);
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 1px 8px;
  white-space: nowrap;
  overflow: hidden;
}
.mock-body {
  position: relative;
  flex: 1;
  min-height: 0;
  container-type: inline-size;
  overflow: hidden;
}
/* blueprint ahead / developed behind — the carriage's own three zones.
   Boundary at --cx (px inside the mock body). The DRAFT copy is the in-flow
   one: it gives the mock its intrinsic height; the developed copy registers
   absolutely over it (same content, same width — same height). */
.site-sheet { overflow: hidden; }
.site-draft { position: relative; z-index: 1; }
.mock-body .site-live { position: absolute; inset: 0; }
.site-live { z-index: 2; }
html.js .construct-stage .site-live {
  -webkit-mask-image: linear-gradient(90deg,
    #000 calc(var(--cx, 0px) - 26px), transparent calc(var(--cx, 0px) + 26px));
  mask-image: linear-gradient(90deg,
    #000 calc(var(--cx, 0px) - 26px), transparent calc(var(--cx, 0px) + 26px));
}
/* the release must OUTRANK the masking rule above (html.js + 3 classes) —
   at lower specificity the mask silently survives construct-done and the
   returning carriage would un-develop the finished site */
html.js .construct-done .construct-stage .site-live {
  -webkit-mask-image: none;
  mask-image: none;
}

/* one shared foot row — §26: every px this strip does not spend is vertical
   budget the mock's fit transform gets back, so the proof renders LARGER */
.construct-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem 1.6rem;
  flex-wrap: wrap;
  padding-top: 0.6rem;
}
/* the width proof controls — explicit, touch-safe, and honest: each one is
   navigation to the scroll position that demonstrates that width */
.proof-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.proof-btn {
  min-height: 44px;
  padding: 0.5rem 1rem;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.12em;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
.proof-btn[aria-pressed="true"] {
  border-color: var(--accent-ink);
  color: var(--accent-ink);
}
.proof-btn:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.proof-note {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  color: var(--graphite-2);
}
.construct-cta {
  display: flex;
  align-items: center;
  gap: 1.2rem;
  flex-wrap: wrap;
}
.construct-cta .cta-line {
  font-family: var(--serif);
  font-weight: 600;
  font-size: var(--t-h3);
  color: var(--ink);
  margin: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   BUILD → CREATE — the one earned paper feed (§31)
   ═══════════════════════════════════════════════════════════════════════════ */
.act-feed .act-pin { display: block; }
.feed-sheet {
  position: absolute;
  inset: 0;
  transform: translate3d(0, calc(var(--lift, 0) * -118%), 0);
  /* THE torn edge — the only one on the page */
  clip-path: polygon(0 0, 100% 0, 100% 96.5%,
    96% 98.2%, 90% 97%, 83% 98.8%, 76% 97.4%, 68% 98.9%, 61% 97.2%,
    53% 98.6%, 46% 97.1%, 38% 98.8%, 31% 97.3%, 23% 98.7%, 16% 97.2%,
    9% 98.9%, 4% 97.6%, 0 98.4%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
}
.feed-shadow {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 9%;
  background: linear-gradient(180deg, transparent, rgba(28, 20, 10, 0.22));
  opacity: var(--fold, 0);
  pointer-events: none;
}
.feed-caption {
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: var(--ls-label);
  color: var(--graphite);
  text-transform: uppercase;
}
.feed-mini {
  width: min(560px, 74%);
  aspect-ratio: 16 / 10;
  background: var(--paper-hi);
  border: 1px solid var(--line);
  box-shadow: var(--sh-lift);
  overflow: hidden;
  position: relative;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ACT IV — HANDOVER · the visitor receives the lens
   ═══════════════════════════════════════════════════════════════════════════ */
.act-handover {
  position: relative;   /* the spine's terminus anchors to this section */
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}
/* the rail's end: a short printed terminus reaching the creation zone,
   closed with an end-stop — the route hands the instrument to the visitor */
.v3-rail.ho-terminus {
  bottom: auto;
  height: clamp(150px, 26svh, 260px);
}
.v3-rail.ho-terminus::after {
  content: "";
  position: absolute;
  bottom: -1px;
  left: 50%;
  width: 16px;
  height: 3px;
  transform: translateX(-50%);
  border-radius: 2px;
  background: rgba(31, 31, 28, 0.28);
}
.ho-zone {
  position: relative;
  flex: none;
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  min-height: clamp(240px, 46svh, 460px);
  padding: clamp(1.4rem, 4vh, 3rem) var(--gutter);
  display: grid;
  /* THE LINE SITS LOW IN THE ZONE. Centred, the instrument reached it while
     still coming down the rail and had to turn hard onto it. Dropping the line
     to the lower third gives the descent room to finish first, so the turn onto
     the baseline is a corner rather than a corner-and-a-stop -- and the run out
     to the right edge afterwards is one continuous move. */
  align-content: end;
  padding-bottom: clamp(2.2rem, 9vh, 5.5rem);
  /* pan-y, NOT none: vertical panning must keep working over a 46svh band —
     the lens is driven by HORIZONTAL drags, which pan-y still delivers.
     Completion restores full native behaviour below. */
  touch-action: pan-y pinch-zoom;
}
.ho-zone.ho-complete { touch-action: auto; }
/* the workspace states its bounds quietly: registration corners only */
.ho-zone::before,
.ho-zone::after {
  content: "";
  position: absolute;
  width: 20px;
  height: 20px;
  border: 0 solid var(--line);
  pointer-events: none;
}
.ho-zone::before { top: 10px; left: 12px; border-left-width: 1px; border-top-width: 1px; }
.ho-zone::after { bottom: 10px; right: 12px; border-right-width: 1px; border-bottom-width: 1px; }
/* inside the creation zone the visitor IS the lens — the pointer yields.
   Only while the zone is live and only for fine pointers. */
.ho-zone.ho-cursor { cursor: none; }

.ho-headline {
  position: relative;
  font-family: var(--serif);
  font-weight: 600;
  font-size: clamp(2.2rem, 1rem + 4.6vw, 4.6rem);
  line-height: var(--lh-tight);
  color: var(--ink);
  margin: 0;
  white-space: nowrap;
}
.ho-draft, .ho-live { display: block; }
.ho-draft {
  position: absolute;
  inset: 0;
  color: transparent;
  -webkit-text-stroke: 1.2px rgba(110, 106, 99, 0.8);
}
@supports not (-webkit-text-stroke: 1px black) {
  .ho-draft { color: var(--graphite-2); }
}
html.js .ho-live {
  -webkit-mask-image: linear-gradient(90deg,
    #000 calc(var(--ho-reveal, 0) * 112% - 6%), transparent calc(var(--ho-reveal, 0) * 112% + 6%));
  mask-image: linear-gradient(90deg,
    #000 calc(var(--ho-reveal, 0) * 112% - 6%), transparent calc(var(--ho-reveal, 0) * 112% + 6%));
}
.ho-complete .ho-live { -webkit-mask-image: none; mask-image: none; }
.ho-complete .ho-draft { display: none; }
.ho-hint {
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.16em;
  color: var(--graphite-2);
  margin: 1rem 0 0;
}

/* the product handover — ordinary, accessible commerce (§33) */
.ho-products {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: clamp(1.4rem, 4vh, 3rem) var(--gutter) clamp(2.4rem, 6vh, 4.6rem);
}
.ho-q {
  font-family: var(--serif);
  font-weight: 600;
  font-size: var(--t-h3);
  color: var(--ink);
  margin: 0 0 1.1rem;
}
.path-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(0.9rem, 2vw, 1.6rem);
  margin-bottom: 1.4rem;
}
.path-card {
  display: block;
  padding: 1.1rem 1.2rem 1.2rem;
  background: var(--paper-hi);
  border: 1px solid var(--line);
  box-shadow: var(--sh-mount);
  text-decoration: none;
  color: var(--ink-2);
}
.path-card:hover, .path-card:focus-visible {
  border-color: var(--accent-ink);
  outline: none;
  box-shadow: var(--sh-lift);
}
.path-card .pc-kicker {
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.18em;
  color: var(--accent-ink);
  display: block;
  margin-bottom: 0.4rem;
}
.path-card .pc-name {
  font-family: var(--serif);
  font-weight: 600;
  font-size: var(--t-h3);
  color: var(--ink);
  display: block;
  margin-bottom: 0.3rem;
}
.ho-actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.ho-actions .v3-btn { cursor: pointer; }
.ho-login {
  display: inline-flex;
  font-family: var(--sans);
  font-weight: 600;
  font-size: var(--t-small);
  color: var(--ink-2);
  background: none;
  border: 0;
  padding: 0.6rem 0.4rem;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Public brochure footer and the small launch-information documents share the
   same material and type system as the V3 page. */
.launch-footer {
  position: relative;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem 2rem;
  flex-wrap: wrap;
  padding: clamp(1.4rem, 4vw, 2.6rem) var(--gutter);
  background: var(--paper-hi);
  border-top: 1px solid var(--line);
  color: var(--ink-2);
}
.launch-footer nav { display: flex; gap: 0.8rem 1.4rem; flex-wrap: wrap; }
.launch-footer a,
.launch-doc a { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 3px; }
.launch-doc {
  width: min(calc(100% - 2 * var(--gutter)), 760px);
  margin-inline: auto;
  padding-block: clamp(3rem, 8vw, 7rem);
}
.launch-doc header { margin-bottom: clamp(2rem, 5vw, 3.5rem); }
.launch-doc .doc-kicker {
  margin-bottom: 0.8rem;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--accent-ink);
}
.launch-doc h1 { font-size: var(--t-h1); }
.launch-doc h2 { margin-top: 2rem; font-size: var(--t-h3); }
.launch-doc p,
.launch-doc li { margin-top: 0.8rem; max-width: 68ch; }
.launch-doc ul { padding-left: 1.2rem; }
.launch-doc .doc-note {
  margin-top: 2rem;
  padding: 1rem 1.2rem;
  background: var(--paper-hi);
  border: 1px solid var(--line);
}
.ho-login:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }

/* ═══════════════════════════════════════════════════════════════════════════
   RESPONSIVE — genuine recomposition per act (§34)
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 819px) {
  .expose-stage { grid-template-columns: clamp(26px, 6vw, 40px) minmax(0, 1fr); }
  .contact-sheet {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    overflow-y: visible;
  }
  .contact-sheet.is-brochure {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
  }
  .contact-sheet.is-brochure .cs-service { min-height: 270px; }
  /* four frames cannot share one phone viewport: the sheet becomes a
     column and FEEDS THROUGH the pin with the act's own travel — the
     enlarger head holds its rail while the sheet passes it, which is how
     a real enlarger meets a long strip. --sheet-shift is written by
     js/acts.js from the same u that drives the exposures. */
  .cs-frame { aspect-ratio: 4 / 5; flex: none; }
  .contact-sheet {
    transform: translate3d(0, calc(var(--sheet-shift, 0) * -1px), 0);
    will-change: transform;
  }
  .expose-stage { overflow: hidden; }
  .bridge-frame { width: min(52vh, 88%); }
  .path-grid { grid-template-columns: minmax(0, 1fr); }
  .ho-headline { white-space: normal; }
  .ho-draft { position: absolute; }
  .track-hint { display: none; }
  .proof-note { width: 100%; margin-left: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   REDUCED MOTION — every act rests developed; no clocks, no sweeps
   ═══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  .cs-frame::after { display: none; }
  .path-card { transition: none; }
}


/* ── ACT HEADLINE · graphite until the lens has passed ──────────────────────
   The hero's own rule, reused: ahead of the instrument the page is a working
   drawing, behind it the page is committed ink. Here the lens rides DOWN the
   rail rather than across the line, so the boundary is driven by how far it has
   descended past the headline -- the ink then sweeps the line left to right.

   ONE TEXT NODE. The ink copy is ::after content:attr(data-text), so the
   accessible name is never duplicated and no screen reader reads it twice.
   Everything is under html.js, so a scriptless visit gets plain ink. */
html.js .act-develop {
  position: relative;
  color: transparent;
  -webkit-text-stroke: 1.1px var(--graphite-2);
}
html.js .act-develop::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  color: var(--ink);
  -webkit-text-stroke: 0;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(90deg,
    #000 calc(var(--dev-x, -40px) - 30px), transparent calc(var(--dev-x, -40px) + 30px));
  mask-image: linear-gradient(90deg,
    #000 calc(var(--dev-x, -40px) - 30px), transparent calc(var(--dev-x, -40px) + 30px));
}
/* no text-stroke support: keep it legible rather than inventing a second look */
@supports not (-webkit-text-stroke: 1px black) {
  html.js .act-develop { color: var(--graphite-2); }
}


/* ── THE LAST STRETCH ──────────────────────────────────────────────────────
   The instrument entered from far left on scroll; it has to be able to leave
   the same way. The handover is the last section and the page ended with it, so
   there was no travel to map an exit onto -- the run out could only be fired by
   the visitor finishing the line by hand.

   This gives the beat its own scroll, exactly as every other act has: the line
   sticks while the spacer passes under it, and that pass is what walks the lens
   across the line and out to the right. The hand still works; it just is not
   the only way through any more. */
.ho-stage { position: relative; flex: none; }
.ho-zone { position: sticky; top: 0; }
/* flex: none -- .act-handover is a flex column and a bare height on a flex item
   is still shrinkable, so the spacer collapsed to nothing. The zone's own top
   then sat past the document's last scroll position and the beat could never
   start: measured stickAt 13155 against a maxScroll of 13154. */
.ho-travel { height: 118svh; flex: none; }
.is-static .ho-zone { position: relative; top: auto; }
.is-static .ho-travel { height: 0; }


/* ── THE LINE ENDS WHERE THE INSTRUMENT LEAVES ─────────────────────────────
   Left-aligned, the line finished around the middle of the sheet and the lens
   then had to cross a stretch of empty paper before it could go -- so the exit
   read as leaving from the middle of the screen rather than off the end of the
   sentence. Hugging the line to the right margin puts its last glyph where the
   run-out begins: the instrument completes the line and is already at the edge.

   Nothing in the mechanism moves. The reveal mask is a percentage of the
   headline's OWN box and the walk reads that box's rect, so both follow the
   type wherever it sits. */
.ho-headline { width: fit-content; margin-left: auto; }
.ho-hint { text-align: right; }


/* ── THE HANDOVER'S OWN SEQUENCE ───────────────────────────────────────────
   Everything in this beat is a function of --ho-reveal, the same 0..1 the lens
   walks, so the whole thing scrubs forward and back with the instrument.

     --ho-type    the approach: the question types itself in while the lens
                  rolls down the rail and turns onto the line. The instrument
                  is never parked waiting for the type -- both move together.
     --ho-reveal  the crossing: the lens inks "Now, make it yours"
     the last 14% of the reveal brings the two ways forward underneath

   The question is typed by clipping whole characters off a monospace line --
   ch units, so the cut always lands between glyphs and never mid-letter. The
   caret sits on the cut and leaves when the sentence is finished. */
.ho-q {
  --ho-letter-run: 4.7em;
  margin: 0 0 clamp(0.7rem, 2vh, 1.4rem);
  text-align: right;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--graphite-2);
}
.ho-q-ink {
  display: inline-block;
  white-space: nowrap;
  overflow: hidden;
  vertical-align: bottom;
  /* 29 characters plus the tracking between all 29 glyphs; sizing only in
     ch silently clipped "become?" because letter-spacing is outside ch. */
  width: calc((29ch + var(--ho-letter-run)) * clamp(0, var(--ho-type, 0), 1));
  border-right: 1px solid var(--accent-ink);
  /* the caret retires with the sentence */
  border-right-color: rgba(138, 92, 24, calc(1 - clamp(0, (var(--ho-type, 0) - 0.94) / 0.06, 1)));
}

/* the two ways forward, arriving under the finished line */
.ho-after {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-4);
  justify-content: flex-end;
  margin-top: clamp(1rem, 3vh, 2.2rem);
  opacity: clamp(0, (var(--ho-reveal, 0) - 0.86) / 0.14, 1);
  transform: translateY(calc((1 - clamp(0, (var(--ho-reveal, 0) - 0.86) / 0.14, 1)) * 10px));
  pointer-events: none;
}
.ho-complete .ho-after { pointer-events: auto; }

/* the hint has done its job once the sentence is finished */
.ho-hint { opacity: calc(1 - clamp(0, (var(--ho-reveal, 0) - 0.7) / 0.3, 1)); }
.ho-complete .ho-hint { opacity: 1; }


/* ── THE LINE STARTS ON BLANK PAPER ────────────────────────────────────────
   The draft used to sit there in pencil from the moment the section arrived,
   so only "yours" was ever a reveal. The whole sentence is hidden now.

   But it is not simply switched off: the pencil is masked too, on a boundary
   that runs slightly AHEAD of the ink's. So at any instant the line reads
   blank paper -> pencil -> ink, with the instrument sitting on the seam
   between the last two. The drawing is made and committed in one pass, which
   is the site's own language rather than a fade.

   Both boundaries are the same 0..1 the lens walks, so this scrubs backwards
   with everything else. */
html.js .ho-draft {
  -webkit-mask-image: linear-gradient(90deg,
    #000 calc((var(--ho-reveal, 0) + 0.17) * 112% - 6%),
    transparent calc((var(--ho-reveal, 0) + 0.17) * 112% + 6%));
  mask-image: linear-gradient(90deg,
    #000 calc((var(--ho-reveal, 0) + 0.17) * 112% - 6%),
    transparent calc((var(--ho-reveal, 0) + 0.17) * 112% + 6%));
}
.ho-complete .ho-draft { -webkit-mask-image: none; mask-image: none; }

/* The handover stays sticky while its final travel completes. On a narrow
   viewport the headline and two actions are taller than on desktop, so the
   preview cards need a reserved landing below that sticky content. This keeps
   every word and action in ordinary reading order instead of letting the next
   block slide underneath the conversion message. */
@media (max-width: 819px) {
  .ho-after {
    max-width: 100%;
    gap: 0.45rem 0.9rem;
  }
  .ho-after .v3-btn,
  .ho-login {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .ho-q {
    --ho-letter-run: 2.9em;
    letter-spacing: 0.1em;
  }
  .ho-draft {
    -webkit-text-stroke-width: 1.35px;
    -webkit-text-stroke-color: rgba(92, 88, 81, 0.92);
  }
}
