/* ============================================================
   synqa.ca - pass 5 mobile composition
   The brief (section 9) treats mobile as its own composition, not a
   media-query degradation.  Several signature moves drop entirely
   (M08 pin, M11 ticker, M13 trail, M14 magnetic, M18 split, M23
   double-take); others adapt (M09 reduced translation, M19 moves
   from top to bottom of viewport); two new moves appear (M25 tap-
   to-reveal hint, M26 drawer slide).
   ============================================================ */

/* ----------------------------------------------------------------
   Hamburger button + drawer - present on every page; visible only
   under the mobile breakpoint.
   ----------------------------------------------------------------*/
.shell-nav-hamburger {
  display: none;
  background: none;
  border: none;
  padding: 8px;
  margin: 0;
  cursor: pointer;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
}
.shell-nav-hamburger:focus { outline: 2px solid var(--clay); outline-offset: 2px; }
.shell-nav-hamburger-bars {
  display: flex; flex-direction: column; gap: 5px;
  width: 22px;
}
.shell-nav-hamburger-bars i {
  display: block;
  height: 1px;
  background: var(--ink);
  transition: transform 240ms var(--ease-standard), opacity 120ms linear;
}
/* The hamburger ALWAYS reads Ink. It sits on the nav bar's own backdrop - the
   docked bone-92% blur (data-docked, past scroll t>0.3, site.js) or the bone Ch1
   opening while undocked - NOT directly on the chapter surface behind. The old
   [data-surface="ink"] -> var(--bone) flip assumed the bars sat on the ink chapter
   and made them bone-on-bone (invisible) over ink chapters (Ch2/Ch5/Ch8...), which
   is the whole nav for most of the scroll. Owner: "just make it always ink." Every
   page hero is bone, so there is no undocked-over-ink case to invert for. */
[data-surface="ink"] .shell-nav-hamburger-bars i { background: var(--ink); }

/* Drawer - fixed full-height panel, slides in from the right.
   Inline transform driven by mobile.js (gotcha-1 safe). */
.shell-drawer {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: min(360px, 86vw);
  background: var(--bone);
  z-index: 60;
  display: flex;
  flex-direction: column;
  padding: 28px;
  gap: 24px;
  transform: translateX(100%);
  will-change: transform;
  visibility: hidden;
  box-shadow: var(--shadow-lg);
  /* The drawer now carries the full 8-link inventory (Home / Work /
     Theta / Holm / Rewrite / PatchPal / About / Contact) plus head
     and foot. On short viewports (landscape phones, etc.) the link
     list could overflow the panel; allow vertical scroll so every
     link is reachable. The foot stays pinned via margin-top: auto. */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
.shell-drawer[data-open="true"] { visibility: visible; }
.shell-drawer-head {
  display: flex; align-items: center; justify-content: space-between;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}
.shell-drawer-mark img { height: 22px; width: auto; }
.shell-drawer-close {
  background: none; border: none; padding: 8px;
  cursor: pointer;
  font-family: var(--font-mono); font-size: var(--fs-stamp);
  letter-spacing: var(--tracking-stamp); text-transform: uppercase;
  color: var(--stone);
}
.shell-drawer-close .period { color: var(--clay); }
.shell-drawer-links {
  display: flex; flex-direction: column; gap: 12px;
  font-family: var(--font-display);
  /* 26px (was 28px) so the full 10-link inventory keeps an even rhythm
     on the common 740-844px phones without the Mobile Apps group needing
     its own smaller size. */
  font-size: 26px;
  line-height: 1.05;
}
/* M26 drawer · short-viewport accommodation. On phones in landscape
   (or smaller windows under 720px tall), 8 display-size links + head
   + foot can clip. Step the type + gap down so the inventory stays
   on one panel without scroll on the common cases. */
@media (max-height: 720px) {
  .shell-drawer-links { font-size: 22px; gap: 10px; }
  .shell-drawer-links a { padding: 4px 0; }
}
@media (max-height: 560px) {
  .shell-drawer-links { font-size: 18px; gap: 6px; }
  .shell-drawer { gap: 16px; padding: 20px; }
}
.shell-drawer-links a {
  color: var(--ink);
  border-bottom: none;
  padding: 8px 0;
  text-decoration: none;
}
.shell-drawer-links a .period { color: var(--clay); }
/* M15 press state (the brief: hover-italic translates to pressed
   state on tap; this fires as :active on touch). */
.shell-drawer-links a:active {
  font-style: italic;
  color: var(--clay);
}
/* M26 drawer · Mobile Apps group label + product links.
   Decision: consistent sizing, NOT a collapsible disclosure. The drawer
   is the full site menu (a flat vertical list where every destination
   should be reachable at a glance); hiding the four product pages
   (Theta/Holm/Rewrite/PatchPal) behind a tap would add friction to reach
   primary destinations. So we keep all links visible and remove the
   rhythm jump instead. The previous treatment dipped to 20px + 8px gap
   for the apps and then jumped back to the big size for About/Contact;
   now the apps inherit the same 26px size and the same 12px gap as the
   rest of the list, so the whole inventory reads as one even cascade.
   The mono MOBILE APPS label is the only hierarchy cue (a quiet category
   header), and a small indent keeps the group legibly grouped. No
   downward triangle: the group is not collapsible, so a triangle would
   be a false affordance. */
.shell-drawer-group-label {
  font-family: var(--font-mono); font-size: var(--fs-stamp);
  letter-spacing: var(--tracking-stamp); text-transform: uppercase;
  color: var(--stone);
  padding: 6px 0 2px;
  border-top: 1px solid var(--border);
  margin-top: 4px;
}
.shell-drawer-group {
  display: flex; flex-direction: column; gap: 12px;
  padding-left: 14px;
}
/* apps inherit the .shell-drawer-links 26px size (no override): the group
   reads at the same rhythm as Home / Services / About, only indented and
   labelled. */

.shell-drawer-foot {
  margin-top: auto;
  display: flex; flex-direction: column; gap: 8px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  font-family: var(--font-mono); font-size: var(--fs-stamp);
  letter-spacing: var(--tracking-stamp); text-transform: uppercase;
  color: var(--stone);
}
.shell-drawer-foot .period { color: var(--clay); }
.shell-drawer-foot .toronto { color: var(--clay); }
.shell-drawer-foot .capetown { color: var(--harbor); }

/* Scrim under the drawer.  Inline opacity driven by mobile.js. */
.shell-drawer-scrim {
  position: fixed;
  inset: 0;
  background: var(--bone-300);
  z-index: 59;
  opacity: 0;
  visibility: hidden;
  will-change: opacity;
}
.shell-drawer-scrim[data-open="true"] { visibility: visible; }

/* M25 tap-to-reveal hint - only visible on touch, only on Ch 3
   redaction lines.  Pulses once on chapter enter; fades after the
   first tap on any line.
   position:absolute takes the hint out of normal flow so that fading
   or hiding it (display:none) causes zero layout shift on the
   .ch-03-line-body; the containing block stays the same height.
   Centered ON the bar (owner ask: not above it): z-index:3 keeps it above
   .ch-03-line-bar (position:absolute, covers the full line-body); Bone text
   reads clearly against the Ink bar, with the Clay dot as the accent. */
.ch-03-line-mobile-hint {
  display: none;
  position: absolute;
  top: 50%;
  left: 12px;
  transform: translateY(-50%);
  z-index: 3;
  font-family: var(--font-mono); font-size: 9px;
  letter-spacing: var(--tracking-stamp); text-transform: uppercase;
  color: var(--bone);
  pointer-events: none;
  will-change: opacity;
}
.ch-03-line-mobile-hint .dot { color: var(--clay); margin-right: 6px; }

/* ----------------------------------------------------------------
   Breakpoint - mobile composition kicks in below 1024px OR on any
   touch-primary device, mirroring section 9's rule.
   ----------------------------------------------------------------*/

@media (max-width: 1023px), (pointer: coarse) {

  /* ---------- Persistent shell ---------- */

  .shell-nav {
    padding: 14px 20px;
  }
  .shell-nav-links { display: none; }
  /* Hide the desktop CTA in nav; the drawer holds the same call. */
  .shell-nav > .shell-nav-cta { display: none; }
  .shell-nav-hamburger { display: inline-flex; }

  /* Place stamps: the two FIXED corner stamps are hidden on mobile.
     On the narrow mobile width they were position:fixed over scrolling
     content and collided with headings / breadcrumbs / footers across
     ~12 surfaces (home ch-01/ch-08/ch-09, /about, /services, /work,
     /theta, /contact, /holm, /rewrite, /patchpal, /privacy-inquiry,
     /404, the legal pages). The place-stamp BRAND RULE (Toronto in Clay
     43.65 N, Cape Town in Harbor 33.92 S) is still satisfied on every
     page in the mobile composition: it is carried cleanly in the nav
     drawer foot (.shell-drawer-foot) on every page, in every page
     footer (homepage .footer-coord; subpage .subpage-footer-stamp +
     .subpage-footer-bottom), and on the homepage ch-01 dateline. So the
     fixed overlay is dropped, not the identity. */
  .shell-stamp-tr,
  .shell-stamp-bl { display: none; }

  .shell-chapter-stamp {
    font-size: 9px;
    top: 80px; left: 20px;
  }
  /* The fixed M20 corner chapter stamp wraps its active index in an OPAQUE
     .stamp-chip Ink pill (foundations.css). At top:80px/left:20px that pill sits
     exactly where the Ch2 thesis heading ("We don't sell software...") rises out
     of its pin, so the solid pill eclipsed the heading letters under it (and gave
     the Ch4 stamp a heavy on-ink block). On mobile the corner stamp should read
     as the quiet mono wayfinder it is on desktop, never an opaque eclipse: strip
     the chip treatment back to plain --stamp-fg mono text so heading letters are
     never hidden. Desktop (where the heading centres lower + the corner is wider)
     keeps the chip. */
  .shell-chapter-stamp .stamp-chip {
    background: transparent !important;
    color: var(--stamp-fg) !important;
    padding: 0 !important;
  }

  /* M11 mono ticker - disabled. */
  .shell-ticker { display: none !important; }

  /* M13 cursor period trail - disabled (CSS-side; mobile.js also
     skips creating dots when pointer: coarse). */
  .shell-trail { display: none; }

  /* M14 magnetic CTA - disabled implicitly by mobile.js (the
     pointer:fine gate already covers this), no CSS needed. */

  /* M03 carbon copy - disabled. */
  .carbon { display: none !important; }

  /* ---------- Chapter 1 · cold open ---------- */

  /* Cold open is a centered wordmark hero. It MUST render under 40vh of the
     device: the harness flags any section over 40% viewport with < 80 text
     chars as a DEAD BAND (the cold open is ~73 chars). --m-coldopen-h is 38svh
     for that reason (the plan's 70svh was itself a dead band). M01 dock / M05
     erase / M29 glyph roll stay intact. The mono-top + place stamp insets are
     pulled in tight so the SECTION stays under 40vh - they no longer sit a
     fifth of a (now-tall) viewport from each edge. */
  .ch-01 {
    height: auto; min-height: var(--m-coldopen-h);
    padding: var(--m-space-xl) 0;
  }

  /* M05 erase / .chapter-bridge runway left at the desktop default (site.css,
     100vh) on mobile too - the pass-3 mobile trim + the retimed erase were
     reverted at the owner's request (the erase read wrong on real-device scroll). */

  /* Ch1 ambient grid: STATIC + FILLS THE OPENING SCENE (pass 7.4). The field is now
     absolute (homepage-fields.js `centered`) so it scrolls UP with the page like the
     live v12 site (owner: "make it static") and, being absolute not fixed, its slice
     art can no longer ZOOM when the iOS chrome swings the viewport (owner bug 2). But
     the ch-01 box is only ~22svh, so a plain inset:0 field would be a thin top strip
     (owner's earlier "crosses only on top"). Give it the full opening-viewport height
     instead - and use 100LVH (the LARGE / stable viewport), NOT svh/dvh, so the height
     is CONSTANT as the chrome shows/hides: static fill with zero zoom. The bridge below
     is made transparent so the grid shows through the opening scene behind the wordmark
     (its bone backdrop still comes from body/root); the M05 erase canvas (z:28) covers
     the grid during the Bone->Ink transition and the opaque Ch2 Ink panel occludes it
     past the runway, so crosses only ever paint on Bone. Mobile-scoped; desktop (fixed
     pin-clip field) untouched. */
  /* pass 8.1: raw 100lvh FALSIFIED on-device - in the frame-resize webview
     world lvh re-resolves on EVERY direction change (the whole frame swings
     750..874), so the raw-lvh field height zoomed exactly like the old fixed
     mount. The FROZEN px token re-lands only at quiet adoption (site.js
     publishViewportTokens, scroll-compensated, on every page) - constant
     through churn in BOTH worlds, rotation-safe via the width-path republish.
     vh baseline per the companion law. Same swap applied to the ch-05/06
     sticky svgs below and the sub-page backdrops (synqa-backgrounds.css,
     subpages.css). */
  #ch-01 .ch-ambient-field { bottom: auto; height: 100vh; height: calc(var(--lvh-px, 1vh) * 100); }
  .chapter-bridge { background: transparent; }

  /* Ch5 ambient field: VIEWPORT-scale art, STICKY ride (pass 8, owner: "ch5
     background is now too big... go back to the standard sizing"). The pass-7.3
     `centered` mount binds the field box to the WHOLE ~4-screen-tall ch-05
     section, and the 1360x720 `slice` art scales to the box -> ~5x blow-up.
     Restore the old viewport-scale look WITHOUT position:fixed (fixed is the
     WKWebView bleed class killed in 7.3): the field DIV stays absolute inset:0
     (bound + clipped to the section - the bleed-impossible guarantee), and the
     inner SVG becomes STICKY at one viewport tall, riding the scroll through the
     chapter exactly like the old fixed field read. The sticky svg clamps INSIDE
     the div (no negative-margin tricks: a sticky element's clamp uses the margin
     box, so a -100lvh margin would let the painted box hang past the section -
     measured escape 874px into ch-06; the plain in-flow svg clamps at the div
     bottom instead). overflow moves hidden->clip on this div ONLY: overflow:
     hidden makes the div a scroll container, which silently kills any inner
     sticky (it sticks to the never-scrolling div, i.e. never); overflow:clip
     clips identically WITHOUT scroll-container-ness, so the svg sticks to the
     viewport. contain:layout paint (base rule) keeps clipping as a second belt.
     Pre-overflow:clip engines (iOS <16) ignore the invalid value, keep hidden,
     and the svg degrades to a static viewport-scale field parked at the section
     top - correct art scale, just no ride. vh baseline per the companion law.
     ALSO applied to ch-06 (~2300px tall): at the 3.2x section-scale its isometric
     field spread so faint it effectively vanished - the owner's earlier "ch6's
     background isnt showing at all" complaint resurfacing; viewport scale restores
     the approved visibility. ch-07 is ~1 viewport tall (no blow-up) and ch-04's
     field is behind the pin composition - both left as-is. */
  #ch-05 .ch-ambient-field--pin,
  #ch-06 .ch-ambient-field--pin { overflow: clip; }
  #ch-05 .ch-ambient-field--pin svg,
  #ch-06 .ch-ambient-field--pin svg {
    position: sticky;
    top: 0;
    /* pass 8.1: frozen token, not raw lvh - see the ch-01 note above. */
    height: 100vh;
    height: calc(var(--lvh-px, 1vh) * 100);
  }

  /* M05 erase canvas stays ABOVE Ch2 on mobile (the desktop z:28; site.css).
     The painted erase must play OVER the Ch2 thesis heading as it rises out of
     its pin from the BOTTOM of the viewport - that overlap IS the designed M05
     reveal (the brush erases the giant "Synqa." mark to UNVEIL the heading). A
     prior pass dropped the canvas below Ch2 (z:1) to kill the un-erased "a."
     eclipse, but that removed the reveal on the come-up: the heading rose bare /
     popped-in, which the owner flagged as "out of place". So the canvas is left
     at its desktop z-index here; the eclipse is solved purely in TIME instead -
     site.js applyErase() now dissolves the whole canvas (the late-erased right
     "a." fragment included) BEFORE the rising heading climbs from the lower
     viewport into the mid/top band. Net: come-up = painted erase over the
     heading (the reveal); mid/top = canvas fully gone, heading clean. Desktop is
     unchanged (its heading centres lower and never enters the canvas band). */

  .ch-01-mono-top { top: var(--m-space-md); font-size: 9px; }
  .ch-01-stamp {
    bottom: var(--m-space-md); font-size: 9px; gap: 6px;
    flex-direction: column; align-items: center;
  }
  .ch-01-stamp .dot { display: none; }

  /* M01 dock wordmark sizes down for mobile + lands at the smaller
     nav slot.  HERO_W is hard-coded in site.js; the visible target
     scale stays at 0.18 but the dock target nav slot is now ~62px,
     so visually the wordmark ends up smaller anyway. */
  .dock-wordmark { width: 200px; }
  /* width + min-height: visual mark is 62x22; tap area is 44px tall
     (fixed nav, so no page height impact). */
  .shell-nav-brand { width: 62px; height: auto; min-height: 44px; }
  .shell-nav-brand img { height: 22px; }

  /* ---------- Chapter 2 · the line ---------- */

  .ch-02 { padding: var(--m-space-lg) 0; min-height: auto; }
  .ch-02-inner { padding: 0 24px; }
  .line-head { font-size: clamp(36px, 9vw, 56px); }
  .ch-02-foot {
    font-size: 9px; margin-top: 32px;
    flex-wrap: wrap;
  }

  /* ---------- Chapter 3 · the chair ---------- */

  /* section-pad-y (40px) shrunk to --m-space-md (24px); inner gap reduced from
     --m-space-lg (32px) to --m-space-md (24px). The M25 tap-to-reveal hint and
     M09 redaction lines stay unchanged. */
  .ch-03 {
    min-height: auto; padding: var(--m-space-md) 0;
  }
  .ch-03-inner { padding: 0 24px; gap: var(--m-space-md); }
  .ch-03-head { font-size: clamp(26px, 7vw, 38px); }
  .ch-03-lines { gap: var(--m-space-md); }
  .ch-03-line { flex-direction: column; align-items: flex-start; gap: 0; }
  .ch-03-line .lead { transform: none; font-size: 18px; }
  /* The line-body sizes to its inline-block text in the mobile column,
     so the shortest line (the first, "The CRM that doesn't talk to
     invoicing.") produced a redaction bar ~half the width of the others.
     Stretch the body to the full column width so every TAP-TO-READ bar
     spans the same width and the three read as a consistent stack. The
     bar (width:100% of the body) then fills the column on all three. */
  .ch-03-line-body { align-self: stretch; width: 100%; }
  .ch-03-line-text { font-size: clamp(18px, 4.8vw, 24px); }
  /* Hide the desktop cursor-proximity hint, show the tap-to-read
     hint instead.  mobile.js handles the pulse + fade-out. */
  .ch-03-line-hint { display: none; }
  .ch-03-line-mobile-hint { display: block; }

  /* ---------- Chapter 4 - the turn (M18 vertical sticky split) ---------- */

  /* The chapter scroll runway: --m-split-runway (150svh, was 230) tightens the
     mobile M18 scrub. applyCh04 (pass3.js, mobile branch) derives its scrub
     runway from ch04.offsetHeight - stableVH, so this becomes ~50svh of parting
     scroll - the CSS change alone keeps the JS in lockstep, no JS edit needed.
     The pin (sticky, 100svh) and the 50/50 svh halves are unchanged. */
  .ch-04 {
    height: var(--m-split-runway); min-height: unset;
    background: var(--bone);
  }

  /* Sticky stage - the whole visible area for the pin duration.
     overflow:hidden clips the sliding halves as they depart.
     No overflow/transform/filter on any ancestor (body uses overflow-x:clip
     which is safe and does not break sticky).

     COVER = the LARGE viewport, not svh. A 100svh pin only reaches the
     small (toolbar-shown) viewport; when the mobile toolbar HIDES the
     viewport grows to lvh and a ~svh->lvh band at the bottom of the pin
     uncovers what is behind #ch-04 (the documented Ch4 toolbar-band leak).
     Sizing the pin to max(100lvh, 100svh + --m-cover-pad) makes it reach
     the large viewport so nothing leaks under the clamp at any toolbar
     state, and guarantees the box always exceeds 100svh (clears the
     harness svhCoverRisk gate, which the verify viewport cannot otherwise
     distinguish from svh). The runway/scrub math in applyCh04 keeps using
     stableVH (svh) so the page never jumps on toolbar show/hide. */
  .ch-04-pin {
    position: sticky; top: 0; overflow: hidden;
    height: 100vh;
    /* THE PIN-BACKDROP SURFACE LAW (pass 5). The pin's backdrop is never a
       designed surface: the halves are self-contained (explicit Bone/Ink
       above) and the unified Ink panel covers the pin behind them, so this
       background only ever paints through ERROR gaps. During an in-app
       frame-resize churn the stale-phase gap between the pinned halves and
       the live window exposed it as the field-recorded light bar (pass-5
       geometry gate: probe light where both settled worlds are dark,
       convicted at f29dea6). Ink matches the chapter surface, so any
       transient exposure paints the chapter's own color and reads as
       nothing. Settled compositions never show this pixel (the 78-cell
       pixel harness holds the identity). */
    background: var(--ink);
  }
  /* The svh sizing rides an @supports gate for two reasons. (1) IACVT: a
     var()-containing modern line is parse-ACCEPTED by pre-svh engines
     (Chromium <108, WebKit <15.4) and only fails at computed-value time,
     which would unset height and collapse the pin past the vh baseline
     above. (2) The frame-resize churn law: raw svh re-resolves INSTANTLY
     when an in-app browser resizes the WKWebView frame (the Instagram
     top-header class), reflowing the pin mid-gesture with no JS involved,
     so capable engines consume the JS-published px tokens instead
     (--svh-px / --lvh-px, site.js publishViewportTokens, refreshed only at
     quiet adoption). The 1svh/1lvh fallbacks keep exact stock behavior
     with JS absent; the v12 large-viewport cover law is unchanged. */
  @supports (height: 100svh) {
    .ch-04-pin {
      /* pass 8.4 - THE PIN RE-FREEZES (the parked-jump conviction). The owner's
         third recording: parts of Ch4 jump on a flip DEPENDING ON WHERE THE USER
         IS PARKED - the release-point signature. With the SECTION token-frozen
         (150 x --svh-px) but the pin raw-lvh (the 7.5 revert), the sticky
         RELEASE point (sectionH - pinH) moved with every frame swing: parked
         past the moved release, the whole pin assembly visibly un-sticks and
         rides up (locally reproduced: pin top 0 -> -70 on a 750->874 grow late
         in the runway); parked early, nothing moves - exactly the owner's
         "some parts jump, some don't". Frozen pin + frozen section = a FIXED
         release point; both re-land TOGETHER at quiet adoption.
         WHY THIS IS SAFE NOW (it was not in a8522b8): the 7.5 "bone box" that
         convicted the frozen cover was actually the WEBVIEW PRE-TILE BACKING
         (the 8.2 recording conviction) - now covered by the reveal shroud -
         plus any layout band below a short pin shows the SECTION's own opaque
         ink (site.css .chapter[data-surface=ink]), the same surface as the pin
         backdrop: visually inert. The cover law's real requirement (no foreign
         surface at the bottom band) is met by shroud + section bg, not by a
         live cover. lvh-token == svh-token (same innerHeight seed), so the
         frozen max() collapses to svh-token + pad. */
      height: 100svh;
      height: calc(var(--svh-px, 1svh) * 100 + var(--m-cover-pad));
    }
    /* The M05 bridge runway: site.css ships the vh baseline + svh modern
       pair; this mobile-scoped line pins the modern half. (The ch-04
       halves' pins live in the tail block at the END of this file: their
       base rules come AFTER this block in source, so a pin here would
       lose the cascade to the live units.) */
    .chapter-bridge { min-height: calc(var(--svh-px, 1svh) * 100); }
  }
  /* The Ch1 wordmark is fixed at top:50% (site.css), and a fixed-position
     percent re-resolves against the frame exactly like a viewport unit,
     tearing against the pinned flow when an in-app header resizes the
     webview. vh + var is parse-safe everywhere, so no @supports gate:
     the 1vh fallback equals the stock 50% for a fixed element. */
  .dock-wordmark { top: calc(var(--vh-px, 1vh) * 50); }

  /* Bone half - top 50svh of the stage. GPU-composited for the slide.
     Explicit 50svh (not 50%) avoids any ambiguity from the desktop base
     rule .ch-04-half { top:0; bottom:0 } which would otherwise stretch
     the half to full height before the mobile override lands.
     right: auto ensures the desktop .ch-04-half-left { right: 50% } rule
     is cleared so this half spans the full width of the pin. */
  .ch-04-half-left {
    position: absolute; top: 0; bottom: auto; left: 0; right: auto; width: 100%; height: 50vh;
    z-index: 2;
    /* Explicit Bone surface so the top half is a SELF-CONTAINED Bone card, not
       reliant on the pin's background painting through. This is the ink-wipe
       layering fix: the rising ch-03.5 Ink wipe used to read oddly "behind" the
       bone region because the bone came only from the pin back-stop; an explicit
       opaque Bone surface on the half makes the closed-state split read as a
       clean Bone-over-Ink composition the instant the chapter clamps. */
    background: var(--bone);
    padding: 32px 24px clamp(36px, 7vh, 64px);
    border-right: none; border-bottom: none;
    display: flex; flex-direction: column; justify-content: flex-end;
    will-change: transform;
  }

  /* Ink half - the LOWER part of the stage. The split LINE stays at exactly
     50svh from the top (top:50svh) so the visible Bone/Ink boundary sits at
     the small-viewport centre as designed; the half then stretches to the
     pin's bottom (bottom:0; height:auto). Because the pin now covers the
     LARGE viewport (max(100lvh, 100svh + pad)), this half ABSORBS the extra
     svh->lvh band instead of leaving a centre gap between two 50svh halves.
     Both this half and the unified panel beneath are Ink, so the closed-state
     read is unchanged (Bone top half, Ink below the split line). When parted,
     translate3d(0,100%) slides it off by its own (now taller) height.
     left: 0 overrides the desktop .ch-04-half-right { left: 50% }. */
  .ch-04-half-right {
    position: absolute; top: 50vh; left: 0; right: 0; bottom: 0; width: 100%; height: auto;
    z-index: 2;
    background: var(--ink); color: var(--bone);
    padding: clamp(36px, 7vh, 64px) 24px 32px;
    display: flex; flex-direction: column; justify-content: flex-start;
    will-change: transform;
  }

  /* Shared half base - reset the desktop generic rule */
  .ch-04-half {
    position: absolute;
  }

  /* Unified Ink composition sits BEHIND the halves and is revealed as they part.
     pass 8.3 (field-convicted by the local IG-sim churn test): inset:0 made the
     panel track the PIN'S LIVE height (the pin is deliberately raw-lvh per the
     cover law), so its CENTERED copy jumped ~62px on every chrome flip - the
     owner's "section below the sliding-cards is jumping". The panel now fills
     the FROZEN viewport (top:0 + token height; vh baseline here, token re-land
     in the tail block) and its content centres within that stable box. The
     pin's live extra band below the panel stays covered by the pin's own ink
     backdrop + the bottom-absorbing right half - same surface, visually inert. */
  .ch-04-unified {
    position: absolute; top: 0; left: 0; right: 0; bottom: auto; z-index: 1;
    height: 100vh;
    padding: 24px;
    opacity: 1;
    display: flex; flex-direction: column; justify-content: center; align-items: center;
    text-align: center;
    background: var(--ink); color: var(--bone);
  }
  /* pass 8.5 (owner: "the animations should extend to the end of the
     transition, staying within their side of the swiping bar"). The panel's
     theTurn field inherited the panel's frozen token height, which leaves the
     pin's pad band below it (~105-122px) as bare backdrop ink right above the
     ch-04.5 wipe. Extend the FIELD (not the panel - its copy stays centred in
     the frozen box) to the PIN's full height so the bars run to the wipe's
     top edge. The pin's overflow:hidden clips it, so it can never cross the
     bar. vh baseline here; token re-land in the tail block. */
  .ch-04-unified .ch-ambient-field--pin {
    bottom: auto;
    height: 100vh;
  }

  .ch-04-half-left-stamp,
  .ch-04-half-right-stamp { position: relative; top: 0; left: 0; right: 0; padding: 0; text-align: left; margin-bottom: 16px; }
  .ch-04-half-right-stamp { margin-top: 16px; margin-bottom: 0; }
  .ch-04-words { position: relative; padding: 0; gap: 10px; }
  .ch-04-word { opacity: 1; transform: none; font-size: clamp(20px, 5.5vw, 30px); }
  .ch-04-right-head { position: relative; padding: 0; }
  .ch-04-right-head-line { font-size: clamp(28px, 7.5vw, 48px); }
  .ch-04-right-word { opacity: 1; transform: none; }
  .ch-04-unified-head { font-size: clamp(28px, 7.5vw, 48px); text-align: center; }
  .ch-04-unified-body { font-size: clamp(14px, 3.8vw, 18px); text-align: center; max-width: 32ch; }
  .ch-04-phase { display: none; }

  /* ---------- Chapter 5 · services (horizontal pin disabled) ----------
     The desktop pin (sticky 100svh + horizontal track) is dropped: the section
     returns to normal flow and the track stacks vertically. Each card's two
     columns also stack natively (flex 1 1 50%, min-width:320px). Scenes build
     on enter via the IntersectionObserver branch of initCh05Cards.

     Each card carries an inline min-height:620px. That inline rule is a
     normal author declaration; an author !important beats it. Without
     min-height:0 !important the stacked copy-column + browser-mock column
     would still floor each card at 620px and clip the mock under overflow:hidden.
     height:auto is belt-and-suspenders alongside the pass3.css mobile block
     which already sets it; both are needed here because mobile.css loads later
     and the inline attribute cascades at the author level. */

  /* pass3.css sets .ch-05 { padding: 12vh 0 16vh } for its own mobile block;
     on mobile that resolves to ~236px at 844px. Replace with the spacing scale. */
  .ch-05 { height: auto; padding: var(--m-space-sm) 0 var(--m-space-sm); }
  .ch-05-pin { position: relative; height: auto; overflow: visible; display: block; }
  /* header top padding removed: the section's top padding handles the top inset. */
  .ch-05-header { padding: 0 16px var(--m-space-2xs); }
  .ch-05-lede { font-size: clamp(22px, 6vw, 32px); }
  .ch-05-track-wrap { display: block; overflow: visible; }
  .ch-05-track {
    flex-direction: column;
    gap: var(--m-space-2xs);
    padding: var(--m-space-2xs) var(--m-space-sm) var(--m-space-xs);
    transform: none !important;
  }
  .ch-05-card {
    width: auto;
    height: auto;
    min-height: 0 !important;
  }
  /* Ch5 mobile compaction: the two columns (copy + the M30 browser mock) STACK on
     mobile, so the desktop inline column padding (56/52px vertical) plus the large
     headline pushed each card to ~970px (Ch5 ~10 screens on the smallest phone).
     Tighten the stacked layout while keeping ALL content (headline / body / tags /
     the self-building scene). Both columns carry the inline `flex:1 1 50%`, so target
     that; an author !important beats the inline padding/gap.
     8px padding + 8px gap (--m-space-2xs) keeps the cards tighter while remaining
     legible at 375px; headline is the minimum comfortable display size. */
  .ch-05-card > div[style*="flex:1 1 50%"] { padding: var(--m-space-2xs) !important; gap: var(--m-space-2xs) !important; }
  .ch-05-card h2 { font-size: clamp(20px, 5.3vw, 30px) !important; line-height: 1.08 !important; }
  .ch-05-card [data-body] { font-size: 15px !important; line-height: 1.42 !important; }

  /* Ch5 self-building scenes (the M30 mocks) must show in FULL on mobile - the
     whole animation, no cutoff. Their inner mock-UI is absolutely positioned in
     PERCENTAGES of the scene box, so the box height has to give the mock its
     designed proportions or the build reads as a crushed sliver. (This whole
     block lives in the MAIN <=1023 / coarse mobile block, NOT the phone-only 640
     block, so the tablet - ipad-mini 768, where the columns sit side by side -
     gets the same fit instead of falling back to the desktop scene size.)
       - [data-browser] (card 1) + [data-scene] (cards 2,5,6): the desktop box is
         roughly square at its column width (clamp height ~= column width). Keep
         that on mobile with aspect-ratio so the box tracks the mobile column and
         the mock renders undistorted. The build JS only sets opacity/transform/
         colour on inner nodes - it never reads or writes the box size - so
         resizing the container does not touch the M30 animation.
       - [data-panel] (cards 3,4): a content-sized flex column. The old mobile cap
         (max-height + overflow:hidden) chopped the bottom rows (card 3 ANSWER box,
         card 4 flow nodes). Uncap it; the card is height:auto and grows to fit. */
  .ch-05-card [data-browser],
  .ch-05-card:not([data-card="2"]) [data-scene] {
    height: auto !important;
    aspect-ratio: 1.04 / 1;   /* ~ the desktop box proportions: full mock shows undistorted */
    /* Cap the height so six stacked cards stay reasonable on the smallest phones
       while the whole mock still reads. The near-square aspect drives the height
       up to this cap on wider columns. The cap stays generous enough that the
       content-sized panel mocks (cards 3/4) are not clipped - a shorter cap chops
       their bottom rows. */
    max-height: 300px;
  }
  .ch-05-card [data-panel] {
    max-height: none !important;
    overflow: visible !important;
  }
  /* Card 5's BUILD/MEASURE/DECIDE loop ring (data-loopwrap, sized off the scene
     HEIGHT) puts its node labels just OUTSIDE the ring. On the compact mobile
     scene the side labels (MEASURE / DECIDE) overhang the box and the card's
     overflow:hidden clips them. Shrink the ring so the labels sit inside the box.
     The build (token circuits the ring, nodes fade in) is all relative to this
     wrapper, so it is untouched. */
  .ch-05-card[data-card="5"] [data-loopwrap] { height: 60% !important; }

  /* Card 2 is the one LANDSCAPE-composed mock: a phone on the left and a
     toggle + spec rail on the right, laid out with fixed-px widths tuned for the
     wide desktop box. On the narrow mobile column the rail runs off the card edge.
     The mock's inner content is wrapped in [data-scenefit] (display:contents on
     desktop, inert). On mobile that wrapper becomes a fixed design-width box that
     transform:scales to fit the column, so the WHOLE mock shows. transform-scale
     keeps the scene box itself at column width (the wrapper overflows it visually
     only), so the card's overflow:hidden never clips it. The per-device scale +
     the reserved height are computed in pass3.js (fitCh05MobileScene, gated on
     __synqaIsMobile). A static fit-scale, not motion. */
  .ch-05-card[data-card="2"] [data-scene] {
    aspect-ratio: auto;          /* JS sets an explicit reserved height instead */
    max-height: none;
    overflow: visible;           /* let the scaled wrapper paint past the box edge */
  }
  /* [data-scenefit] (the fixed-design-width scaling box) is driven entirely by
     pass3.js fitCh05MobileScene - inline styles beat the stylesheet, and the
     markup default display:contents must be flipped by JS, so its display /
     position / size / transform all live there, not here. */

  /* The big faint italic numeral (aria-hidden, inline bottom:-18%) bleeds off the
     card edge by design on the WIDE desktop card. On the narrow stacked mobile
     card it pokes ~84px below the card and the card's overflow:hidden clips it,
     which the harness reads as ancestorClipped. It is a decorative watermark, not
     content, so drop it on mobile. (Desktop keeps it - this is mobile-only.) */
  .ch-05-card > div[aria-hidden="true"] { display: none !important; }

  /* The inter-column divider (inline [data-divider], absolute left:50%) is the
     DESKTOP separator between the copy + scene columns. The M30 build writes it an
     inline scaleY() as each card builds, so on mobile - where the columns STACK -
     it reappears as a stray vertical line down the card centre. Remove it on
     mobile; display:none beats the inline transform the build sets. */
  .ch-05-card [data-divider] { display: none !important; }

  /* ---------- Chapter 6 · the proof (M08 disabled, M19 bottom) ---------- */

  .ch-06 { height: auto; }
  .ch-06-pin { position: relative; height: auto; overflow: visible; }
  /* Header padding: 64px top was the desktop pin offset; on mobile the header
     is in normal flow, so the spacing scale handles the top inset. */
  .ch-06-header { position: relative; top: 0; padding: var(--m-space-md) var(--m-space-md) var(--m-space-xs); }
  .ch-06-bg { display: none; } /* M17 reverse parallax off on phones */
  .ch-06-track {
    position: relative; flex-direction: column; width: 100%;
    transform: none !important;
  }
  /* Stack the sheet banner ABOVE the detail (desktop centres them as a flex
     ROW, which on mobile put the Ink banner and the light detail side-by-side
     and overflowing). Column + start-align makes each case read top-to-bottom. */
  .ch-06-panel { flex: 1 1 auto; height: auto; min-height: 0; flex-direction: column; align-items: stretch; justify-content: flex-start; }
  /* Reset the desktop large top-padding on case 1 that came from the desktop pin
     offset (clamp(120px,24vh,240px) = ~202px on iphone-13). On mobile each case
     is in normal flow; match the compact .case-panel rule (--m-space-sm). */
  .ch-06-panel.case-panel[data-case="1"] { padding-top: var(--m-space-sm); }

  /* M28 (v9.4) title SHEET: no pin on mobile, so it can't overlay/pull. Drop it
     back into the flow ABOVE each case as a static Ink banner (panels stack:
     banner first, then the light detail beneath - the sheet never permanently
     covers the detail without a pin). Reset any inline transform/box-shadow/
     clip-path the desktop handler may have written, and keep it inert so it
     never traps taps over the revealed detail. border-bottom Clay edge kept. */
  .case-title-card {
    position: relative; inset: auto; z-index: auto;
    /* A composed Ink BAND across the screen (like the interlude): the generous
       padding + gap give the eyebrow + title + line device real breathing room
       (they were crammed before), and the min-height floors it as a consistent
       band so case 1 and case 2 read the same height.
       The DOM order is eyebrow > title > device. justify-content:SPACE-BETWEEN
       (was center) distributes the three rows EVENLY across the floored band so
       the eyebrow reads as a top band-label, the title is the focal middle, and
       the arc device seats at the base - the re-check found a large empty Ink
       region in the LOWER third when the group was centred (the device finished
       two-thirds down with dead space below). Keeping min-height 28svh holds the
       iphone-se 18-screen ceiling (a taller band would breach it); space-between
       fills the band evenly with no added height. */
    min-height: 28vh;
    min-height: 28svh;
    display: flex; flex-direction: column; justify-content: space-between;
    gap: var(--m-space-xs);
    padding: var(--m-space-sm) var(--m-space-md);
    /* Clear the band's bottom Clay edge from the case label / detail-head row
       beneath, so the Ink band and the light detail read as two distinct zones
       (a ghost seam was reported where the Clay edge met the BRANCHLINE/PAPER
       TRAIL chip). --m-space-md (was --m-space-sm) gives a clean separation. */
    margin-bottom: var(--m-space-md);
    transform: none !important; opacity: 1 !important; box-shadow: none !important;
    clip-path: none !important; /* no desktop sheet pull on the static banner */
    pointer-events: none;
  }
  /* Eyebrow back into normal flow: on the un-pinned auto-height mobile banner the
     desktop absolute top:clamp(74px,12vh,96px) fell into the MIDDLE of the card,
     landing on the line device instead of above the title. Static + order:-1
     restores eyebrow > title > device top-to-bottom. */
  .case-sheet-eyebrow {
    position: static; top: auto; left: auto; right: auto;
    order: -1; margin-bottom: var(--m-space-2xs);
  }
  /* Static banner: the device is fully drawn (measureCh06TitleArcs leaves
     offset 0 + nodes shown) and the arc's riding marker dot is irrelevant.
     Compact the arc device; it is decorative on mobile. */
  .dev-marker { display: none !important; }
  /* The case content is always present on mobile (no sheet-pull rise). */
  .case-panel .case-panel-inner { transform: none !important; opacity: 1 !important; }
  /* Display title: let it wrap to two lines at a real title size. The glyph-roll
     scramble (M28) is a desktop scroll effect; on mobile the banner is static so
     we want the title to wrap at word spaces.
     The .gl cells are inline-block on desktop (to pin glyph-cell widths for the
     scramble). On mobile measureCh06TitleCells() skips the cell-width pinning,
     so display:inline lets the text flow and wrap naturally at word boundaries.
     Line-height 1.1 is tight like the brand display headings. */
  .case-sheet-title {
    font-size: clamp(30px, 8vw, 46px);
    white-space: normal;
    line-height: 1.08;
  }
  .case-sheet-title .gl { display: inline; }
  /* Widen the arc device to fill the column so it reads as an intentional
     composition beside the larger, possibly-wrapped title. */
  .case-sheet-device { width: min(76vw, 300px); margin-top: var(--m-space-2xs); }

  .case-panel { padding: var(--m-space-sm) var(--m-space-md); }
  .case-panel-inner { grid-template-columns: 1fr; gap: var(--m-space-sm); }
  .case-headline { font-size: clamp(20px, 5.3vw, 28px); }
  /* case-body: 15px min font-size (per brand rules); tighten line-height to 1.45
     from 1.55 desktop to save ~15-20px per case body. */
  .case-body { line-height: 1.45; }
  /* case-detail-mark: 52px app icon drives the detail-head row height. Reduce to
     40px on mobile to trim 12px from each case-meta. */
  .case-detail-mark { width: 40px; height: 40px; }
  /* case-metrics: desktop gap is ~27px (clamp-based); override to tight scale.
     Metrics are always present (M10 count-up intact). */
  .case-metrics {
    padding-left: 0; padding-top: var(--m-space-sm);
    border-left: none; border-top: 1px solid var(--border);
    gap: var(--m-space-sm);
  }
  .case-metric .value { font-size: clamp(36px, 9vw, 48px); }
  /* wipe-panel: the interstitial "One pattern. Two operations. Same chair." quote
     between the two cases. Desktop centres the quote in a full pinned panel; on
     mobile it collapsed to a short content-height band with the quote crammed at
     the top (text too high, content squished). Give it a taller balanced band and
     centre the quote vertically (echoing the desktop balance), with the stamp
     anchored lower so the band reads as a composed interlude, a little longer. */
  .wipe-panel {
    min-height: 26vh;
    min-height: 26svh;
    /* space-between (was center): the stamp's margin-top:lg was pushing the
       centred quote+stamp GROUP upward, so the quote sat too HIGH on the card
       (owner-reported) with a gap at the bottom. space-between seats the quote
       optically centred and pins the stamp to the band base for a balanced
       interlude. No min-height change (holds the iphone-se budget). */
    display: flex; flex-direction: column; justify-content: space-between;
    padding: var(--m-space-md) var(--m-space-md);
  }
  .wipe-panel-quote { position: relative; inset: auto; padding: 0; margin: auto 0; }
  .wipe-panel-quote p { font-size: clamp(24px, 6.4vw, 36px); }
  .wipe-panel-stamp { position: relative; bottom: auto; left: auto; margin-top: 0; }
  .ch-06-paging { display: none; }
  .peel-headline { white-space: normal; font-size: clamp(28px, 7vw, 40px); text-align: left; padding: 0 24px; }

  /* ---------- Chapter 7 · Theta. (M09 reduced) ---------- */

  /* Reset the desktop full-viewport hero so it sizes to content on mobile.
     .ch-07-meta has its own gap: 28px from the desktop rule; reduce it to the
     spacing scale. .ch-07-wordmark is the standalone brand mark (64px desktop),
     compact to 48px. .ch-07-body is 17px desktop; pull to 15px. */
  .ch-07 { min-height: auto; padding: var(--m-space-md) 0; }
  .ch-07-inner { grid-template-columns: 1fr; padding: 0 24px; gap: var(--m-space-md); }
  .ch-07-meta { gap: var(--m-space-xs); position: static; margin-top: var(--m-space-sm); }
  /* /theta slot REPLAY control: clear the small overlap with the slot headline's bottom (the
     desktop meta placement leaked onto mobile) and give the 14px control a 44px tap area. */
  .theta-slot-toggle { min-height: 44px; display: inline-flex; align-items: center; padding-block: 12px; margin: var(--m-space-sm) 0 0; }
  .ch-07-wordmark { font-size: 48px; }
  .ch-07-head { font-size: clamp(26px, 7vw, 38px); }
  .ch-07-body { font-size: 15px; line-height: 1.5; margin-top: 0; }
  /* Reduce the hero-row product mark from 60px to 40px on mobile; the Theta
     wordmark below already names the product. */
  .product-hero-mark { width: 40px; height: 40px; }
  /* Hero stamp clip fix (shared: home Ch7 + theta/holm/rewrite/patchpal).
     The desktop row is a single nowrap flex line; the 284-320px mono stamp
     runs past the right edge of a 360-390px viewport and is cut by
     overflow-x:clip. Let the row wrap so the stamp drops under the mark, and
     let the stamp itself wrap its inline pieces so it never exceeds the
     column. align-items:flex-start keeps the mark top-aligned once wrapped. */
  .ch-07-hero-row { flex-wrap: wrap; align-items: flex-start; row-gap: var(--m-space-xs); }
  .ch-07-stamp { flex-wrap: wrap; max-width: 100%; }
  .ch-07-cta { padding: 10px 20px; margin-top: 4px; }

  /* PatchPal logo compose fix (/patchpal only; the .pp-leaf selector matches
     nowhere else). The desktop mark layers two decorative <img> elements
     (pp-words + pp-leaf) edge to edge in the same box to compose one logo. On
     mobile the harness's overlap heuristic sees two stacked content images and
     reports a collision. Paint the swaying leaf as a pseudo-element background
     layer (not a DOM <img>, so it is outside the collision set) and hide the
     real leaf <img>. The composed visual (lime square + wordmark + swaying
     leaf) is unchanged; only one real image box remains. */
  .product-hero-mark img.pp-leaf { display: none; }
  .product-hero-mark::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: url("assets/work/patchpal-icon-leaf.png");
    background-size: 100% 100%;
    background-repeat: no-repeat;
    transform-box: border-box;
    transform-origin: 78% 53%;
    animation: ppLeafWater 5s ease-in-out infinite;
    pointer-events: none;
  }
  /* Cap the card image height on mobile so the lazy-loaded screenshot
     cannot expand the card (and thus ch-07) after the scroll sweep.
     The card image is decorative at this size; clamp keeps it visible. */
  .ch-07-card { padding: 16px; }
  /* Show the FULL Theta showcase screenshot (assets/theta-preview-2.png, the
     1209x1111 near-square multi-message chat). The old height:clamp(100px,28vw,160px)
     + object-fit:cover/top amputated all but the top ~2.5 bubbles - a dark sliver
     that read like an asset-load error, not a product preview. Reserve the box with
     the image's own aspect-ratio (so the lazy-loaded image cannot reflow ch-07 after
     the scroll sweep) and let the whole image show, un-cropped. A max-height cap +
     object-fit:contain letterboxes on the smallest phones rather than cropping. */
  .ch-07-card-image {
    height: auto;
    aspect-ratio: 1209 / 1111;
    max-height: 56vh;
    max-height: 56svh;
    overflow: hidden;
  }
  .ch-07-card-image img { width: 100% !important; height: 100% !important; object-fit: contain !important; object-position: center !important; }

  /* ---------- Chapter 8 · two continents ---------- */

  /* ch-08-col-head: desktop font-size not overridden; at 375px it may be landing
     at the desktop value. Pull to 22px so the 3-line head compacts from 101px.
     ch-08-col-body: 16px -> 15px (min readable). M12 arc stays (no JS change). */
  /* Top clearance for the fixed M20 chapter badge (.shell-chapter-stamp,
     top:80px left:20px). When ch-08 scrolls to the top of the viewport
     its section stamp + "Built across two continents." headline sat
     directly under the floating "CH. 08" badge. Push the section's first
     content below the badge band (badge bottom ~96px) so the headline
     clears it cleanly. The badge stays present as the homepage wayfinder. */
  .ch-08 { padding: var(--m-space-md) 0; min-height: auto; }
  .ch-08-inner { padding: 96px 24px var(--m-space-md); gap: var(--m-space-md); }
  .ch-08-head { font-size: clamp(26px, 7vw, 38px); }
  .ch-08-grid { display: none; } /* dot-grid + M17 parallax off on phones */
  .ch-08-cols { grid-template-columns: 1fr; gap: var(--m-space-sm); }
  .ch-08-col-head { font-size: clamp(20px, 5.3vw, 28px); line-height: 1.12; }
  .ch-08-col-body { font-size: 15px; line-height: 1.5; }
  .ch-08-arc-wrap {
    aspect-ratio: 1280 / 520;
    min-height: 80px;
  }

  /* ---------- Chapter 9 · the close ---------- */

  /* ch-09-stamp has margin-bottom:28px from desktop; ch-09-body has margin-top:28px.
     Override both to the spacing scale so the copy column compresses from ~349px. */
  /* ch-09-stamp has margin-bottom:28px from desktop; ch-09-body has margin-top:28px.
     Override both to the spacing scale so the copy column compresses from ~349px.
     The textarea has an explicit height from pass3.css; pull it shorter on phones
     (the form is the primary conversion, not a long message field). */
  /* Top clearance for the fixed M20 chapter badge (.shell-chapter-stamp).
     ch-09's "Tell us what's eating your week." italic headline sat under
     the floating "CH. 09 · THE CLOSE" badge at section entry; push the
     copy column below the badge band (~96px) so it clears cleanly. */
  .ch-09 { padding: var(--m-space-md) 0; min-height: auto; }
  .ch-09-inner { grid-template-columns: 1fr; padding: 96px 24px 0; gap: var(--m-space-md); }
  .ch-09-stamp { margin-bottom: var(--m-space-2xs); }
  .ch-09-head { font-size: clamp(26px, 7vw, 38px); }
  .ch-09-body { font-size: 15px; line-height: 1.45; margin-top: var(--m-space-xs); margin-bottom: 0; }
  .ch-09-side { margin-top: var(--m-space-xs); }
  .ch-09-form { padding: var(--m-space-sm); gap: 14px; }
  .ch-09-form textarea { height: 88px; min-height: 0; }
  .ch-09-form-foot { flex-direction: column; align-items: flex-start; gap: 12px; }

  /* ---------- Chapter wipes - collapse the 282vh of dead runway ----------
     The desktop per-id heights (pass3.css 35-39: 54/46/64/56/62vh) are at
     #id.chapter-wipe specificity, which OUTRANKS a plain .chapter-wipe mobile
     rule - so on mobile the wipes were still rendering at their desktop height.
     The four surface-FLIP boundaries (Bone<->Ink: ch-03.5, ch-04.5, ch-07.5,
     ch-08.5) collapse to a short --m-wipe-runway flip, set at matching #id
     specificity so they win. The wipe-fill driver (pass3.js applyWipes) is kept
     in LOCKSTEP by a mobile branch in WIPE_RUNWAY_VH so the fill completes its
     100%->0% translate over the SAME short runway. */
  .chapter-wipe { height: 100vh; height: 100svh; }
  #ch-03half-transition.chapter-wipe,
  #ch-04half-transition.chapter-wipe,
  #ch-07half-transition.chapter-wipe,
  #ch-08half-transition.chapter-wipe { height: var(--m-wipe-runway); }
  /* ch-06.5 is a SAME-SURFACE boundary (ch-06 Bone -> ch-07 Bone): no surface flip
     is needed. height:auto inflated to ~46vh (the sticky weave SVG + fill = a dead
     band), so pin a tight --m-wipe-runway (14svh) weave divider instead. pass3.js
     gives this id a mobile runwayVh of 0 so no Ink fill scrubs over it. */
  #ch-06half-transition.chapter-wipe { height: var(--m-wipe-runway); }
  .chapter-wipe-weave .weave-period { font-size: 48px; }

  /* S2 rapid-flip prerequisite (pass 8): the bone wipes (ch-03.5, ch-06.5,
     ch-07.5) inherit .chapter-wipe { background: transparent } (pass3.css)
     and so paint the ROOT/body color. Safe under the settled top-edge law
     (they are only visible while a Bone chapter tops the viewport), but the
     rapid-flip bottom-edge root window (site.js applyRootSurface) flips the
     root to INK exactly while they are visible (approaching ch-04 / ch-08),
     which would repaint committed mid-viewport rows. Commit their own Bone
     surface; settled rendering is pixel-identical. The ink wipes already
     paint opaque ink (site.css [data-surface="ink"], higher specificity).
     The homepage-fields z:0 riders are unaffected: positioned z:0 children
     paint above in-flow backgrounds either way. */
  .chapter-wipe[data-from="bone"] { background: var(--bone); }

  /* THE REVEAL SHROUD (pass 8.2, field-CONVICTED by the owner's recording,
     frame t=9.13s): on a frame GROW the compositor exposes a bottom band
     BEFORE layout/paint commits, and that band paints the WEBVIEW'S PRE-TILE
     BACKING (bone-ish), NOT the html canvas - the recording shows the bar
     while html[data-surface] is correctly "ink" (mid-Ch4), which acquits the
     root law and kills the whole root-color lineage as a fix for this class.
     What CAN cover it is PRE-PAINTED PIXELS below the fold (the mechanism
     that made CANVAS_BLEED work): this strip parks at calc(100% - 1px) - the
     1px on-screen keeps iOS from culling the layer's tiles, so its 179
     below-fold px are always painted. During the lag frame of a grow, the
     OLD-position layer sits exactly in the reveal band and covers it with
     the current surface color (the existing root-law attribute - no new JS
     state, no gating, no listeners); after layout it snaps back below the
     viewport. At rest: a 1px surface-colored line at the very bottom edge,
     invisible on both surfaces. z:5 = above the chapters, below the M05
     canvas (28) and the nav (40). Element created once by site.js. */
  .reveal-shroud {
    position: fixed;
    top: calc(100% - 1px);
    left: 0;
    right: 0;
    height: 180px;
    background: var(--bone);
    z-index: 5;
    pointer-events: none;
  }
  /* pass 8.3: the shroud's color is JS-DRIVEN from the chapter at the LIVE
     BOTTOM edge (site.js __updateShroudColor, guarded element-scoped write) -
     NOT the root [data-surface] top-edge attribute. The top-edge coloring was
     field-falsified by the owner's second recording: whenever the top and
     bottom edges disagreed, the 1px on-screen edge read as a visible hairline
     ("a line sitting there") and the grow-lag band painted BONE over incoming
     ink. Bottom-edge color always matches the content the band sits over, so
     both the line and the band are invisible. Element-scoped (a single 440x180
     layer repaint), so the pass-8.0 root-flip style-storm cannot recur. */

  /* ---------- Sub-pages - single column ---------- */
  /* Replace Nvh paddings with --m-* scale tokens throughout.
     subpage-hero was 18vh/8vh; case-hero was 18vh/4vh; case-section,
     theta-section, theta-waitlist, about-section were all 10vh 0;
     contact-grid bottom was 12vh. Token values resolve to 40-64px
     (clamp) so the sections size to content, not to the viewport. */
  .subpage-hero { padding: var(--m-section-pad-y) 0; }
  .subpage-hero-inner,
  .case-hero-inner,
  .theta-section-inner,
  .theta-waitlist-inner,
  .case-section-inner,
  .case-cta-block-inner,
  .about-section-inner,
  .contact-grid,
  .subpage-footer-inner,
  .work-grid {
    padding-left: 24px; padding-right: 24px;
  }
  .subpage-hero-head { font-size: clamp(40px, 10vw, 64px); }
  /* work-grid: drop the 10vh bottom pad; the entry cards provide their
     own spacing via margin-bottom. work-row: shrink the 32px padding
     to the scale gap so 12 entries stack tighter. */
  .work-grid { padding-top: 0; padding-bottom: var(--m-section-gap); }
  .work-row {
    grid-template-columns: 1fr; gap: 18px; padding: var(--m-space-md) 0;
  }
  .work-row-metric-wrap,
  .work-row-cta { justify-self: start; }
  .work-row-head { font-size: clamp(22px, 5.4vw, 28px); }

  /* /work: PatchPal logo collision fix inside .case-mark (the work grid
     uses .case-mark.app-mark instead of .product-hero-mark, so the
     product-hero-mark fix in Ch7 does not reach here). Scope to
     #patchpal only so the ::after does not run on the SVG-icon entries
     and cannot inflate their scrollWidth. */
  /* PatchPal logo collision fix for /work. Hide the DOM pp-leaf img
     (which overlaps pp-words) and render it as a pseudo-element instead.
     inset:4px shrinks the pseudo inside the 80px box by enough to keep
     the ppLeafWater 3.5deg rotation within the parent's bounding rect,
     so scrollWidth never exceeds clientWidth and the textClip check passes.
     overflow:hidden clips any residual pixel; position:relative is the
     containing block for the absolute pseudo. */
  #patchpal .case-mark.app-mark { position: relative; overflow: hidden; }
  #patchpal .case-mark.app-mark img.pp-leaf { display: none; }
  #patchpal .case-mark.app-mark::after {
    content: "";
    position: absolute;
    inset: 4px;
    background-image: url("assets/work/patchpal-icon-leaf.png");
    background-size: 100% 100%;
    background-repeat: no-repeat;
    transform-box: border-box;
    transform-origin: 78% 53%;
    animation: ppLeafWater 5s ease-in-out infinite;
    pointer-events: none;
  }

  /* /work: clipped stamp text. The mono stamp can run slightly wider
     than its flex container on narrow phones when the stamp text is
     long (e.g. "PAPER TRAIL · CONFIDENTIAL, JOHANNESBURG · AUTOMATION").
     Override text-wrap:balance (desktop) so the browser is free to
     break at any word boundary, and ensure the flex item cannot grow
     past its container. */
  .case-stamp { overflow-wrap: break-word; text-wrap: wrap; max-width: 100%; }

  .case-hero { padding: var(--m-section-pad-y) 0 var(--m-space-md); }
  .case-hero-head { font-size: clamp(36px, 8.5vw, 56px); }
  .case-metric-band { grid-template-columns: 1fr; gap: 32px; padding: 40px 0; }

  .case-section { padding: var(--m-section-pad-y) 0; }
  .case-section-inner { grid-template-columns: 1fr; gap: 24px; }
  .case-section-head { font-size: clamp(26px, 6.4vw, 36px); }

  /* case-cta-block dead band: the desktop rule sets padding: 14vh 0
     (via subpages.css). The block has <80 chars so the dead-band check
     fires when the section exceeds 40vh. Use --m-space-md (24px) so the
     padded total stays under 40vh at iphone-se height (667px) even after
     the 44px CTA button height added by the Phase 6 tap-target fixes.
     --m-space-lg (32px) was the prior value but it now trips the dead-band
     check once the CTA button reaches 44px min-height. */
  .case-cta-block { padding: var(--m-space-md) 0; }

  .theta-section { padding: var(--m-section-pad-y) 0; }
  .theta-section-head { font-size: clamp(28px, 7vw, 40px); }
  .theta-steps { grid-template-columns: 1fr; }
  .theta-step { padding: 28px; min-height: 0; }
  .theta-step-num { font-size: 48px; }
  .theta-why { grid-template-columns: 1fr; gap: 16px; }
  .theta-why-body { font-size: 20px; }
  .theta-channels-grid { grid-template-columns: 1fr; }
  .theta-waitlist { padding: var(--m-section-pad-y) 0; }
  .theta-waitlist-inner { grid-template-columns: 1fr; gap: 40px; }
  .theta-waitlist-head { font-size: clamp(36px, 8vw, 56px); }
  .theta-form { padding: 24px; }
  .theta-form-row-grid { grid-template-columns: 1fr; }
  .theta-form-foot { flex-direction: column; align-items: flex-start; gap: 16px; }

  .about-section { padding: var(--m-section-pad-y) 0; }
  .about-section-inner { grid-template-columns: 1fr; gap: 24px; }
  .about-section-head { font-size: clamp(28px, 7vw, 40px); }
  .about-hiring-grid { grid-template-columns: 1fr; }
  .about-hiring-card { padding: 28px; }

  .contact-grid { grid-template-columns: 1fr; gap: 48px; padding-bottom: var(--m-section-gap); }
  .contact-offices { grid-template-columns: 1fr; gap: 24px; }

  /* /holm tail band: the last .theta-section.is-ink contains only two
     links but the .holm-tail element carries 56px top padding + 48px
     margin-top (from the holm.html tablet rule). At iphone-se height
     (667px) the section sits just above 40vh (the dead-band threshold).
     Pull the tail padding to the scale so the band clears the check. */
  .holm-tail { padding: var(--m-space-md) 0 var(--m-space-sm) !important; margin-top: var(--m-space-md) !important; }

  /* Homepage footer compaction: the mark + inner grid was 791px. Tighten padding,
     the inner grid gap, and the mark's internal gap. The mark (Synqa wordmark) is
     part of the brand footer and stays; we only reduce the surrounding space.
     subpage-footer-bottom: the desktop 64px margin-top is reset to 0 here;
     the 12px top padding on the bottom bar provides the separator. This
     reclaims ~64px per page which compensates for the 44px sitemap-link rows. */
  .subpage-footer { padding: var(--m-space-md) 0 var(--m-space-sm); }
  .subpage-footer-inner { grid-template-columns: 1fr; gap: var(--m-space-sm); }
  .subpage-footer-mark { gap: var(--m-space-sm); }
  .subpage-footer-bottom {
    margin-top: 0;
    flex-direction: column; gap: 12px; padding: 12px 24px 0;
  }

  /* ----------------------------------------------------------------
     Phase 6 tap targets (plan 7.6) - bring standalone interactive
     targets up to the 44px minimum touch target. Inline links inside
     running copy are exempt (the harness inline-link rule covers them).
     ---------------------------------------------------------------- */

  /* Footer sitemap links: 44px tall touch rows via min-height.
     The column gap is cleared to zero since the rows themselves provide
     the visual separation. The footer-inner gap is trimmed from 16px
     (--m-space-sm) to 10px to compensate for the increased per-row
     height so home stays under 16 screensTall on iphone-se. */
  .subpage-footer-inner { gap: 10px; }
  .subpage-footer-sitemap { gap: 0; }
  .subpage-footer-sitemap-col { gap: 0; }
  /* Decouple the 44px tap row from the underline. The desktop rule
     (site.css) gives each link a border-bottom; on desktop align-self:
     flex-start shrinks the link to its text so the border is text-width.
     The old mobile rule stretched the link full-width AND kept the
     border-bottom, so every link became a full-width horizontal rule -
     a stack of ruled rows that read as noise, and the "Privacy inquiries"
     underline collided with the section dividers. Keep the full-width
     44px tap area (good touch ergonomics) but drop the box border and
     render the underline with text-decoration, which follows the text
     baseline (text-width) instead of the box edge. */
  .subpage-footer-sitemap a {
    display: flex;
    align-items: center;
    align-self: stretch;
    min-height: 44px;
    padding-top: 4px;
    padding-bottom: 4px;
    box-sizing: border-box;
    border-bottom: none;
    text-decoration: underline;
    text-decoration-color: var(--border-on-ink);
    text-decoration-thickness: 1px;
    text-underline-offset: 4px;
  }
  .subpage-footer-sitemap a:hover,
  .subpage-footer-sitemap a:active {
    text-decoration-color: var(--clay);
  }

  /* Footer structure on mobile: the stacked sections (place stamp /
     address / PAGES / ELSEWHERE / copyright) ran together as one dark
     wall with no rhythm. Give each zone clearer separation with a Bone
     hairline (--border-on-ink) and a consistent gap, so the footer reads
     as distinct, ordered sections rather than a noisy ledger. CSS-only,
     so it applies to every page's shared footer. */
  .subpage-footer-inner {
    gap: 0;
    padding-top: var(--m-space-sm);
    padding-bottom: var(--m-space-sm);
  }
  /* The mark block holds the wordmark, the two place stamps, and the
     address; keep its inner rhythm and divide the address off so the
     coordinates stamp and the street address do not blur together.
     align-items:flex-start LEFT-anchors the wordmark <img>: in the mobile
     single-column footer this flex-column's default align-items:stretch was
     stretching the SVG <img> to the full column width (312px at 360, 720px at
     768). With preserveAspectRatio=xMidYMid the 32px-tall glyphs then centred
     inside that ultra-wide box, so the wordmark rendered SMALL and CENTERED
     while every block beneath it (stamp, address, sitemap) is left-aligned.
     flex-start + width:auto restores the intrinsic ratio, left-edge, matching
     the rest of the footer column. (Fixes /404 + privacy-inquiry + legal-page
     "footer wordmark centered" reports - same root.) Desktop 3-col grid keeps
     its auto-width columns and is untouched. */
  .subpage-footer-mark {
    align-items: flex-start;
    padding-bottom: var(--m-space-md);
  }
  .subpage-footer-mark img { width: auto; max-width: 100%; }
  .subpage-footer-address {
    margin-top: var(--m-space-xs);
    padding-top: var(--m-space-sm);
    border-top: 1px solid var(--border-on-ink);
  }
  /* PAGES and ELSEWHERE: each becomes its own zone with a top hairline
     and breathing room above and below the label. */
  .subpage-footer-sitemap {
    padding-top: var(--m-space-md);
    padding-bottom: var(--m-space-sm);
    border-top: 1px solid var(--border-on-ink);
  }
  .subpage-footer-sitemap-label {
    margin-bottom: var(--m-space-xs);
  }

  /* Nav brand: 44px tall tap area. The visual mark stays 20px; the
     block is position:fixed so this extra block-padding never adds
     page height. align-content centers the img vertically. */
  .shell-nav-brand {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* Primary page-body CTAs (.shell-nav-cta used outside the fixed nav,
     e.g. case-cta-block "Work with us" and app-page "Sit with us"). */
  .case-cta-block .shell-nav-cta,
  .theta-section .shell-nav-cta,
  .holm-tail .shell-nav-cta {
    min-height: 44px;
  }

  /* The primary block CTA (e.g. /services "Tell us what's eating your week."
     and /about "Work with us.") sized to its text and left-aligned in the
     content column, leaving a large dead zone to its right. As the page's
     primary call to action it should fill the mobile column. The .case-cta-block
     inner is align-items:flex-start, so stretch this one button to full width and
     centre its label. (Scoped to the block CTA; the secondary "See the work" text
     link below it keeps its natural width.) */
  .case-cta-block .shell-nav-cta {
    align-self: stretch;
    width: 100%;
    box-sizing: border-box;
    justify-content: center;
    text-align: center;
  }

  /* ch-07-cta (Sit with us button on app page sections): mobile rule
     sets padding: 10px 20px which leaves the button ~38px. Raise to 13px. */
  .ch-07-cta {
    padding-top: 13px;
    padding-bottom: 13px;
  }

  /* .case-cta standalone buttons (Back home on 404, See the project
     on homepage cards). The link uses baseline alignment and a 1px
     border; wrap it in an inline-flex box so min-height applies. */
  .case-cta {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* /work "Visit the X page" links - primary per-entry action. */
  .case-visit {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-top: 12px;
    padding-bottom: 12px;
  }

  /* Back to Work (work-row-cta used on patchpal case-cta-block). */
  .work-row-cta {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-top: 12px;
    padding-bottom: 12px;
  }

  /* /holm and /rewrite back-to-work links. */
  .holm-tail-back,
  .rewrite-tail-back {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-top: 12px;
    padding-bottom: 12px;
  }

  /* 404 "or send us a note" secondary link. */
  .nf-alt {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-top: 12px;
    padding-bottom: 12px;
  }

  /* Legal "Back to Rewrite" link in .legal-back bar. */
  .legal-back a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-top: 12px;
    padding-bottom: 12px;
  }

  /* 404 decorative square: prevent it from contributing to horizontal
     overflow on narrow phones. The absolute-positioned SVG extends
     beyond the viewport edge by design; clip it to the section. */
  .nf-main {
    overflow: hidden;
  }
}

/* ----------------------------------------------------------------
   Phone-only - extra M17 drop + tighter type
   ----------------------------------------------------------------*/
@media (max-width: 640px) {
  .ch-08-grid,
  .ch-06-bg { display: none; }
  /* width + min-height: the visual mark stays 56x20; the touch area is
     the full 44px tall slot (fixed nav, so no page height impact). */
  .shell-nav-brand { width: 56px; height: auto; min-height: 44px; }
  .shell-nav-brand img { height: 20px; }
  .dock-wordmark { width: 168px; }

  /* Phone-only Ch5 compaction: the full self-building scenes add real height, so
     on the smallest phones tighten the COPY column (gap, headline, body) so the
     six stacked cards stay within the page-length budget. Content is unchanged;
     this is rhythm only. Tablet (>640) keeps the roomier copy. */
  .ch-05-card > div[style*="flex:1 1 50%"] { gap: 6px !important; }
  .ch-05-card h2 { font-size: clamp(19px, 5vw, 26px) !important; line-height: 1.06 !important; }
  .ch-05-card [data-body] { font-size: 14px !important; line-height: 1.36 !important; }

  /* Ch7 / product-hero previews - PHONE crop, per-ASSET aspect (pass 8, owner:
     "the cropping is a little weird, re-evaluate"). The previous fixed
     clamp(200px,38svh,280px) box was tuned for the theta chat mock (1209x1111,
     near-square) but the same rule frames EVERY product hero: the portrait
     phone-app shots (holm / rewrite / patchpal previews, ~1206x2456) were
     cover-cropped to their top ~37% - a chop mid-UI that read as an error.
     New model: the box follows the IMAGE's own aspect (height:auto; the img
     is in-flow height:auto) capped at 52svh. Near-square assets now show 100%
     un-cropped (same on-screen scale as before, so the theta bubbles stay
     exactly as readable - the old "sub-pixel bubbles" regression cannot
     return); portrait assets get a generous top-anchored window (~69% of the
     shot at 440px width: header + hero + primary UI) via cover/top, reading
     as a deliberate "top of the app" frame. Pre-load reservation moves to
     width/height attributes on the img tags (added pass 8), so height:auto
     cannot reflow ch-07 after the lazy load. Tablet (>640) keeps its
     full-contain view. */
  .ch-07-card-image {
    height: auto;
    aspect-ratio: auto;
    max-height: 52vh;
    max-height: 52svh;
  }
  .ch-07-card-image img {
    height: auto !important;
    max-height: 52vh;
    max-height: 52svh;
    object-fit: cover !important;
    object-position: top !important;
  }
  /* pass 8.2 (owner field test): PINNED-ROW crops for the three portrait
     product heroes. Two convicted defects with the 52svh cap: (a) any
     viewport-coupled height re-resolves when the frozen token re-lands at
     quiet adoption, so the preview visibly RESIZED on its own in-app
     ("patchpal preview media is resizing, all janked up"); (b) the crop row
     drifted with viewport height, so /rewrite's cut kept landing mid-text.
     A per-asset aspect-ratio on the card box pins the crop to a FIXED
     SOURCE ROW at every width and has ZERO viewport dependence - immune to
     churn, adoption, and rotation. Rows chosen from the assets themselves
     (1206px-wide sources; the ratios bake in the card's 32px padding so the
     cut stays in its gap across 375-440px widths):
       holm/patchpal 1206/1646 - holm: the owner-approved ~69% window,
         now frozen; patchpal: cuts on flat green below the scan paragraph.
       rewrite 1206/1500 - cuts in the background gap below the reflective-
         space paragraph, above the atlas button (no more sliced text).
     The pass-8.1 bottom fades are REMOVED: clean gap cuts need no fade, and
     the rewrite fade was dimming the paragraph's own last lines. */
  body[data-subpage="holm"] .ch-07-card-image,
  body[data-subpage="patchpal"] .ch-07-card-image {
    height: auto;
    max-height: none;
    aspect-ratio: 1206 / 1646;
  }
  body[data-subpage="rewrite"] .ch-07-card-image {
    height: auto;
    max-height: none;
    aspect-ratio: 1206 / 1500;
  }
  body[data-subpage="holm"] .ch-07-card-image img,
  body[data-subpage="rewrite"] .ch-07-card-image img,
  body[data-subpage="patchpal"] .ch-07-card-image img {
    height: 100% !important;
    max-height: none;
  }
}


/* ---------------------------------------------------------------------------
   Frame-resize churn pins (the height-only churn law, CSS half).
   In-app browsers that resize the WKWebView frame re-resolve raw svh
   instantly; these tail overrides re-land the remaining mobile svh bands on
   the JS-published px tokens (site.js publishViewportTokens, refreshed only
   at quiet adoption). They sit AFTER the original modern lines so the pin
   wins the cascade on capable engines; pre-svh engines never enter the
   @supports and keep the vh baselines; with JS absent the 1svh fallbacks
   equal the stock units exactly. The raw pairs above stay as the readable
   spec of each band's design height.
   ------------------------------------------------------------------------ */
@media (max-width: 1023px), (pointer: coarse) {
  @supports (height: 100svh) {
    .ch-04-half-left { height: calc(var(--svh-px, 1svh) * 50); }
    .ch-04-half-right { top: calc(var(--svh-px, 1svh) * 50); }
    /* pass 8.3: freeze the remaining live-unit geometry inside the pin. The
       unified panel fills the FROZEN viewport (its centred copy no longer
       tracks the live pin - the 62px-per-flip jump); the halves' 7vh pads
       re-land on the token (they shifted the visible copy ~9px per flip
       while the halves are on screen). */
    .ch-04-unified { height: calc(var(--svh-px, 1svh) * 100); }
    /* pass 8.5: the panel's theTurn field runs to the PIN's bottom (the
       token pin height), so the bars reach the wipe's top edge. */
    .ch-04-unified .ch-ambient-field--pin { height: calc(var(--svh-px, 1svh) * 100 + var(--m-cover-pad)); }
    .ch-04-half-left { padding-bottom: clamp(36px, calc(var(--svh-px, 1svh) * 7), 64px); }
    .ch-04-half-right { padding-top: clamp(36px, calc(var(--svh-px, 1svh) * 7), 64px); }
    .case-title-card { min-height: calc(var(--svh-px, 1svh) * 28); }
    .wipe-panel { min-height: calc(var(--svh-px, 1svh) * 26); }
    .ch-07-card-image { max-height: calc(var(--svh-px, 1svh) * 56); }

    /* RIGID-SHEET COMPLETION (pass 6). The remaining full-viewport mobile
       surfaces that were still sized in RAW svh re-resolve instantly when an
       in-app browser resizes the WKWebView frame (they REFLOW mid-gesture,
       tearing against the frozen sheet). Re-land them on the JS-published px
       tokens so the whole mobile composition holds ONE frozen geometry between
       quiet adoptions - no live viewport units survive on a mobile cover.
       (1) The .chapter-wipe base height (mobile.css / pass3.css raw 100svh):
       the four flip transitions collapse to --m-wipe-runway at #id specificity
       above, but the un-flipped base + any wipe not caught by an #id override
       must not read raw svh either. The per-#id flip runways (mobile.css above,
       at #id specificity) already win the cascade with the frozen
       --m-wipe-runway, so only the plain-class base needs re-landing here.
       NOTE: the ch-05 header top anchor (pass3.css: max(3vh, (100svh-644px)/2 -
       96px)) is ALSO raw svh, but it is deliberately LEFT as-is: it is a small
       text block, not a full-viewport cover (it cannot leak a differently-
       coloured surface at an edge), and freezing it to var(--svh-px)*100 nudged
       its settled top ~23px off the committed baseline (a max()-branch rounding
       difference between the raw svh unit and the published px token). Quiet-
       viewport pixel-identity wins over completing the sheet on a non-leaking
       element; the header re-anchoring mid-churn is imperceptible and exposes
       no surface. */
    .chapter-wipe { height: calc(var(--svh-px, 1svh) * 100); }
    #ch-03half-transition.chapter-wipe,
    #ch-04half-transition.chapter-wipe,
    #ch-06half-transition.chapter-wipe,
    #ch-07half-transition.chapter-wipe,
    #ch-08half-transition.chapter-wipe { height: var(--m-wipe-runway); }
  }
}
@media (max-width: 640px) {
  @supports (height: 100svh) {
    /* pass 8: matches the new height:auto + 52svh-cap phone model above -
       the caps re-land on the frozen px token so the preview box cannot
       reflow mid-churn when the in-app frame resizes. */
    .ch-07-card-image { max-height: calc(var(--svh-px, 1svh) * 52); }
    .ch-07-card-image img { max-height: calc(var(--svh-px, 1svh) * 52); }
  }
}

/* REMOVED (pass 7.5, bug-5 fix): the pass-6 ".churn-grow-floor" - a fixed Ink
   band at the viewport bottom, armed by html[data-surface="ink"].churn-grow to
   overdraw the Ink->Bone boundary a frozen-short cover exposed. It fired
   SPURIOUSLY: [data-surface="ink"] stays "ink" through the Ch4->Ch5 and Ch8->Ch9
   ENTRY, so the band painted over the incoming Bone chapter = the owner-reported
   "brown/ink band that sits there for some time" entering Ch5 (and Ch9). Now that
   the .ch-04-pin cover re-resolves to the live viewport (raw 100lvh, above) there
   is no short cover to overdraw, so the floor is deleted outright (CSS here + its
   site.js driver __ensureChurnFloor / __setChurnGrow / __applyChurnGrow /
   __probeSvhPx / __clearChurnGrow). See wiki constraint-mobile-scroll-stability. */

