/* ============================================================================
   FOOTPRINT — THE JOURNEY
   ----------------------------------------------------------------------------
   One vertical document. Each act pins the viewport and translates a
   horizontal track with transform:translate3d() — no layout properties are
   animated, so the whole thing stays on the compositor.

   Layer map:
     §1  base + shell chrome (HUD, chapter rail, skip link)
     §2  hero / THE MARK
     §3  act + track mechanics (shared by every template)
     §4  TEMPLATE: sylva       (act 01 — data, lifted from footprint-sylva)
     §5  TEMPLATE: product     (spare — kept for reuse, currently unassigned)
     §5b TEMPLATE: growth engine (act 02 — CRM & app, live Command Center)
     §5c TEMPLATE: books         (act 04 — reviews & proof, story shelf)
     §5d TEMPLATE: calcom        (spare — previously act 05, superseded by Kage)
     §5e TEMPLATE: rejouice      (act 03 — social, film-scrubbed opener)
     §5f TEMPLATE: kage          (act 05 — AI agents, live WebGL temple)
     §5g TEMPLATE: motion        (act 06 — motion & interaction, pointer-driven)
     §5h TEMPLATE: xora          (act 07 — paid ads, hydrated React snapshot)
     §5i TEMPLATE: kononenko     (act 09 — the bureau, editorial document)
    §5j TEMPLATE: sketchbook    (act 10 — field notes, interactive book)
     §5k TEMPLATE: shelf         (act 08 — the growth library, 3D bookcase)
     §6  TEMPLATE: studio      (spare — previously act 03)
     §7  outro
     §8  responsive + reduced motion

   Adding a template: copy one of §4–§6, change the `data-template` selector,
   and give it its own --t-* tokens, type scale and motion signature.
   Then run build/scope_act.py on the template's stylesheet — do NOT paste a
   template's CSS in unscoped, and do NOT skip its overflow:hidden -> clip
   sanitizer (see §3).
   ========================================================================== */

/* ============================ §1  BASE + SHELL ============================= */

/* FP-ACT01-04 (Al, 2026-09-26): "we did not properly match the font, so we
   want to make sure we change that as well."

   The Sylva act is lifted from footprint-sylva, and that template sets its
   entire page in Lexend — a variable family (100–900) self-hosted as a single
   woff2. My port had carried the journey's own stack instead, so every heading
   in act 01 was set in the wrong face at the wrong optical weight, which is
   exactly the kind of miss that reads as "close but cheap" without being
   nameable. Same file, same unicode-range, same font-display as the source.

   `font-weight: 100 900` is the whole point: Lexend is variable, so a single
   36 KB file serves the 300 label, the 400 heading and the 600 stat alike. */
@font-face {
  font-family: 'Lexend';
  /* Path is relative to THIS stylesheet, which lives in assets/ — so it is
     `sylva/…`, not `assets/sylva/…`. Writing the latter resolved to
     assets/assets/sylva/… and 404'd, which the browser reports only as
     "A network error occurred" on document.fonts.load, with the face status
     stuck on "error". The file itself served perfectly the whole time. */
  src: url('sylva/lexend-latin.woff2') format('woff2');
  font-weight: 100 900; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  --void: #040907;
  --paper: #f5f7f4;
  --accent: #19d585;
  --accent-deep: #0e9f5d;

  --f-display: 'Satoshi', Inter, system-ui, -apple-system, sans-serif;
  --f-body: 'Inter', system-ui, -apple-system, sans-serif;
  --f-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;

  --gut: clamp(24px, 5vw, 96px);
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body.fpj {
  margin: 0;
  background: var(--void);
  color: var(--paper);
  font-family: var(--f-body);
  /* clip, not hidden: overflow-x:hidden makes <body> a scroll container.
     Current Chrome still sticks through it, but it is one engine change away
     from breaking every .act__stage, and clip costs nothing. */
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img, svg { display: block; max-width: 100%; }
a { color: inherit; }
::selection { background: var(--accent); color: #04120b; }

.fpj-skip {
  position: fixed; top: 10px; left: 10px; z-index: 200;
  transform: translateY(-160%);
  background: var(--accent); color: #04120b;
  padding: 10px 16px; border-radius: 999px;
  font: 600 13px/1 var(--f-body); text-decoration: none;
}
.fpj-skip:focus { transform: none; }

.noscript {
  padding: 40px var(--gut);
  font-family: var(--f-mono); font-size: 14px;
}

/* ---- HUD ------------------------------------------------------------------ */

.hud {
  position: fixed; z-index: 70;
  left: var(--gut); bottom: 26px;
  width: min(320px, 60vw);
  pointer-events: none;
  opacity: 0; transition: opacity .5s ease;
  mix-blend-mode: difference;
}
.hud.is-on { opacity: 1; }
.hud__row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 8px;
  font-family: var(--f-mono); font-size: 11px;
  letter-spacing: .22em; text-transform: uppercase;
}
.hud__chapter { font-weight: 600; }
.hud__count { opacity: .7; font-variant-numeric: tabular-nums; }
.hud__bar {
  height: 2px; background: rgba(255, 255, 255, .22); overflow: hidden;
}
.hud__bar i {
  display: block; height: 100%; width: 0%;
  background: #fff; transform-origin: left;
  transition: width .12s linear;
}
.hud__hint {
  margin-top: 8px; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .3em; text-transform: uppercase; opacity: .55;
}

/* ---- chapter rail -------------------------------------------------------- */

.rail {
  position: fixed; z-index: 70;
  right: 20px; top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 16px;
}
.rail__dot {
  -webkit-appearance: none; appearance: none;
  width: 11px; height: 11px; padding: 0; border: 0; border-radius: 50%;
  background: rgba(255, 255, 255, .3);
  cursor: pointer; transition: background .35s, box-shadow .35s, transform .35s;
}
.rail__dot:hover { transform: scale(1.35); }
.rail__dot[aria-current='true'] {
  background: var(--c, var(--accent));
  box-shadow: 0 0 0 5px rgba(255, 255, 255, .09);
}

/* ============================ §2  HERO / THE MARK ========================== */

/* 250vh of runway for the WebGL timeline: scatter -> icon -> wordmark ->
   read hold (0.72-0.90) -> universe dispersal (0.90-1.0). The engine's phase
   map in mark.js is tuned against this height — change one, change the other. */
.mark {
  position: relative;
  /* 620vh, not 420vh. The morph timeline needs scroll distance to breathe:
     at 250vh the span was 1350px and the icon formed 338px in — one wheel
     flick — with a dead 0.72-0.90 stretch after it. 420vh gave 2880px, which
     fixed the dead band but was still read as "too fast" (Al, 2026-09-26):
     2880px is only ~3.2 viewports for the whole chaos -> icon -> word arc, and
     with mark.js damping at 9 the eased value tracked the wheel almost 1:1, so
     a single flick skipped most of it. 620vh gives 4680px (~5.2 viewports) so
     every beat in applyProgress() has real travel, and the damping was eased
     to 5 in the same change so the mark trails the scroll instead of snapping.
     Mobile and reduced-motion override this to height:auto and never scrub. */
  height: 620vh;
  background: var(--void);
}
.mark__stage {
  position: sticky; top: 0;
  height: 100vh; overflow: hidden;
}
.mark__canvas { position: absolute; inset: 0; width: 100%; height: 100%; }

/* FP-INTRO-01 — the "big bang" pre-roll film layer.
   Sits UNDER the canvas (inserted as the stage's first child) so the live
   particle engine paints on top; the crossfade only moves opacity. z-index 1
   keeps it above the stage background but below grain/vignette/chrome, so the
   film inherits the site's own grade instead of fighting it.
   Pointer-events off: the film is never interactive. */
/* FP-INTRO-01 — the "big bang" pre-roll.
   An AUTO-PLAYING full-screen overlay, not a scroll section.

   It used to be a 300vh scroll section that scrubbed the film from scroll
   position, which meant the film only moved if you scrolled and the visitor had
   to scroll the whole runway to reach the end. Al: "it's on scroll motion, and
   I need it to auto-play and end on our opening scene after the explosion."

   position:fixed is the load-bearing choice. The film's last frame is the
   engine's p=0.00 chaos field, and the hero shows that same field at scroll 0.
   Because the overlay costs NO document height, the hero is already sitting at
   the top of the flow underneath it the entire time the film plays — so when the
   overlay fades, the identical field is revealed with nothing to hide and no
   layout shift. A scroll section could not do this: finishing it would move the
   document and the reveal would land somewhere else.

   z-index 80 sits above .hud and .rail (both 70), which are fixed and would
   otherwise paint over the film.

   FAIL-CLOSED, and now also FAIL-VISIBLE. Any path that does not run a pre-roll
   — reduced motion, mobile, a mid-page reload, a 404 on film.js, a decode error
   — sets data-state="unavailable", which is display:none. That matters MORE than
   it did when this was a scroll section: a fixed overlay left in place is an
   opaque black panel over the entire page, so every no-film path must remove it
   rather than merely collapse its height. */
.bang {
  position: fixed; inset: 0; z-index: 80;
  background: var(--void);
  overflow: hidden;
  opacity: 1;
  /* visibility flips only AFTER the fade, so the reveal is a dissolve rather
     than an instant disappearance */
  transition: opacity .7s ease, visibility 0s linear .7s;
}
.bang[data-state="unavailable"] { display: none; }
.bang[data-state="done"] {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.bang__stage {
  position: absolute; inset: 0;
  overflow: hidden;
}
/* the film surface itself — opaque; the overlay's fade reveals the hero */
.bang__film {
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none; overflow: hidden;
  background: #000;   /* the engine's clear colour: no seam when it takes over */
}
.bang__chrome {
  position: absolute; z-index: 3; pointer-events: none;
  font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  color: rgba(255,255,255,.42);
}
.bang__chrome--tl { top: 26px; left: 28px; }
.bang__chrome--br { bottom: 26px; right: 28px; }
/* Skip. 28.8s is a long time to hold someone's viewport with no way out, and the
   destination is the last frame, so skipping is seamless rather than a cut.
   Kept quiet: it is an escape hatch, not a call to action. */
.bang__skip {
  position: absolute; z-index: 4;
  top: 20px; right: 24px;
  padding: 7px 14px;
  font: inherit; font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  color: rgba(255,255,255,.5);
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.16);
  border-radius: 999px;
  cursor: pointer;
  -webkit-appearance: none; appearance: none;
  transition: color .2s ease, border-color .2s ease, background .2s ease;
}
.bang__skip:hover { color: #fff; border-color: rgba(255,255,255,.45); background: rgba(255,255,255,.11); }
.bang__skip:focus-visible { outline: 2px solid rgba(255,255,255,.55); outline-offset: 2px; }
/* never show chrome on a section that has been taken out of the document */
.bang[data-state="unavailable"] .bang__chrome,
.bang[data-state="unavailable"] .bang__stage { display: none; }

/* The pre-roll owns the viewport while it plays. Without this the page scrolls
   underneath the overlay and the visitor lands somewhere arbitrary when the film
   finishes. Cleared by the player on completion, retirement, or a 90s watchdog. */
html.fpj-locked, html.fpj-locked body { overflow: hidden; }

/* The in-hero film layer is retired (kept for the retired path only). */
.film {
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none; overflow: hidden;
  background: #000;
}
.film__seg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  /* no transition: this is a scrubbed playhead, not a slideshow */
}
.film__seg.is-active { opacity: 1; }
.mark__grain {
  position: absolute; inset: 0; pointer-events: none; opacity: .5;
  background-image: radial-gradient(rgba(255,255,255,.05) 1px, transparent 1px);
  background-size: 3px 3px;
}
.mark__vignette {
  position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(120% 80% at 50% 50%, transparent 40%, rgba(0,0,0,.72) 100%);
}

.mark__chrome {
  position: absolute; z-index: 3;
  font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .3em; text-transform: uppercase;
  color: rgba(230, 255, 244, .5);
}
.mark__chrome--tl { top: 26px; left: var(--gut); }
.mark__chrome--tr { top: 26px; right: var(--gut); }
.mark__chrome--bl { bottom: 26px; left: var(--gut); }
.mark__chrome--br { bottom: 26px; right: var(--gut); }
.mark__readout {
  position: absolute; z-index: 3; top: 26px; left: 50%;
  transform: translateX(-50%);
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .28em;
  color: rgba(53, 224, 143, .75);
}

/* copy fades up as the mark resolves, and out again as the acts arrive.
   opacity clamps itself, so no JS classes are needed. */
.mark__copy {
  position: absolute; z-index: 4; left: var(--gut); right: var(--gut);
  bottom: clamp(96px, 16vh, 190px);
  max-width: 720px;
  opacity: min(calc((var(--mp, 0) - .22) * 5), calc((.96 - var(--mp, 0)) * 7));
  transform: translateY(calc((.5 - min(var(--mp, 0), .5)) * -34px));
}
.mark__eyebrow {
  margin: 0 0 14px;
  font-family: var(--f-mono); font-size: 11px; letter-spacing: .34em;
  text-transform: uppercase; color: var(--accent);
}
.mark__title {
  margin: 0;
  font-family: var(--f-display); font-weight: 900;
  font-size: clamp(38px, 6.4vw, 88px); line-height: .96; letter-spacing: -.04em;
}
.mark__sub {
  margin: 18px 0 0; max-width: 54ch;
  font-size: clamp(14px, 1.25vw, 18px); line-height: 1.6;
  color: rgba(230, 255, 244, .68);
}

.mark__cue {
  position: absolute; z-index: 4; left: 50%; bottom: 34px;
  transform: translateX(-50%);
  width: 1px; height: 46px; background: rgba(255, 255, 255, .18); overflow: hidden;
  opacity: calc(1 - var(--mp, 0) * 3);
}
.mark__cue span {
  position: absolute; left: 0; top: -46px; width: 100%; height: 46px;
  background: linear-gradient(180deg, transparent, var(--accent));
  animation: cue 1.9s ease-in-out infinite;
}
@keyframes cue { 0% { top: -46px; } 60%, 100% { top: 46px; } }

/* ============================ §3  ACT MECHANICS ============================ */

.acts { position: relative; z-index: 2; }

.act {
  position: relative;
  background: var(--t-bg, #050b09);
  color: var(--t-ink, #e6fff4);
  isolation: isolate;
}
/* the seam: a deliberate hard cut between templates */
.act__seam {
  position: absolute; top: 0; left: 0; right: 0; height: 3px; z-index: 6;
  background: linear-gradient(90deg, var(--t-accent), var(--t-accent2));
}

.act__stage {
  position: sticky; top: 0;
  height: 100vh; overflow: hidden;
}
.act__track {
  display: flex; align-items: center;
  height: 100vh;
  will-change: transform;
  transform: translate3d(0, 0, 0);
}
.panel {
  --lp: 0;
  position: relative;
  flex: 0 0 auto;
  height: 100vh;
  display: flex; align-items: center;
  padding: 0 var(--gut);
}
.panel > * { width: 100%; }

/* generic parallax: [data-parallax="0.3"] drifts against the track */
[data-parallax] {
  transform: translateX(calc(var(--lp) * var(--px, 0) * -130px));
}

/* shared card shell — every template restyles it from scratch */
.p-card, .p-open {
  opacity: .18;
  filter: blur(7px);
  transition: opacity .75s ease, filter .75s ease;
}
.panel.is-live .p-card,
.panel.is-live .p-open { opacity: 1; filter: none; }

/* wayfinding inside a pinned act */
.act__hud {
  position: absolute; z-index: 7; left: var(--gut); right: var(--gut);
  bottom: 74px; display: flex; align-items: center; gap: 16px;
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .28em;
  text-transform: uppercase; color: var(--t-dim); pointer-events: none;
}
.act__hud b { color: var(--t-accent); font-weight: 600; }
.act__hud .bar { flex: 1; height: 1px; background: var(--t-line); position: relative; }
.act__hud .bar i {
  position: absolute; inset: 0 auto 0 0; width: var(--w, 0%);
  background: var(--t-accent);
}

/* ========================= §4  TEMPLATE: SYLVA ========================= */
/* Act 01 — Data. Lifted from the Footprint Sylva template: a mossy forest
   scene behind everything, with floating "bone" cards on top. The backdrop
   is two SVGs layered into the stage (stars + moss mounds), so the world
   stays put while the cards translate horizontally past it.               */

.act[data-template='sylva'] {
  --t-bg: #383b34;
  --t-panel: #f2f3ef;
  --t-ink: #23261f;
  --t-dim: #7c8177;
  --t-line: rgba(255,255,255,.06);
  --t-card-line: rgba(35,38,31,.08);
  --t-accent: #f0e7bd;
  --t-accent2: #cfe0c0;

  /* FP-ACT01-04: Sylva is a Lexend page. The act reads its type through these
     two tokens, so pointing them at the family is the whole fix — no rule in
     §4 had to change. Weights matter as much as the face: the reference sets
     body/labels at 300 and headings at 400, where the journey's own stack
     defaulted everything to 600. Lexend is variable, so 300 and 400 are real
     optical weights rather than a synthesised lighter. */
  --t-fd: 'Lexend', var(--f-display);
  --t-fm: 'Lexend', var(--f-mono);
  --t-fw-body: 300;
  --t-fw-head: 400;
  --t-fw-stat: 600;

  /* the liquid-metal pill, in this act's unit system. --u is 1px on the
     reference's 1600x880 frame; the stage matches that frame box-for-box, so
     the button lands at the same physical size the designer drew. */
  --u: calc(100vw / 1600);
}

/* the mossy forest is painted on the sticky stage so the world does not
   scroll past while the cards translate horizontally over it */
.act[data-template='sylva'] .act__stage {
  background-color: #383b34;
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 900' preserveAspectRatio='xMidYMid slice'><g fill='%23cfe0c0' opacity='.5'><circle cx='120' cy='80' r='1.2'/><circle cx='260' cy='140' r='1'/><circle cx='380' cy='60' r='1.6'/><circle cx='540' cy='120' r='1'/><circle cx='700' cy='80' r='1.4'/><circle cx='840' cy='160' r='1'/><circle cx='1020' cy='60' r='1.6'/><circle cx='1180' cy='140' r='1.2'/><circle cx='1280' cy='80' r='1'/><circle cx='180' cy='260' r='1'/><circle cx='480' cy='220' r='1.2'/><circle cx='980' cy='280' r='1'/><circle cx='1120' cy='220' r='1.4'/><circle cx='320' cy='380' r='1'/><circle cx='1180' cy='380' r='1.2'/></g></svg>"),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 900' preserveAspectRatio='xMidYMax slice'><defs><radialGradient id='m' cx='50%25' cy='100%25' r='75%25'><stop offset='0%25' stop-color='%23cfe0c0' stop-opacity='.22'/><stop offset='60%25' stop-color='%23cfe0c0' stop-opacity='.05'/><stop offset='100%25' stop-color='%23cfe0c0' stop-opacity='0'/></radialGradient><linearGradient id='ml' x1='0' y1='0' x2='0' y2='1'><stop offset='0%25' stop-color='%233f453a'/><stop offset='100%25' stop-color='%23262320'/></linearGradient><linearGradient id='mr' x1='0' y1='0' x2='0' y2='1'><stop offset='0%25' stop-color='%23263025'/><stop offset='100%25' stop-color='%23161512'/></linearGradient></defs><ellipse cx='720' cy='940' rx='950' ry='280' fill='url(%23m)'/><path d='M -60 900 C -60 690, 80 570, 240 580 C 340 588, 410 540, 480 580 C 540 615, 560 670, 600 720 C 640 780, 680 850, 720 900 Z' fill='url(%23ml)'/><path d='M 720 900 C 760 740, 880 600, 1020 580 C 1140 562, 1240 580, 1300 640 C 1370 710, 1420 830, 1460 900 Z' fill='url(%23mr)'/><path d='M 320 900 C 380 820, 460 760, 540 800 C 620 840, 660 880, 700 900 Z' fill='%23303028' opacity='.55'/><g fill='%23eef1e7' opacity='.85'><circle cx='180' cy='620' r='2.5'/><circle cx='340' cy='660' r='3'/><circle cx='420' cy='700' r='2'/><circle cx='520' cy='760' r='2.5'/><circle cx='900' cy='620' r='3'/><circle cx='1020' cy='660' r='2'/><circle cx='1140' cy='720' r='3'/><circle cx='1240' cy='660' r='2.5'/><circle cx='1300' cy='760' r='2'/></g><g fill='%23f0e7bd' opacity='.55'><circle cx='640' cy='420' r='2'/><circle cx='420' cy='340' r='1.5'/><circle cx='900' cy='380' r='2'/><circle cx='1180' cy='300' r='1.5'/></g></svg>"),
    radial-gradient(ellipse at 50% 100%, rgba(207,224,192,.18) 0%, transparent 60%);
  background-repeat: no-repeat;
  background-size: 100% 100%, 100% 100%, 100% 100%;
  background-position: top center, bottom center, bottom center;
}

/* the giant wordmark, ghosted into the moss the way Sylva does */
.act[data-template='sylva'] .act__stage::after {
  content: 'FOOTPRINT'; position: absolute; left: 50%; top: 56%;
  transform: translate(-50%, -50%);
  font-family: var(--t-fd); font-weight: 400;
  font-size: clamp(120px, 18vw, 320px); letter-spacing: .04em;
  color: rgba(255,255,255,.045); pointer-events: none;
  white-space: nowrap;
}

.act[data-template='sylva'] .p-card {
  transform: translateY(calc(var(--lp) * 10px));
}

/* the real Sylva Three.js scene paints into <canvas id="scene"> when
   three.min.js + sylva-scene.js are loaded. It sits behind the cards. */
/* ---------------------------------------------------------------------------
   FP-ACT01-06 (Al, 2026-09-26): "the tiles themselves—one of them actually
   hides behind the moss feature. It makes it look a lot cleaner."

   That is the single most characteristic thing about the Sylva composition,
   and the port had lost it: the whole track sat at z-index 1, so every card
   painted above the moss, always. The reference instead opens the layer stack
   so the moss can get BETWEEN the two cards:

       #scene (the moss)            z 3
       .card--about                 no z-index  -> paints UNDER the moss
       .card--stove                 z 4         -> paints OVER the moss
       .knob                        z 4         -> stays in front, so the
                                                   occluded card is still
                                                   clickable

   We reproduce it here. The reason it works is subtle and worth stating: a
   positioned element with `z-index: auto` does NOT open a stacking context, so
   it is painted in the parent's own layer and can be overlapped by a sibling
   with a positive z-index. Give that card ANY z-index — even 0 — and it opens
   its own context, jumps above, and the drape disappears.

   So the rule is: the track must NOT carry a z-index, the occluded card must
   NOT carry one, and the foreground moss must be a real sibling painted after
   both. */
/* ---------------------------------------------------------------------------
   FP-ACT01-08 (Al, 2026-09-28): this is the REAL moss, and z-index 3 is the
   reference's own value.

   Al, on the FP-ACT01-07 z-order fix:
     "you put the tile on top of the dark patch but the dark patch is still
      there... it covers the beautiful mossy sylva tree branch. just to the
      right of it you can see the color difference. we need a nice beautiful
      background not just moving the tile"

   He was right, and the fix had addressed the wrong half. FP-ACT01-07 raised
   the CARD above `.sylva-fore`, which only moved the symptom: the dark patch
   stayed exactly where it was, still painted over the moss.

   The patch itself was the defect, and it should never have existed. The
   reference stylesheet has NO dark foreground overlay — the drape is the moss
   itself. Its own comments say so. On #scene, the reference reads "three.js
   canvas (z 3 — above card 1, below everything else)". On .card--about it
   reads "No z-index here on purpose. z-index:auto keeps the card out of its
   own stacking context, so the body paints under #scene (the moss drapes over
   its shoulder) while .knob, at z 4, still sits in front."

   (Quoted as prose, deliberately. Nesting the reference's comment delimiters
   inside this one terminated it early, left seven lines of English where a
   selector should be, and the parser discarded the rule below — so the canvas
   was absent from the CSSOM entirely while sitting in the file, looking fine
   to grep. This is the same failure as the missing comment opener in nama.css,
   from the opposite direction: there a delimiter was absent, here one was
   present that should not have been. Never paste a nested block comment into
   a block comment.)

   The port had the stack inverted. The real moss was parked at z-index 0,
   where it could never drape over anything, and so a fake near-opaque CSS
   ellipse (`.sylva-fore`, see below) was invented to imitate the drape. The
   imitation is what has been covering the branch art this whole time.

   This is the reference's own z 3, and it is what finally made the moss do the
   draping itself instead of a painted fake. The live order as of FP-ACT01-10:

     .sylva-canvas  3     real moss — the BACKGROUND
     .sy-float      4     near card — in front of the moss
     .sy-float--2   5     far card — in front of the near card
     .act__hud      7     chrome — above everything

   That middle column used to read `auto` for the near card, faithfully copying the
   reference, which drapes its body over .card--about. Al rejected that: "i didnt
   want to hide the tile behind". The reference is a single-viewport hero; this is
   a document, and a card carrying a title has to stay readable. The moss is now
   behind both cards rather than woven through one of them.

   The z-indexes only mean anything because the canvas shares a stacking context
   with the two cards, and it did not while it sat in the stage.
   .act__track is translated by journey.js, and a transform opens a stacking
   context regardless of z-index — so both cards were sealed inside the track's
   context (effective 0) while a canvas at 3 in the stage beat the whole track.
   The result was the moss over BOTH cards: on the live act, "Clarify First"
   was buried until only "st" of the title showed. No z-index can put an element
   "between" two others that live in a different context, so the canvas had to
   move. journey.js now appends it inside .panel, and all three resolve to
   .act__track — which is what makes 3 / 4 / 5 a real total order.

   The canvas must also be exactly viewport-sized. sylva-scene.js lays its world
   out against `hero.clientWidth` (the full-bleed stub) and calls
   `renderer.setSize(W, H, false)` — that trailing false is Three.js's
   updateStyle, so the drawing buffer is 1440x900 while the canvas BOX is
   whatever this rule says. Any mismatch squashes the moss horizontally and
   shifts it. .panel measures 1440x900 at x=0; .p-open--sylva measures neither
   1296 nor 1800 depending on how the gutter is handled, which is why it is not
   used. */
.act[data-template='sylva'] .sylva-canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block; z-index: 3;
  pointer-events: none;
}

/* the moss BODY — behind the cards. The act__stage already paints the hills
   as a background; this canvas is the animated three.js version of the same
   silhouette, and sits at the bottom of the stack. */
.act[data-template='sylva'] .act__track {
  position: relative;
  /* deliberately NO z-index — see the block comment above. A z-index here
     traps .sy-float--1 below nothing and the drape stops working. */
}

/* FP-ACT01-08 (Al, 2026-09-28): .sylva-fore is DELETED, and the rules are
   kept here only as the record of what it was and why it went.

   It was a fabricated stand-in for the moss drape — a 62%x74% ellipse filled
   with

       radial-gradient(58% 66% at 74% 88%,
         rgba(38,48,37,.96) 0%, rgba(27,34,26,.99) 58%, rgba(20,26,19,1) 100%)

   at z-index 5, masked to a soft crest. It existed because the real moss was
   parked at z-index 0 and could not drape over anything, so something had to.
   But a near-opaque dark ellipse is not moss. Al:

     "the dark patch is still there... it covers the beautiful mossy sylva tree
      branch. just to the right of it you can see the color difference"

   The colour difference he could see to the right of it was the real moss
   showing where the fake patch stopped — which is the clearest possible proof
   that the patch was covering something worth seeing.

   The reference has no such element. The drape is the moss, done properly, by
   putting the real canvas at the reference's z-index 3 (see .sylva-canvas
   above). Nothing is lost: the moss still goes over card 1 and under card 2,
   which is the depth cue FP-ACT01-06 was after — it is just now made of moss
   instead of a black smudge.

   The element is also gone from the markup in acts.js, so this is not a rule
   waiting to be resurrected by a stale stylesheet. */

/* --- opener --- */
.p-open--sylva {
  position: relative; display: block;
  width: 100%; height: 100%;
}
.sy-nav {
  position: absolute; top: 38px; left: 50%; transform: translateX(-50%);
  display: flex; gap: 4px; padding: 5px;
  background: rgba(0,0,0,.22); border: 1px solid rgba(255,255,255,.08);
  border-radius: 999px; backdrop-filter: blur(8px);
}
.sy-nav__p {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 16px; border-radius: 999px;
  font-size: 12.5px; font-weight: 500; color: rgba(255,255,255,.78);
  text-decoration: none; letter-spacing: .02em;
  transition: background .3s, color .3s;
}
.sy-nav__p::before {
  content: ''; width: 14px; height: 14px; border-radius: 4px;
  background: var(--t-accent); opacity: .85; flex: 0 0 auto;
}
.sy-nav__p:hover { color: #fff; background: rgba(255,255,255,.06); }

.sy-headline {
  position: absolute; left: var(--gut); top: 38%; max-width: 540px;
  transform: translateY(-50%);
}
.sy-h {
  display: block;
  /* FP-ACT01-04: the reference sets this headline in Lexend 400. It was hard-
     coded to 800 here, and that — not the family — is what made the port read
     as a different typeface. Lexend's 800 is a heavy, slab-ish cut that shares
     almost nothing with its 400, so simply changing the family name would have
     left the page still wrong. Variable family, real weights, no synthesis. */
  font-family: var(--t-fd); font-weight: var(--t-fw-head);
  font-size: clamp(40px, 5.6vw, 88px); line-height: .98;
  letter-spacing: -.035em; color: #fff;
  text-shadow: 0 6px 24px rgba(0,0,0,.28);
}
.sy-sub {
  display: block; margin-top: 22px; max-width: 42ch;
  font-weight: var(--t-fw-body);
  font-size: clamp(14px, 1.2vw, 17px); line-height: 1.62;
  color: rgba(255,255,255,.78);
}
.sy-cta {
  display: inline-flex; align-items: center; gap: 10px;
  margin-top: 28px;
  padding: 12px 20px 12px 14px; border-radius: 999px;
  background: rgba(0,0,0,.42); color: #fff;
  border: 1px solid rgba(255,255,255,.18);
  font-size: 13px; font-weight: 600; text-decoration: none;
  backdrop-filter: blur(8px);
  transition: background .3s, border-color .3s;
}
.sy-cta::before {
  content: ''; width: 22px; height: 22px; border-radius: 6px;
  background: var(--t-accent);
  background-image: repeating-linear-gradient(0deg, rgba(0,0,0,.18) 0 1px, transparent 1px 4px);
}
.sy-cta:hover { background: rgba(0,0,0,.6); border-color: var(--t-accent); }

.sy-play {
  position: absolute; left: 14%; bottom: 28%;
  width: 72px; height: 72px; border-radius: 50%;
  background: rgba(0,0,0,.45); border: 1px solid rgba(255,255,255,.14);
  display: grid; place-items: center; backdrop-filter: blur(6px);
}
.sy-play::before {
  content: ''; width: 0; height: 0;
  border-left: 14px solid #fff;
  border-top: 9px solid transparent; border-bottom: 9px solid transparent;
  margin-left: 4px;
}
.sy-play::after {
  content: ''; position: absolute; inset: -14px; border-radius: 50%;
  border: 1px dashed rgba(255,255,255,.18);
  animation: spin 26s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.sy-sprout {
  position: absolute; left: 22%; bottom: 24%; width: 38px; height: 38px;
  color: var(--t-accent);
}

/* --- floating bone cards -------------------------------------------------
   FP-ACT01-01 (Al, 2026-09-26): "floating tiles and pieces of information.
   They need to not only be clickable, but they need to actually hover and
   stand out, and have floating motion rather than staying stagnant."

   Three separate faults were behind "stagnant", all now fixed:
     1. `data-parallax` set `--px` on the card (journey.js buildAct) but NOTHING
        read it here. The generic rule in §3 targets `.panel`-level children;
        the cards are absolutely positioned inside `.p-open`, so the var was
        written and never consumed — measured transform was identity, matrix
        (1,0,0,1,0,0). The cards now consume it themselves.
     2. Even consuming it, scroll-parallax alone is motion you only see when
        you are already moving. Adding a continuous `float` keyframe gives the
        cards life while the reader is still.
     3. The two cards are offset by different depths (0.10 / 0.10) AND now
        different phase/duration, so they drift out of step rather than
        bobbing as a rigid pair.
   Interaction: hover lifts + scales + brightens the shadow, focus-visible
   matches for keyboard, and both cards are real targets (role=link, tabindex,
   pointer cursor) — motion-app taps them through to the panel's own CTA. */
.sy-float {
  position: absolute;
  background: var(--t-panel);
  border-radius: 18px; padding: 14px;
  box-shadow:
    0 28px 60px -28px rgba(0,0,0,.6),
    0 1px 0 rgba(255,255,255,.4) inset;
  border: 1px solid rgba(35,38,31,.06);
  cursor: pointer;
  /* FP-ACT01-10 (Al, 2026-09-28): z-index 4 — BOTH cards now sit above the
     moss. Al: "i didnt want to hide the tile behind".

     This deliberately REVERSES the invariant that FP-ACT01-06 introduced and
     that FP-ACT01-08 went to real lengths to preserve. The reference does drape
     its body over .card--about (no z-index, so #scene at 3 covers its shoulder),
     and the port faithfully reproduced that — verified by pixel, 25,791
     differing px inside the near card's box. Being faithful to the reference was
     the wrong call here: the depth cue reads as the card being buried, and the
     card carries a title and a line of copy that have to be readable. The
     reference is a single-viewport hero; the journey is a document, and legibility
     outranks a flourish nobody asked for by name.

     The moss still reads as a background, because that is what it now is — behind
     everything rather than woven through it. The branch still crosses the frame;
     it just no longer eats a card.

     4, not 3: it must be strictly greater than .sylva-canvas's 3. All three
     elements share .act__track as their nearest stacking-context ancestor (see
     the .sylva-canvas comment), so this is a plain total order — but only because
     they are in one context. Keep it that way; if the canvas is ever moved back
     out to the stage, this number means nothing again. */
  z-index: 4;
  transform:
    translate3d(calc(var(--lp, 0) * var(--px, 0) * -130px), 0, 0)
    translateY(calc(var(--fy, 0) * 1px));
  transition:
    box-shadow .5s var(--ease-spring, cubic-bezier(.175,.885,.327,1.41)),
    border-color .5s var(--ease-spring, cubic-bezier(.175,.885,.327,1.41)),
    background-color .5s ease;
  will-change: transform;
}
/* continuous drift — the "floating" Al asked for. Two different durations and
   a negative delay on the second so they are never in phase. */
.sy-float[data-float='a'] { animation: syBob 7.5s ease-in-out infinite; }
.sy-float[data-float='b'] { animation: syBob 9.2s ease-in-out infinite; animation-delay: -2.6s; }
@keyframes syBob {
  0%, 100% { transform:
      translate3d(calc(var(--lp, 0) * var(--px, 0) * -130px), 0, 0)
      translateY(calc(var(--fy, 0) * 1px)); }
  50%      { transform:
      translate3d(calc(var(--lp, 0) * var(--px, 0) * -130px), 0, 0)
      translateY(calc((var(--fy, 0) - 11) * 1px)); }
}
/* The far card sits above the near one, so the pair still reads as two depths
   rather than one flat card on another.

   FP-ACT01-10 (Al, 2026-09-28) raised this to 5. It was 4, the same value the
   base rule now carries, and two siblings at the same z-index in one stacking
   context are ordered by DOM position alone — which works, but only by accident
   of the markup. 5 states the intent outright: far card in front of near card,
   both in front of the moss at 3.

   History, kept because the numbers mean nothing without it:
     FP-ACT01-07 set 6, to clear `.sylva-fore` at 5.
     FP-ACT01-08 deleted `.sylva-fore` — a fabricated dark ellipse covering the
       real moss; Al: "it covers the beautiful mossy sylva tree branch" — and
       returned this to 4, the reference's own value for .card--stove.
     FP-ACT01-09 moved the canvas inside .panel, because the track's transform
       had sealed both cards into a stacking context the canvas sat outside, and
       the moss was painting over both of them.
     FP-ACT01-10 put the base rule on 4 as well, so the moss covers neither. */
.sy-float--2 { z-index: 5; will-change: transform; }
/* hover / focus: lift, brighten, and announce itself as a target.
   The bob is PAUSED on hover on purpose — a card that keeps drifting under
   the cursor is hard to click and reads as unsettled. Every hover property
   here is a paint property (border / background / shadow), never `transform`,
   so it never fights the syBob keyframes. Focus-visible gets the identical
   treatment so keyboard users see the same affordance. */
.sy-float:hover,
.sy-float:focus-visible {
  outline: none;
  border-color: var(--t-accent);
  background: #fff;
  box-shadow:
    0 38px 82px -30px rgba(0,0,0,.72),
    0 0 0 1px var(--t-accent),
    0 1px 0 rgba(255,255,255,.5) inset;
  animation-play-state: paused;
}
/* the image zoom + its hover target live further down, next to
   `.sy-card__img` itself — a second copy sat here and silently fought the
   real one on specificity. Removed. */
.sy-float:hover .sy-card__go,
.sy-float:focus-visible .sy-card__go { opacity: 1; transform: none; }
.sy-float:hover .sy-card__pin,
.sy-float:focus-visible .sy-card__pin {
  background: var(--t-accent); color: #23261f; border-color: transparent;
}
.sy-float--1 {
  width: 320px;
  right: clamp(80px, 18vw, 260px);
  top: 16%;
}
.sy-float--2 {
  width: 280px;
  right: clamp(60px, 10vw, 160px);
  bottom: 18%;
}
/* the image well: a real <img> now, not the empty CSS gradient it was.
   Kept as the same box (aspect + radius) so the card rhythm is unchanged. */
.sy-card__img {
  aspect-ratio: 5/3; border-radius: 12px; margin-bottom: 14px;
  overflow: hidden; background: #2a3226;
  /* the image is its own depth plane, so it takes the card's parallax at a
     REDUCED gain and travels against the card — the reference's `.portal-media`
     idea. Card 1 belongs to a shadowed layer, so it is graded down a touch. */
  transform:
    translate3d(calc(var(--lp, 0) * var(--px, 0) * -16px), 0, 0);
}
.sy-card__img img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  /* FP-ACT01-06 (Al, 2026-09-26): "the images in the floating tiles have a
     QUICK zoom". The reference sets this at 1.25s, which is a slow, cinematic
     push — at the size of these tiles it reads as lag rather than intent. This
     is the one place the port deliberately departs from the source: a 260ms
     expo-out, which lands as a crisp snap. The resting scale stays above 1 so
     there is no sub-pixel wobble on the first frame of the zoom. */
  transform: scale(1.02);
  filter: saturate(.94) contrast(1.02);
  transition:
    transform .26s cubic-bezier(.16,1,.3,1),
    filter .4s ease;
  will-change: transform;
}
.sy-float:hover .sy-card__img img,
.sy-float:focus-visible .sy-card__img img {
  transform: scale(1.14);
  filter: saturate(1.05) contrast(1.05);
}
.sy-float--2 .sy-card__img { aspect-ratio: 4/3; margin: 14px 0 0; }

/* FP-ACT01-05: the liquid-metal pill. Structurally a copy of the reference's
   .liquid-stage: a plate that carries the glass body, a canvas the shader
   paints into, and the button on top. The engine reads all three by class, so
   these three elements must be present and in this order for it to mount. */
.sy-liquid {
  position: relative;
  display: inline-block;
  height: calc(60 * var(--u, .6px)) ;
  min-height: 46px;
  isolation: isolate;
}
.sy-liquid .liquid-plate {
  position: absolute; inset: 0; border-radius: 999px;
  background:
    linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.02) 44%, rgba(255,255,255,0) 64%),
    rgba(9,11,9,.46);
  box-shadow:
    0 calc(10px) calc(22px) rgba(0,0,0,.34),
    0 calc(22px) calc(46px) rgba(0,0,0,.22),
    inset 0 1px 0 rgba(255,255,255,.13),
    inset 0 calc(-1px) calc(3px) rgba(255,255,255,.05);
  transition: background .38s cubic-bezier(.22,.61,.36,1),
              box-shadow .38s cubic-bezier(.22,.61,.36,1);
}
/* the "heated" plate — the engine toggles `.hot` on the HOST element (its
   `document.body` is a shim bound to the host, not the page body), once hover
   passes 0.5. So this selector keys off the host, which keeps the DOM body and
   the shader glow switching on the same frame instead of the bloom arriving
   while the plate is still matte. */
.sy-liquid.hot .liquid-plate {
  background:
    linear-gradient(180deg, rgba(255,255,255,.105), rgba(255,255,255,.02) 44%, rgba(255,255,255,0) 64%),
    rgba(8,10,8,.50);
  box-shadow:
    0 calc(10px) calc(22px) rgba(0,0,0,.44),
    0 calc(32px) calc(66px) rgba(0,0,0,.34),
    0 calc(66px) calc(132px) rgba(0,0,0,.20),
    inset 0 1px 0 rgba(255,255,255,.17),
    inset 0 calc(-1px) calc(3px) rgba(255,255,255,.06);
}
.sy-liquid .liquid-fx {
  position: absolute; inset: 0;
  width: 100%; height: 100%; display: block;
  border-radius: 999px;
  /* the shader draws outside the pill too (that IS the bloom), so the canvas
     has to be allowed to paint past its box without opening a scroll area */
  overflow: visible;
  pointer-events: none;
}
.sy-liquid .liquid-button {
  position: relative;
  height: 100%; width: 100%;
  border: 0; background: none; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  gap: 10px; padding: 0 26px;
  color: #fff;
  font-family: var(--t-fd);
  font-weight: 400; font-size: 16px; line-height: 1;
  letter-spacing: .01em;
  cursor: pointer; outline: none;
  -webkit-tap-highlight-color: transparent;
}
.sy-liquid .liquid-button .ico {
  display: block; flex: none; overflow: visible;
  width: 17px; height: 17px;
}
.sy-liquid .liquid-button .lbl { display: block; }
.sy-liquid .liquid-button:focus-visible {
  outline: 3px solid rgba(255,255,255,.55);
  outline-offset: 6px;
  border-radius: 999px;
}
/* if WebGL2 is missing the engine bails and marks the host; keep the CSS body
   so the control still reads as a button rather than vanishing */
[data-liquid-fallback] .liquid-fx { display: none; }

/* the arrow that appears on hover — the "this is clickable" tell */
.sy-card__go {
  position: absolute; right: 14px; bottom: 12px;
  font-size: 15px; line-height: 1; color: var(--t-ink);
  opacity: 0; transform: translateX(-6px);
  transition: opacity .35s ease, transform .45s var(--ease-spring, cubic-bezier(.175,.885,.327,1.41));
  pointer-events: none;
}
.sy-float--1 .sy-card__go { bottom: auto; top: 22px; right: 22px; }
.sy-card__label {
  font-family: var(--t-fm); font-weight: var(--t-fw-label, 300);
  font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--t-dim);
}
.sy-card__eyebrow {
  display: block; margin-bottom: 10px;
  font-family: var(--t-fm); font-weight: var(--t-fw-label, 300);
  font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--t-dim);
}
.sy-card__h {
  margin: 0; font-family: var(--t-fd); font-weight: var(--t-fw-head);
  font-size: clamp(20px, 2vw, 26px); line-height: 1.15; letter-spacing: -.02em;
  color: var(--t-ink);
}
.sy-card__pin {
  position: absolute; right: -10px; top: 36%;
  width: 38px; height: 38px; border-radius: 50%;
  background: var(--t-panel); border: 1px solid rgba(35,38,31,.08);
  display: grid; place-items: center; color: var(--t-dim);
  box-shadow: 0 8px 16px -8px rgba(0,0,0,.5);
}
.sy-card__pin::before {
  content: ''; width: 14px; height: 14px;
  background:
    radial-gradient(circle at 50% 50%, var(--t-accent) 0 4px, transparent 4px 5px, var(--t-accent) 5px 6px, transparent 6px);
}

/* --- data cards (bone-on-moss) --- */
.sy-panel {
  background: var(--t-panel); border-radius: 18px;
  padding: clamp(22px, 2.4vw, 36px);
  border: 1px solid rgba(35,38,31,.06);
  box-shadow: 0 30px 60px -32px rgba(0,0,0,.55);
  position: relative;
}
.sy-ph {
  display: flex; align-items: center; gap: 10px;
  font-size: 10px; letter-spacing: .26em; text-transform: uppercase;
  color: var(--t-dim); padding-bottom: 16px; margin-bottom: 20px;
  border-bottom: 1px solid var(--t-card-line);
}
.sy-ph i { width: 7px; height: 7px; border-radius: 50%; background: #2a8247; }

.sy-kpis {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px 26px;
}
.sy-kpi { display: grid; gap: 4px; }
.sy-kpi b {
  font-family: var(--t-fd); font-weight: 800;
  font-size: clamp(30px, 3.4vw, 50px); line-height: 1; letter-spacing: -.03em;
  color: var(--t-ink); font-variant-numeric: tabular-nums;
}
.sy-kpi span { font-size: 11px; color: var(--t-dim); }
.sy-trend { font-size: 11px; font-style: normal; }
.sy-trend.up { color: #2a8247; }
.sy-trend.down { color: #c2620e; }

.sy-spark { width: 100%; height: clamp(80px, 11vh, 130px); margin-top: 22px; }
.sy-spark .sy-line {
  fill: none; stroke: var(--t-ink); stroke-width: 2;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1400; stroke-dashoffset: 1400;
  stroke-linecap: round;
}
.panel.is-live .sy-spark .sy-line { animation: draw 1.7s cubic-bezier(.3, .8, .3, 1) forwards; }
@keyframes draw { to { stroke-dashoffset: 0; } }

.sy-pf {
  margin-top: 16px; font-size: 10px; letter-spacing: .2em;
  text-transform: uppercase; color: #9aa097;
}

.sy-bars { list-style: none; margin: 0; padding: 0; display: grid; gap: 18px; }
.sy-bars li {
  display: grid; grid-template-columns: minmax(120px, 200px) 1fr 50px;
  align-items: center; gap: 18px;
}
.sy-bars span { font-size: 12.5px; color: var(--t-dim); }
.sy-bars i {
  display: block; height: 6px; border-radius: 999px;
  background: linear-gradient(90deg, #2a3326, rgba(42,51,38,.15));
  transform: scaleX(0); transform-origin: left center;
  transition: transform 1.1s cubic-bezier(.2, .85, .25, 1);
}
.sy-bars b {
  font-family: var(--t-fd); font-weight: 700; font-size: 14.5px;
  text-align: right; color: var(--t-ink); font-variant-numeric: tabular-nums;
}
.panel.is-live .sy-bars i { transform: scaleX(var(--v)); }
.panel.is-live .sy-bars li:nth-child(1) i { transition-delay: .05s; }
.panel.is-live .sy-bars li:nth-child(2) i { transition-delay: .13s; }
.panel.is-live .sy-bars li:nth-child(3) i { transition-delay: .21s; }
.panel.is-live .sy-bars li:nth-child(4) i { transition-delay: .29s; }
.panel.is-live .sy-bars li:nth-child(5) i { transition-delay: .37s; }

.sy-grid {
  display: grid; grid-template-columns: repeat(16, minmax(0, 1fr)); gap: 5px;
}
.sy-cell {
  aspect-ratio: 1; border-radius: 2px;
  background: rgba(35,38,31,.08);
}
.sy-cell.h1 { background: rgba(35,38,31,.22); }
.sy-cell.h2 { background: rgba(35,38,31,.4); }
.sy-cell.h3 { background: #2a3326; }
.sy-cell.h4 { background: var(--t-accent); box-shadow: 0 0 10px rgba(240,231,189,.5); }
.sy-legend {
  display: flex; gap: 18px; margin-top: 18px;
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--t-dim);
}
.sy-legend span { display: flex; align-items: center; gap: 8px; }
.sy-legend i { width: 12px; height: 12px; border-radius: 1px; background: rgba(35,38,31,.08); }
.sy-legend i.h2 { background: rgba(35,38,31,.4); }
.sy-legend i.h4 { background: var(--t-accent); }

.sy-note {
  margin: 18px 0 0; font-size: 13.5px; line-height: 1.65;
  color: var(--t-dim); max-width: 52ch;
}

.sy-quote { display: grid; align-content: center; gap: 18px; }
.sy-quote-mark {
  font-family: var(--t-fd); font-weight: 800; font-size: 90px; line-height: .5;
  color: var(--t-accent);
}
.sy-quote blockquote {
  margin: 0; font-family: var(--t-fd); font-weight: 600;
  font-size: clamp(20px, 2.4vw, 32px); line-height: 1.28; letter-spacing: -.02em;
  color: var(--t-ink);
}
.sy-quote cite {
  font-size: 11px; letter-spacing: .2em; text-transform: uppercase;
  font-style: normal; color: var(--t-dim);
}
.sy-quote a {
  justify-self: start; margin-top: 10px;
  font-size: 11px; letter-spacing: .2em; text-transform: uppercase;
  text-decoration: none; color: var(--t-ink);
  border: 1px solid var(--t-card-line); border-radius: 2px;
  padding: 13px 20px;
  transition: background .3s;
}
.sy-quote a:hover { background: rgba(35,38,31,.06); }

/* ========================== §5  TEMPLATE: PRODUCT ========================== */
/* Act 02 — CRM & app. Light, rounded, product-UI: soft shadows, real-looking
   boards and a device frame. Motion: 3D tilt + slide-in cards.               */

.act[data-template='product'] {
  --t-bg: #f2f5f2;
  --t-panel: #ffffff;
  --t-soft: #f7f9f7;
  --t-line: rgba(13, 31, 20, .11);
  --t-ink: #0d1f14;
  --t-dim: #5c6b62;
  --t-accent: #3b5bff;
  --t-accent2: #0e9f5d;
  --t-fd: var(--f-display);
  --t-radius: 18px;
  --t-shadow: 0 24px 60px -28px rgba(13, 31, 20, .28);
}
.act[data-template='product'] .p-card {
  transform: perspective(1400px) rotateY(calc(var(--lp) * -7deg)) translateZ(0);
  transform-style: preserve-3d;
}

.p-open--product { max-width: 1080px; }
.q-kick {
  font-family: var(--f-body); font-weight: 600; font-size: 11px;
  letter-spacing: .3em; text-transform: uppercase; color: var(--t-accent);
  display: flex; align-items: center; gap: 12px; margin-bottom: 20px;
}
.q-kick::before {
  content: ''; width: 34px; height: 6px; border-radius: 999px; background: var(--t-accent);
}
.q-h {
  margin: 0; font-family: var(--t-fd); font-weight: 900;
  font-size: clamp(52px, 10vw, 170px); line-height: .9; letter-spacing: -.045em;
  color: var(--t-ink);
}
.q-lead {
  margin: 24px 0 0; max-width: 50ch; font-size: clamp(15px, 1.35vw, 19px);
  line-height: 1.62; color: var(--t-dim);
}
.q-list {
  list-style: none; margin: 30px 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 10px;
}
.q-list li {
  font-size: 13px; color: var(--t-dim);
  background: rgba(255, 255, 255, .9); border: 1px solid var(--t-line);
  border-radius: 999px; padding: 10px 18px;
  box-shadow: 0 2px 10px -6px rgba(13, 31, 20, .3);
}
.q-list b { color: var(--t-ink); font-weight: 600; }
.q-meta {
  margin-top: 36px; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .26em; text-transform: uppercase; color: #93a29a;
}

.q-panel {
  background: var(--t-panel); border: 1px solid var(--t-line);
  border-radius: var(--t-radius); box-shadow: var(--t-shadow);
  padding: clamp(22px, 2.4vw, 38px);
}
.q-ph {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  padding-bottom: 18px; margin-bottom: 24px; border-bottom: 1px solid var(--t-line);
}
.q-ph h3 {
  margin: 0; font-family: var(--t-fd); font-weight: 800;
  font-size: clamp(19px, 2vw, 27px); letter-spacing: -.025em; color: var(--t-ink);
}
.q-ph span { font-size: 12px; color: var(--t-dim); }

.q-board {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px;
}
.q-col {
  background: var(--t-soft); border: 1px solid var(--t-line);
  border-radius: 14px; padding: 14px;
}
.q-col h4 {
  margin: 0 0 12px; display: flex; align-items: center; justify-content: space-between;
  font-family: var(--f-body); font-size: 11px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--t-dim);
}
.q-col h4 em {
  font-style: normal; font-size: 10px; color: #fff; background: var(--t-ink);
  border-radius: 999px; padding: 3px 8px;
}
.q-task {
  background: #fff; border: 1px solid var(--t-line); border-radius: 11px;
  padding: 12px 13px; margin-bottom: 9px;
  box-shadow: 0 3px 12px -8px rgba(13, 31, 20, .35);
  opacity: 0; transform: translateY(14px) rotate(-1.2deg);
}
.q-task b {
  display: block; font-family: var(--t-fd); font-weight: 700; font-size: 13.5px;
  color: var(--t-ink); letter-spacing: -.01em;
}
.q-task span { display: block; margin-top: 4px; font-size: 11.5px; color: var(--t-dim); }
.panel.is-live .q-task {
  animation: taskIn .62s cubic-bezier(.2, .85, .3, 1) forwards;
  animation-delay: calc(var(--d, 0) * 110ms + 120ms);
}
@keyframes taskIn {
  to { opacity: 1; transform: translateY(0) rotate(0deg); }
}

.q-flow { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; }
.q-step {
  display: flex; gap: 16px; align-items: flex-start;
  opacity: 0; transform: translateX(-22px);
  padding-bottom: 14px; border-bottom: 1px dashed var(--t-line);
}
.q-step i {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: 10px;
  display: grid; place-items: center;
  background: rgba(59, 91, 255, .1); color: var(--t-accent);
  font-family: var(--f-mono); font-size: 11px; font-style: normal; font-weight: 700;
}
.q-step b {
  display: block; font-family: var(--t-fd); font-weight: 700; font-size: 15px;
  color: var(--t-ink);
}
.q-step span { font-size: 12.5px; color: var(--t-dim); }
.panel.is-live .q-step {
  animation: stepIn .6s cubic-bezier(.2, .85, .3, 1) forwards;
}
.panel.is-live .q-step:nth-child(1) { animation-delay: .08s; }
.panel.is-live .q-step:nth-child(2) { animation-delay: .18s; }
.panel.is-live .q-step:nth-child(3) { animation-delay: .28s; }
.panel.is-live .q-step:nth-child(4) { animation-delay: .38s; }
.panel.is-live .q-step:nth-child(5) { animation-delay: .48s; }
@keyframes stepIn { to { opacity: 1; transform: none; } }

.q-stat {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px;
  margin-top: 24px; padding-top: 22px; border-top: 1px solid var(--t-line);
}
.q-stat b {
  display: block; font-family: var(--t-fd); font-weight: 800;
  font-size: clamp(26px, 2.8vw, 40px); letter-spacing: -.03em; color: var(--t-accent);
  font-variant-numeric: tabular-nums;
}
.q-stat span { font-size: 12.5px; color: var(--t-dim); }

.q-panel--device { display: grid; gap: 22px; }
.q-phone {
  width: min(300px, 74%); margin: 0 auto;
  aspect-ratio: 9 / 17.6; border-radius: 34px;
  background: linear-gradient(170deg, #101a14, #060d09);
  border: 8px solid #1a251e;
  box-shadow: 0 40px 70px -34px rgba(13, 31, 20, .6), inset 0 0 0 1px rgba(255, 255, 255, .06);
  padding: 26px 16px 16px; position: relative; overflow: hidden;
  color: #eafff4;
}
.q-phone__notch {
  position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
  width: 74px; height: 5px; border-radius: 999px; background: rgba(255, 255, 255, .18);
}
.q-phone__head {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 12px 6px 14px; border-bottom: 1px solid rgba(255, 255, 255, .1);
}
.q-phone__head b { font-family: var(--t-fd); font-size: 16px; font-weight: 800; }
.q-phone__head span { font-size: 10px; color: rgba(234, 255, 244, .55); }
.q-notif {
  display: flex; gap: 11px; align-items: flex-start;
  padding: 12px 10px; border-radius: 13px;
  background: rgba(255, 255, 255, .05); margin-top: 10px;
}
.q-notif i {
  flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%; margin-top: 5px;
  background: var(--t-accent);
}
.q-notif i.amber { background: #ffb347; }
.q-notif i.green { background: #19d585; }
.q-notif b { display: block; font-size: 12.5px; font-weight: 600; }
.q-notif span { font-size: 10.5px; color: rgba(234, 255, 244, .55); }
.q-phone__bar {
  position: absolute; left: 16px; right: 16px; bottom: 14px;
  display: flex; justify-content: space-around;
}
.q-phone__bar span { width: 30px; height: 3px; border-radius: 999px; background: rgba(255, 255, 255, .22); }
.q-phone__bar span:first-child { background: var(--t-accent); }
.q-note {
  margin: 0; text-align: center; font-size: 13px; color: var(--t-dim); max-width: 44ch;
  justify-self: center;
}

.q-panel--quote { display: grid; align-content: center; gap: 16px; }
.q-quote-mark {
  font-family: var(--t-fd); font-weight: 900; font-size: 88px; line-height: .55;
  color: var(--t-accent); opacity: .28;
}
.q-panel--quote blockquote {
  margin: 0; font-family: var(--t-fd); font-weight: 600;
  font-size: clamp(20px, 2.4vw, 32px); line-height: 1.3; letter-spacing: -.02em;
  color: var(--t-ink);
}
.q-panel--quote cite {
  font-size: 12px; font-style: normal; color: var(--t-dim); letter-spacing: .04em;
}
.q-cta {
  justify-self: start; margin-top: 8px;
  background: var(--t-ink); color: #fff; text-decoration: none;
  font-weight: 600; font-size: 13px;
  padding: 14px 24px; border-radius: 999px;
  box-shadow: 0 14px 30px -14px rgba(13, 31, 20, .6);
  transition: transform .25s, background .25s;
}
.q-cta:hover { transform: translateY(-2px); background: var(--t-accent); }

/* ======================= §5b TEMPLATE: GROWTH ENGINE ======================= */
/* Act 02 — CRM & app. Lifted from the catalog's footprint-growth-engine.
   This act is not a picture of a CRM, it IS one: the Command Center switches
   channels, redraws its chart, refills its activity feed and re-projects
   pipeline from the budget slider, and the calculator actually calculates.

   The template's full stylesheet ships in assets/ge.css, re-rooted under
   .act[data-template='ge'] so none of it can leak. Everything below is only
   what the journey itself needs: HUD/seam tokens, vertical compression so a
   section authored for a scrolling page fits a 100vh panel, and the --lp
   motion signature.                                                          */

.act[data-template='ge'] {
  --t-bg: #f5f7f4;
  --t-panel: #ffffff;
  --t-ink: #0d1f14;
  --t-dim: #52645a;
  --t-line: rgba(13, 31, 20, .12);
  --t-accent: #0e9f5d;
  --t-accent2: #4f5df0;
  --t-fd: 'Sora', var(--f-display);
  --t-fm: 'Space Grotesk', var(--f-mono);
}
.act[data-template='ge'] .act__hud { color: #8a9a90; }

/* fit-to-panel — ge-app.js measures each .ge-fit and scales it down only when
   the section's natural height would overflow the panel. */
.ge-fit {
  transform-origin: 50% 50%;
  will-change: transform;
}

/* page padding was written for a vertical document; a pinned act has none */
.act[data-template='ge'] .hero { min-height: 0; padding: 0; }
.act[data-template='ge'] .cmd,
.act[data-template='ge'] .compare,
.act[data-template='ge'] .calc { padding: 0; }
.act[data-template='ge'] .stats { padding: 54px 0; }
.act[data-template='ge'] .cmd .shell { margin-top: 20px; }
.act[data-template='ge'] .cmd .body { min-height: min(560px, calc(100vh - 300px)); }
.act[data-template='ge'] .cmd .chart { min-height: min(230px, 26vh); }
.act[data-template='ge'] .cmd .feed { min-height: 116px; }
.act[data-template='ge'] .calc .card { margin-top: 26px; }
.act[data-template='ge'] .cmp-frame { margin-top: 26px; max-height: 48vh; }
.act[data-template='ge'] .sgrid { margin-top: 30px; }

/* motion signature: the section drifts against the track and settles at
   centre. Kept on the inner <section> so ge-app.js is free to own the
   transform on .ge-fit for the fit-to-panel scale. */
.act[data-template='ge'] .ge-fit > section {
  transform: translate3d(calc(var(--lp) * -30px), 0, 0);
}

/* the template credits line under the opener chips */
.ge-meta {
  margin-top: 30px; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .28em; text-transform: uppercase; color: var(--dim2);
}

/* ========================= §5c TEMPLATE: BOOKS ============================ */
/* Act 04 — Reviews & proof. Lifted from the catalog's footprint-books, whose
   native form is the argument: three client stories presented as books on a
   shelf. Click one and the next panel rewrites itself with that story.

   The template's stylesheet ships in assets/books.css, re-rooted under
   .act[data-template='books']. Everything below is only what the journey
   needs: seam tokens, the fixed->absolute corrections, panel geometry, and
   the pieces I authored rather than lifted (opener copy, story detail,
   scoreboard).

   Two template rules MUST be corrected here, not in books.css:
     .topbar and .toast are position:fixed. Inside .act__track (which is
     translated) a fixed element is positioned against the track, not the
     viewport, so the topbar would sit thousands of pixels off screen and the
     toast would follow the track sideways. The journey has its own HUD, so
     the topbar is simply dropped.                                            */

.act[data-template='books'] {
  --t-bg: #29251d;
  --t-ink: #eee2ca;
  --t-dim: #c5b79e;
  --t-line: rgba(238, 226, 194, .16);
  --t-accent: #c3a47b;
}
.act[data-template='books'] .act__hud { color: #a2957c; }

/* the template's own chrome makes no sense inside an act */
.act[data-template='books'] .topbar { display: none; }

/* fit-to-panel — books-app.js scales these down only on overflow */
.bk-fit {
  position: relative;
  width: 100%;
  transform-origin: 50% 50%;
  will-change: transform;
}

/* motion signature: drift against the track, settle at centre */
.act[data-template='books'] .bk-fit > * {
  transform: translate3d(calc(var(--lp) * -26px), 0, 0);
}

/* ---- panel 1 · opener ---------------------------------------------------- */
.act[data-template='books'] .bk-open { height: 100%; }
.act[data-template='books'] .bk-open .hero-word {
  top: 8vh;
  font-size: clamp(120px, 15vw, 232px);
  opacity: .5;
}
.bk-open__copy {
  position: absolute; z-index: 4; left: 0; bottom: 12vh;
  width: min(52%, 560px);
}
.bk-kick {
  margin: 0 0 14px; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .28em; text-transform: uppercase; color: var(--t-accent);
}
.bk-open__copy h3, .bk-detail .bk-title {
  margin: 0 0 14px; font-family: var(--serif, Georgia, serif);
  font-weight: 400; font-size: clamp(30px, 3.4vw, 52px);
  line-height: 1.08; letter-spacing: -.02em; color: #f4ecd8;
}
.bk-sub, .bk-desc, .bk-note {
  margin: 0; font-size: 15px; line-height: 1.68; color: var(--t-dim);
}
.bk-chips { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }
.bk-chip {
  padding: 8px 14px; border: 1px solid var(--t-line); border-radius: 999px;
  font-size: 12px; color: var(--t-dim);
}
.bk-chip b { color: #f4ecd8; font-weight: 500; margin-right: 6px; }
.bk-meta {
  margin-top: 26px; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .28em; text-transform: uppercase; color: rgba(197, 183, 158, .55);
}

/* ---- panel 2 · gallery --------------------------------------------------- */
/* The template scatters its books across a full viewport with percentage
   coordinates; a panel is narrower, so the coordinates are re-tuned here. */
.bk-gallery-fit { height: 100%; display: flex; flex-direction: column; justify-content: center; }
.act[data-template='books'] .bk-stage {
  position: relative;
  width: 100%;
  height: min(60vh, 570px);
}
.act[data-template='books'] .bk-stage .gallery { inset: 0; }
/* Books are positioned as a percentage of .bk-stage and are ~1.5x taller than
   they are wide, then rotated in 3D under a 1400px perspective — so their
   projected box is noticeably bigger than their layout box. --w and --y are
   tuned here until the tallest book clears the panel bottom at BOTH 900px and
   720px viewport heights. Measured, not guessed. */
.act[data-template='books'] .book-card[data-book='codex']  { --w: min(18vw, 240px); --y: 41%; }
.act[data-template='books'] .book-card[data-book='claude'] { --w: min(20vw, 268px); --y: 34%; }
.act[data-template='books'] .book-card[data-book='cursor'] { --w: min(18vw, 240px); --y: 43%; }

/* which book the detail panel is showing */
.act[data-template='books'] .book-card[data-active='true'] .front-cover {
  box-shadow: 0 26px 50px rgba(29, 21, 11, .48),
              inset -12px 0 18px rgba(0, 0, 0, .16),
              0 0 0 1px rgba(219, 195, 156, .5);
}
.act[data-template='books'] .book-card[data-active='true'] .open-badge { opacity: 1; }
.bk-cap {
  margin: 18px 0 0; text-align: center; font-family: var(--f-mono);
  font-size: 10px; letter-spacing: .24em; text-transform: uppercase;
  color: rgba(197, 183, 158, .7);
}

/* ---- panel 3 · story detail ---------------------------------------------- */
.bk-detail {
  position: relative; width: 100%; max-width: 760px;
  padding: 4px 0;
}
.bk-kicker {
  margin: 0 0 10px; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .24em; text-transform: uppercase; color: var(--t-accent);
}
.bk-steps { list-style: none; margin: 24px 0 0; padding: 0; }
.bk-steps li {
  display: flex; gap: 16px; padding: 14px 0;
  border-top: 1px solid var(--t-line);
}
.bk-n {
  flex: 0 0 26px; height: 26px; border-radius: 50%;
  border: 1px solid var(--t-line);
  font-family: var(--f-mono); font-size: 11px; line-height: 24px;
  text-align: center; color: var(--t-accent);
}
.bk-sc { display: block; }
.bk-sc strong {
  display: block; margin-bottom: 4px; font-weight: 500; font-size: 15px;
  color: #f4ecd8;
}
.bk-sc span { display: block; font-size: 14px; line-height: 1.6; color: var(--t-dim); }
.bk-block { margin-top: 22px; }
.bk-label {
  display: block; margin-bottom: 8px; font-family: var(--f-mono);
  font-size: 10px; letter-spacing: .24em; text-transform: uppercase;
  color: rgba(197, 183, 158, .7);
}
.bk-block code, .bk-review {
  display: block; font-size: 14px; line-height: 1.66; color: var(--t-ink);
}
.bk-dock {
  display: flex; align-items: center; gap: 14px; margin-top: 26px;
  padding-top: 16px; border-top: 1px solid var(--t-line);
}
.bk-stars { color: var(--t-accent); letter-spacing: .18em; font-size: 13px; }
.bk-src, .bk-year {
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .2em;
  text-transform: uppercase; color: rgba(197, 183, 158, .7);
}
.bk-year { margin-left: auto; }

/* ---- panel 4 · scoreboard ------------------------------------------------ */
.bk-score { width: 100%; max-width: 640px; }
.bk-nums {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px; margin: 22px 0 22px;
}
.bk-num {
  padding: 18px; border: 1px solid var(--t-line); border-radius: 14px;
  background: rgba(238, 226, 194, .04);
}
.bk-num b {
  display: block; font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: 42px; line-height: 1; color: #f4ecd8;
}
.bk-num span { display: block; margin-top: 8px; font-size: 13px; color: var(--t-dim); }
.bk-btn {
  margin-top: 20px; padding: 12px 22px; border-radius: 999px;
  border: 1px solid var(--t-accent); background: transparent;
  color: var(--t-accent); font: inherit; font-size: 13px; cursor: pointer;
}
.bk-btn:hover { background: rgba(195, 164, 123, .12); }
.bk-link {
  display: inline-block; margin-top: 16px; font-size: 13px;
  color: var(--t-dim); text-decoration: none; border-bottom: 1px solid var(--t-line);
}

/* toast — the template's is position:fixed, which is unusable inside the track */
.act[data-template='books'] .bk-toast {
  position: absolute; left: 50%; bottom: 0; transform: translate(-50%, 12px);
  z-index: 40; max-width: min(92%, 520px); padding: 11px 18px;
  border: 1px solid rgba(238, 226, 194, .16); border-radius: 999px;
  background: rgba(36, 32, 25, .92); color: #eadfc7;
  font-size: 12px; opacity: 0; pointer-events: none;
  transition: opacity 250ms ease, transform 350ms ease;
}
.act[data-template='books'] .bk-toast[data-show='true'] {
  opacity: 1; transform: translate(-50%, 0);
}

/* ========================= §5d TEMPLATE: CALCOM ============================ */
/* Act 05 — AI voice & chat agents. Lifted from the catalog's footprint-calcom,
   whose native form IS the argument: a booking screen. An agent that answers,
   qualifies and writes to your calendar is only real if the calendar fills
   itself — so panel 02 puts the call on the left and the calendar it wrote to
   on the right, and lets you confirm a slot.

   The template ships NO javascript at all — 6.7 KB of CSS, zero scripts — so
   unlike acts 01/02/04 there was nothing to port. cal-app.js authors the
   transcript and the availability grid. The six phase cards in panel 03 are
   the template's own markup and classes, and the six phase names are the real
   Footprint Growth System.

   One template rule MUST be corrected here, not in cal.css:
     nav is position:fixed. Inside .act__track (which is translated) a fixed
     element is positioned against the track, not the viewport, so the nav
     would sit thousands of pixels off to the left. The journey has its own
     HUD, so it is simply dropped.                                            */

.act[data-template='cal'] {
  --t-bg: #0a0a0a;
  --t-ink: #fafafa;
  --t-dim: #a1a1aa;
  --t-line: #262626;
  --t-accent: #ffffff;
  --t-accent2: #22c55e;
  --t-fd: 'Inter', var(--f-display);
}

/* the template's own chrome makes no sense inside an act */
.act[data-template='cal'] nav { display: none; }

/* fit-to-panel — cal-app.js scales these down only on overflow */
.cal-fit {
  position: relative;
  width: 100%;
  transform-origin: 50% 50%;
  will-change: transform;
}

/* motion signature: this template is flat and instant, so it barely drifts */
.act[data-template='cal'] .cal-fit > * {
  transform: translate3d(calc(var(--lp) * -18px), 0, 0);
}

/* ---- panel 1 · opener ---------------------------------------------------- */
.act[data-template='cal'] .cal-open { height: 100%; }
.cal-word {
  position: absolute; top: 9vh; left: 0; margin: 0;
  font-family: var(--t-fd); font-weight: 800;
  font-size: clamp(58px, 8.4vw, 148px); line-height: .92; letter-spacing: -.045em;
  color: #ffffff; opacity: .09; white-space: nowrap;
}
.cal-open__copy {
  position: absolute; z-index: 4; left: 0; bottom: 12vh;
  width: min(58%, 620px);
}
.cal-kick {
  margin: 0 0 14px; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .28em; text-transform: uppercase; color: var(--t-accent2);
}
.cal-open__copy h3, .cal-desk__head h4 {
  margin: 0 0 14px; font-family: var(--t-fd); font-weight: 700;
  font-size: clamp(28px, 3.2vw, 50px); line-height: 1.06; letter-spacing: -.03em;
  color: #fafafa;
}
.cal-sub {
  margin: 0; font-size: 15px; line-height: 1.68; color: var(--t-dim);
  max-width: 60ch;
}
.cal-chips { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }
.cal-chip {
  padding: 8px 14px; border: 1px solid var(--t-line); border-radius: 999px;
  font-size: 12px; color: var(--t-dim);
}
.cal-chip b { color: #fafafa; font-weight: 600; margin-right: 6px; }
.cal-meta {
  margin-top: 26px; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .28em; text-transform: uppercase; color: #52525b;
}

/* ---- panel 2 · the call and the calendar it writes to --------------------- */
.cal-desk { display: flex; flex-direction: column; gap: clamp(16px, 2.4vh, 30px); }
.cal-desk__head { max-width: 760px; }

.cal-split {
  display: grid; grid-template-columns: 1.04fr .96fr;
  gap: 18px; align-items: stretch;
  height: min(56vh, 500px); min-height: 400px;
}
.cal-call, .cal-book {
  display: flex; flex-direction: column; min-height: 0;
  border: 1px solid var(--t-line); border-radius: 16px;
  background: #111111; padding: 18px 20px;
}

.cal-call__head {
  display: flex; align-items: center; gap: 14px;
  padding-bottom: 12px; margin-bottom: 12px;
  border-bottom: 1px solid var(--t-line);
  font-family: var(--f-mono); font-size: 9.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--t-dim);
}
.cal-live { display: inline-flex; align-items: center; gap: 8px; color: #22c55e; }
.cal-live i { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; }
/* the pulse only runs while this act owns the screen (see pause/resume) */
.act[data-template='cal'][data-live='true'] .cal-live i {
  animation: cal-pulse 1.6s ease-in-out infinite;
}
@keyframes cal-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: .3; transform: scale(.68); }
}
.cal-num { color: #fafafa; letter-spacing: .12em; }
.cal-dur { margin-left: auto; color: #71717a; }

.cal-log {
  list-style: none; margin: 0; padding: 0;
  flex: 1 1 auto; min-height: 0; overflow: hidden;
  display: flex; flex-direction: column; gap: 10px;
}
.cal-line {
  display: grid; grid-template-columns: 58px minmax(0, 1fr); gap: 12px;
  opacity: 0; transform: translateY(8px);
}
.panel.is-live .cal-line { animation: cal-rise .5s cubic-bezier(.3, .8, .3, 1) forwards; }
.panel.is-live .cal-line:nth-child(1) { animation-delay: .06s; }
.panel.is-live .cal-line:nth-child(2) { animation-delay: .16s; }
.panel.is-live .cal-line:nth-child(3) { animation-delay: .26s; }
.panel.is-live .cal-line:nth-child(4) { animation-delay: .36s; }
.panel.is-live .cal-line:nth-child(5) { animation-delay: .46s; }
.panel.is-live .cal-line:nth-child(6) { animation-delay: .56s; }
.panel.is-live .cal-line:nth-child(7) { animation-delay: .66s; }
.panel.is-live .cal-line:nth-child(8) { animation-delay: .10s; }
.panel.is-live .cal-line:nth-child(9) { animation-delay: .20s; }
@keyframes cal-rise { to { opacity: 1; transform: none; } }
.cal-who {
  padding-top: 3px; font-family: var(--f-mono); font-size: 9px;
  letter-spacing: .16em; text-transform: uppercase; color: #71717a;
}
.cal-line[data-who='out'] .cal-who { color: #22c55e; }
.cal-line[data-who='sys'] .cal-who { color: #a1a1aa; }
.cal-line p { font-size: 13px; line-height: 1.5; color: #e4e4e7; }
.cal-line[data-who='in'] p { color: #fafafa; }
.cal-line[data-who='sys'] p {
  font-family: var(--f-mono); font-size: 11px; line-height: 1.5; color: #8b8b93;
}

.cal-out {
  display: grid; gap: 3px; margin-top: 12px; padding-top: 12px;
  border-top: 1px solid var(--t-line);
}
.cal-out__k {
  font-family: var(--f-mono); font-size: 9px; letter-spacing: .2em;
  text-transform: uppercase; color: #71717a;
}
.cal-out b { font-family: var(--t-fd); font-weight: 700; font-size: 16px; color: #fafafa; }
.cal-out__s { font-size: 12px; color: var(--t-dim); }
.cal-out[data-state='done'] b { color: #22c55e; }

.cal-book__head {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: 14px;
}
.cal-book__head h5 {
  font-family: var(--t-fd); font-weight: 700; font-size: 16px; color: #fafafa;
}
.cal-tz {
  font-family: var(--f-mono); font-size: 9.5px; letter-spacing: .16em;
  text-transform: uppercase; color: #71717a;
}
.cal-days { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; }
.cal-day {
  display: grid; gap: 1px; padding: 9px 4px; text-align: center;
  border: 1px solid var(--t-line); border-radius: 10px;
  background: #0a0a0a; color: var(--t-dim); font: inherit; cursor: pointer;
  transition: border-color .2s, background .2s, color .2s;
}
.cal-day b { font-size: 11px; font-weight: 600; letter-spacing: .06em; }
.cal-day span { font-size: 15px; font-weight: 700; color: #fafafa; }
.cal-day:hover { border-color: #52525b; }
.cal-day[data-sel='true'] { background: #fafafa; border-color: #fafafa; color: #0a0a0a; }
.cal-day[data-sel='true'] span { color: #0a0a0a; }

.cal-slots {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px; margin-top: 14px;
}
.cal-slot {
  display: grid; gap: 2px; padding: 10px 6px; text-align: center;
  border: 1px solid var(--t-line); border-radius: 10px;
  background: #0a0a0a; color: #fafafa; font: inherit; cursor: pointer;
  transition: border-color .2s, background .2s, color .2s;
}
.cal-slot span { font-size: 13px; font-weight: 600; }
.cal-slot i {
  font-style: normal; font-family: var(--f-mono); font-size: 8.5px;
  letter-spacing: .14em; text-transform: uppercase; color: #52525b;
}
.cal-slot:hover:not([disabled]) { border-color: #fafafa; }
.cal-slot[data-sel='true'] { background: #fafafa; border-color: #fafafa; color: #0a0a0a; }
.cal-slot[data-sel='true'] i { color: #3f3f46; }
.cal-slot[disabled] { cursor: not-allowed; opacity: .34; }
/* confirmed slot: green beats the disabled grey, and this rule must come
   after [disabled] at equal specificity for that to hold */
.cal-slot[data-booked='true'] { opacity: 1; border-color: #22c55e; }
.cal-slot[data-booked='true'] span, .cal-slot[data-booked='true'] i { color: #22c55e; }

.cal-confirm {
  margin-top: auto; padding: 12px 18px; border: 0; border-radius: 10px;
  background: #fafafa; color: #0a0a0a;
  font: inherit; font-weight: 600; font-size: 14px; cursor: pointer;
  transition: background .2s, opacity .2s;
}
.cal-confirm:hover:not([disabled]) { background: #e4e4e7; }
.cal-confirm[disabled] { opacity: .38; cursor: not-allowed; }
.cal-confirm[data-done='true'] { background: #22c55e; color: #04140a; opacity: 1; }
.cal-foot { margin-top: 10px; font-size: 11.5px; line-height: 1.5; color: #71717a; }

/* ---- panel 3 · where the agent sits (the template's own phase grid) ------- */
.cal-phases { width: 100%; }
.act[data-template='cal'] .cal-phases .section-header { margin-bottom: 34px; }
.act[data-template='cal'] .cal-phases .section-header h2 {
  font-family: var(--t-fd); font-weight: 700;
  font-size: clamp(26px, 3vw, 40px); letter-spacing: -.03em; margin-bottom: 12px;
}
.act[data-template='cal'] .cal-phases .section-header p {
  font-size: 15px; color: var(--t-dim); max-width: 640px; margin: 0 auto;
}
/* forced to six: the template's auto-fit would drop to five at some widths */
.act[data-template='cal'] .phases-grid {
  grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px;
}
.act[data-template='cal'] .phase-card {
  text-align: left; padding: 18px 16px;
  background: #0a0a0a; border: 1px solid var(--t-line);
  transition: border-color .2s, background .2s;
}
.act[data-template='cal'] .phase-card p {
  margin-top: 10px; font-size: 12.5px; line-height: 1.5; color: var(--t-dim);
}
.act[data-template='cal'] .phase-card[data-here='true'] {
  background: #fafafa; border-color: #fafafa;
}
.act[data-template='cal'] .phase-card[data-here='true'] .number,
.act[data-template='cal'] .phase-card[data-here='true'] h4 { color: #0a0a0a; }
.act[data-template='cal'] .phase-card[data-here='true'] p { color: #52525b; }
.cal-phases__note {
  margin-top: 26px; font-size: 13px; line-height: 1.6;
  color: var(--t-dim); max-width: 660px;
}

/* ---- panel 4 · scoreboard ------------------------------------------------ */
.cal-score { width: 100%; max-width: 560px; }
.cal-nums {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px; margin: 22px 0 26px;
}
.cal-num {
  padding: 18px; border: 1px solid var(--t-line);
  border-radius: 14px; background: #111111;
}
.cal-num b {
  display: block; font-family: var(--t-fd); font-weight: 700;
  font-size: 36px; line-height: 1; letter-spacing: -.03em; color: #fafafa;
}
.cal-num span { display: block; margin-top: 8px; font-size: 12.5px; color: var(--t-dim); }
.cal-quote {
  margin: 0; font-family: var(--t-fd); font-weight: 600;
  font-size: 22px; line-height: 1.35; letter-spacing: -.02em; color: #fafafa;
}
.cal-cite {
  display: block; margin-top: 10px; font-size: 13px;
  font-style: normal; color: var(--t-dim);
}
.cal-btn {
  margin-top: 22px; padding: 12px 22px; border-radius: 10px;
  border: 1px solid var(--t-line); background: transparent; color: #fafafa;
  font: inherit; font-size: 13px; cursor: pointer; transition: border-color .2s;
}
.cal-btn:hover { border-color: #fafafa; }
.cal-link {
  display: inline-block; margin-top: 14px; font-size: 12.5px;
  color: var(--t-dim); border-bottom: 1px solid var(--t-line);
}
/* the template has no toast of its own; authored as absolute, because a
   position:fixed element inside the translated track follows the track */
.act[data-template='cal'] .cal-toast {
  position: absolute; left: 50%; bottom: 0; transform: translate(-50%, 12px);
  z-index: 40; max-width: min(92%, 520px); padding: 11px 18px;
  border: 1px solid var(--t-line); border-radius: 999px;
  background: rgba(24, 24, 27, .94); color: #fafafa;
  font-size: 12px; opacity: 0; pointer-events: none;
  transition: opacity 250ms ease, transform 350ms ease;
}
.act[data-template='cal'] .cal-toast[data-show='true'] {
  opacity: 1; transform: translate(-50%, 0);
}


/* ========================= §5e TEMPLATE: REJOUICE ========================== */
/* Act 03 — Social. The catalog's video/cursor flagship: light-grey canvas,
   near-black 400-weight display type (ZTBros, self-hosted), a fullscreen video
   plate with a lagging blue cursor chip, hover-to-video work tiles, masked
   line-rise copy, and a letter-stagger wordmark closer.

   rej.css (scoped) owns the panel internals; this block owns what an act
   needs that a page does not: the act-level tokens (this is one of the only
   LIGHT acts — the seam, HUD and rail read them), the display font binding,
   a whisper of drift, and the mobile collapse.

   The cursor chip is authored position:absolute inside the opener. It must
   NOT be fixed: a fixed box inside the translated .act__track positions
   against the track (the Calcom nav trap). rej-app.js drives left/top from
   pointer coords relative to the opener rect.                                */

.act[data-template='rej'] {
  --t-bg: #dadada;
  --t-ink: #101010;
  --t-dim: rgba(10, 10, 10, .58);
  --t-line: rgba(10, 10, 10, .22);
  --t-accent: #007bff;
  --t-accent2: #101010;
  --t-fd: 'ztbro', var(--f-display);
}
/* the whole act speaks ZTBros; rej.css re-declares var(--f-mono) per label,
   so kickers and footers stay monospaced */
.act[data-template='rej'] .panel { font-family: var(--t-fd); font-weight: 400; }

/* motion signature: barely-there counter-drift; the horizontal track slide
   IS the motion in this template, so resist the urge to add more */
.act[data-template='rej'] .rej-open__title {
  transform: translate3d(calc(var(--lp) * -22px), 0, 0);
}
.act[data-template='rej'] .rej-work__h {
  transform: translate3d(calc(var(--lp) * -12px), 0, 0);
}

/* the opener and the badge plate are pure absolute-positioning stages;
   §3's `.panel > *` gives width but nothing gives height until rej.css's
   height:100% resolves — that's fine while the stage pins (panel is 100vh),
   and §8 handles the un-pinned phone case */

/* =========================== §6  TEMPLATE: STUDIO ========================== */
/* Spare — previously act 03 (social), replaced by Rejouice. Black + lime,
   brutalist: hard borders, oversized uppercase type, marquee, chunky stat
   blocks. Kept for reuse.                                                     */

.act[data-template='studio'] {
  --t-bg: #0b0b0b;
  --t-panel: #141414;
  --t-line: rgba(255, 255, 255, .16);
  --t-ink: #f3f3ef;
  --t-dim: #9a9a92;
  --t-accent: #ccf24d;
  --t-accent2: #ff5c39;
  --t-fd: var(--f-display);
}
.act[data-template='studio'] .p-card {
  transform: translateX(calc(var(--lp) * -34px)) rotate(calc(var(--lp) * -1.1deg));
}
.act[data-template='studio']::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: repeating-linear-gradient(0deg, rgba(255, 255, 255, .028) 0 1px, transparent 1px 5px);
}

.p-open--studio { max-width: 1150px; }
.s-kick {
  font-family: var(--f-body); font-weight: 800; font-size: 11px;
  letter-spacing: .34em; text-transform: uppercase; color: var(--t-accent);
  margin-bottom: 20px; display: block;
}
.s-h {
  margin: 0; font-family: var(--f-body); font-weight: 900;
  font-size: clamp(66px, 15vw, 250px); line-height: .82; letter-spacing: -.055em;
  text-transform: uppercase; color: var(--t-ink);
}
.s-lead {
  margin: 26px 0 0; max-width: 48ch;
  font-size: clamp(15px, 1.4vw, 20px); line-height: 1.58; color: var(--t-dim);
}
.s-list {
  list-style: none; margin: 30px 0 0; padding: 0; display: grid; gap: 0;
  border-top: 2px solid var(--t-line); max-width: 720px;
}
.s-list li {
  border-bottom: 2px solid var(--t-line);
  padding: 14px 0; font-size: 14.5px; color: var(--t-dim);
  display: flex; gap: 14px; align-items: baseline;
}
.s-list li::before { content: '↳'; color: var(--t-accent); font-size: 13px; }
.s-list b { color: var(--t-ink); font-weight: 700; }
.s-meta {
  margin-top: 34px; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .26em; text-transform: uppercase; color: #6c6c66;
}

.s-panel {
  background: var(--t-panel); border: 2px solid var(--t-line);
  border-radius: 2px; padding: clamp(22px, 2.6vw, 40px);
}
.s-ph {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding-bottom: 18px; margin-bottom: 26px; border-bottom: 2px solid var(--t-line);
}
.s-ph h3 {
  margin: 0; font-family: var(--f-body); font-weight: 900;
  font-size: clamp(18px, 2vw, 26px); letter-spacing: -.01em;
  text-transform: uppercase; color: var(--t-ink);
}
.s-ph span { font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--t-accent); }

.s-cal { display: grid; gap: 10px; }
.s-cal__row {
  display: grid; grid-template-columns: minmax(120px, 200px) repeat(7, minmax(0, 1fr));
  gap: 8px; align-items: center;
}
.s-cal__row b {
  font-size: 12.5px; font-weight: 600; color: var(--t-dim); letter-spacing: .01em;
}
.s-cell {
  height: 34px; border-radius: 2px; border: 1px solid var(--t-line);
  background: transparent;
}
.s-cell.on {
  background: var(--t-accent); border-color: var(--t-accent);
  box-shadow: 0 0 18px -4px rgba(204, 242, 77, .6);
  animation: pop .45s cubic-bezier(.2, .9, .3, 1) backwards;
}
.panel.is-live .s-cell.on { animation-play-state: running; }
.s-cell.on:nth-child(2) { animation-delay: .05s; }
.s-cell.on:nth-child(3) { animation-delay: .1s; }
.s-cell.on:nth-child(4) { animation-delay: .15s; }
.s-cell.on:nth-child(5) { animation-delay: .2s; }
.s-cell.on:nth-child(6) { animation-delay: .25s; }
.s-cell.on:nth-child(7) { animation-delay: .3s; }
.s-cell.on:nth-child(8) { animation-delay: .35s; }
@keyframes pop { from { transform: scale(.3); opacity: 0; } }

.s-legend {
  display: grid; grid-template-columns: minmax(120px, 200px) repeat(7, minmax(0, 1fr));
  gap: 8px; margin-top: 14px;
}
.s-legend span {
  font-family: var(--f-mono); font-size: 9.5px; letter-spacing: .16em;
  color: #6c6c66; text-align: center;
}
.s-legend span:first-child { text-align: left; }

.s-marquee {
  overflow: hidden; border-top: 2px solid var(--t-line);
  border-bottom: 2px solid var(--t-line); padding: 14px 0; margin-bottom: 26px;
}
.s-marquee__track {
  display: flex; gap: 22px; width: max-content;
  animation: slide 16s linear infinite; animation-play-state: paused;
}
.panel.is-live .s-marquee__track { animation-play-state: running; }
.s-marquee__track span {
  font-family: var(--f-body); font-weight: 900; font-size: clamp(18px, 2vw, 28px);
  letter-spacing: -.01em; text-transform: uppercase; color: var(--t-ink);
  white-space: nowrap;
}
.s-marquee__track span:nth-child(even) { color: var(--t-accent); }
@keyframes slide { to { transform: translateX(-50%); } }

.s-posts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
.s-post {
  border: 1px solid var(--t-line); border-radius: 2px; padding: 10px;
  opacity: 0; transform: translateY(22px) scale(.96);
}
.s-post__t {
  aspect-ratio: 4 / 5; border-radius: 2px; margin-bottom: 10px;
  background:
    linear-gradient(135deg, rgba(204, 242, 77, .22), transparent 55%),
    repeating-linear-gradient(45deg, rgba(255, 255, 255, .05) 0 6px, transparent 6px 12px),
    #1c1c1c;
}
.s-post:nth-child(2) .s-post__t {
  background: linear-gradient(135deg, rgba(255, 92, 57, .22), transparent 55%),
    repeating-linear-gradient(45deg, rgba(255, 255, 255, .05) 0 6px, transparent 6px 12px), #1c1c1c;
}
.s-post b {
  display: block; font-size: 12px; font-weight: 700; color: var(--t-ink);
  text-transform: uppercase; letter-spacing: .04em;
}
.s-post span { font-family: var(--f-mono); font-size: 10px; color: var(--t-dim); }
.panel.is-live .s-post {
  animation: slabIn .7s cubic-bezier(.2, .85, .3, 1) forwards;
  animation-delay: calc(var(--d, 0) * 90ms + 100ms);
}
@keyframes slabIn { to { opacity: 1; transform: none; } }

.s-stats {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px;
  margin-bottom: 28px;
}
.s-stat {
  border: 2px solid var(--t-line); border-radius: 2px; padding: 20px 18px;
}
.s-stat b {
  display: block; font-family: var(--f-body); font-weight: 900;
  font-size: clamp(30px, 3.6vw, 54px); line-height: 1; letter-spacing: -.04em;
  color: var(--t-accent); font-variant-numeric: tabular-nums;
}
.s-stat span {
  display: block; margin-top: 8px; font-size: 12px; color: var(--t-dim);
}
.s-bars { display: flex; align-items: flex-end; gap: 8px; height: clamp(90px, 13vh, 150px); }
.s-bars i {
  flex: 1; border-radius: 1px; background: var(--t-accent);
  height: 0; transition: height 1s cubic-bezier(.2, .85, .3, 1);
}
.panel.is-live .s-bars i { height: calc(var(--v) * 100%); }
.s-bars i:nth-child(1) { transition-delay: .04s; }
.s-bars i:nth-child(2) { transition-delay: .1s; }
.s-bars i:nth-child(3) { transition-delay: .16s; }
.s-bars i:nth-child(4) { transition-delay: .22s; }
.s-bars i:nth-child(5) { transition-delay: .28s; }
.s-bars i:nth-child(6) { transition-delay: .34s; }
.s-bars i:nth-child(7) { transition-delay: .4s; }
.s-bars i:nth-child(8) { transition-delay: .46s; }
.s-bars i:nth-child(4n) { background: var(--t-accent2); }
.s-note { margin: 22px 0 0; font-size: 13.5px; line-height: 1.65; color: var(--t-dim); max-width: 50ch; }

.s-panel--quote { display: grid; align-content: center; gap: 16px; }
.s-quote-mark {
  font-family: var(--f-body); font-weight: 900; font-size: 96px; line-height: .5;
  color: var(--t-accent);
}
.s-panel--quote blockquote {
  margin: 0; font-family: var(--f-body); font-weight: 900;
  font-size: clamp(22px, 2.8vw, 40px); line-height: 1.12; letter-spacing: -.035em;
  text-transform: uppercase; color: var(--t-ink);
}
.s-panel--quote cite {
  font-family: var(--f-mono); font-size: 11px; letter-spacing: .2em;
  text-transform: uppercase; font-style: normal; color: var(--t-dim);
}
.s-cta {
  justify-self: start; margin-top: 8px;
  background: var(--t-accent); color: #101010; text-decoration: none;
  font-weight: 800; font-size: 12px; letter-spacing: .12em; text-transform: uppercase;
  padding: 15px 26px; border-radius: 2px;
  transition: transform .2s, background .2s;
}
.s-cta:hover { transform: translate(-2px, -2px); background: var(--t-accent2); color: #fff; }

/* ============================== §7  OUTRO ================================= */

.outro {
  position: relative; z-index: 2;
  background: var(--void); color: var(--paper);
  padding: clamp(90px, 14vh, 180px) var(--gut) 40px;
  border-top: 3px solid var(--accent);
}
.outro__inner { max-width: 1000px; }
.outro__kick {
  margin: 0 0 18px; font-family: var(--f-mono); font-size: 11px;
  letter-spacing: .32em; text-transform: uppercase; color: var(--accent);
}
.outro__h {
  margin: 0; font-family: var(--f-display); font-weight: 900;
  font-size: clamp(34px, 5.6vw, 76px); line-height: .98; letter-spacing: -.04em;
}
.outro__p {
  margin: 26px 0 0; max-width: 62ch; font-size: clamp(14px, 1.2vw, 17px);
  line-height: 1.68; color: rgba(245, 247, 244, .66);
}
.outro__acts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px; margin: 46px 0 0;
}
.outro__act {
  border: 1px solid rgba(255, 255, 255, .12); border-radius: 4px;
  padding: 20px; text-decoration: none;
  transition: border-color .3s, background .3s, transform .3s;
}
.outro__act:hover { transform: translateY(-3px); background: rgba(255, 255, 255, .04); }
.outro__act i {
  display: block; width: 26px; height: 3px; border-radius: 999px; margin-bottom: 14px;
}
.outro__act b {
  display: block; font-family: var(--f-display); font-weight: 800; font-size: 17px;
  letter-spacing: -.02em;
}
.outro__act span {
  display: block; margin-top: 6px; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: .2em; text-transform: uppercase; color: rgba(245, 247, 244, .5);
}
.outro__cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 44px; }
.btn {
  font-weight: 600; font-size: 13.5px; text-decoration: none;
  padding: 15px 26px; border-radius: 999px; transition: transform .25s, background .25s;
}
.btn--solid { background: var(--accent); color: #04120b; }
.btn--solid:hover { transform: translateY(-2px); background: #2ee89a; }
.btn--ghost { border: 1px solid rgba(255, 255, 255, .22); color: var(--paper); }
.btn--ghost:hover { background: rgba(255, 255, 255, .07); }
.outro__foot {
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  margin-top: 70px; padding-top: 22px; border-top: 1px solid rgba(255, 255, 255, .1);
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .22em;
  text-transform: uppercase; color: rgba(245, 247, 244, .38);
}

/* ====================== §8  RESPONSIVE + REDUCED MOTION ==================== */

/* Touch and narrow screens: no pinning. Each act becomes a native
   scroll-snap carousel, which is what a thumb expects anyway. */
@media (max-width: 900px) {
  .act { height: auto !important; }
  /* Sylva's panels are wider than a phone even inside the scroll-snap track,
     which lets the whole document drag sideways. Clip it — the stage is
     static here, so there is no sticky to endanger. */
  .act { overflow-x: clip; }
  .act__stage { position: static; height: auto; overflow: visible; }
  .act__track {
    display: flex; height: auto;
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    transform: none !important;
    /* FP-MOBILE-01: the track is a flex ROW of panels, and flex items default
       to align-items:stretch. Combined with the panels' own heights this left
       a tall void above the content and pushed everything to the bottom of the
       act. start + explicit top padding puts each panel's content where the
       reader's eye already is: the top of the swipe. */
    align-items: flex-start;
    padding: 48px var(--gut) 40px;
    gap: 16px;
    scrollbar-width: none;
  }
  .act__track::-webkit-scrollbar { display: none; }
  .panel {
    width: 88vw !important; height: auto; min-height: 0;
    padding: 0; scroll-snap-align: center;
  }
  .p-card, .p-open { transform: none !important; }
  [data-parallax] { transform: none !important; }

  /* ---- FP-MOBILE-01: the percentage-height collapse ----------------------
     THE root cause of the phone layout, and it is one bug, not ten.

     Every act's interior wrapper is `height: 100%` because on desktop its
     .panel is `height: 100vh`. The rule above sets .panel to `height: auto` so
     the snap carousel can size to content. A percentage height against an
     `auto` parent resolves to `auto`, and for a wrapper whose children are
     themselves absolutely positioned that is ZERO. Measured on a 390x844
     viewport (work/diag-collapsed.mjs), three panels came back literally 0px
     tall — sylva panel 0, shelf panel 0, kono panel 6 — so the reader swipes
     through an act and part of it simply is not there.

     The fix is to stop asking these wrappers to resolve a percentage at all
     and give each a real height on the phone. `min-height` in vh is used
     rather than a fixed px so a short panel is still tall enough to read and a
     long one is not clipped. Do NOT be tempted to fix this by restoring
     `height: 100vh` on .panel — that reintroduces the void, because the panel
     would then be a full viewport tall with a fraction of it filled.

     Each entry is the act's own interior selector, verified against the live
     DOM. Anything NOT listed here already measures correctly on a phone. */
  .act[data-template='sylva'] .p-open--sylva { height: auto; min-height: 64vh; }
  .act[data-template='books'] .bk-open    { height: auto; min-height: 58vh; }
  .act[data-template='kage']  .kage-sect  { height: auto; min-height: 62vh; }
  .act[data-template='kage']  .kage-sect.hero { min-height: 74vh; }

  /* ---- FP-MOBILE-02: text that runs off the right edge -------------------
     diag-mobile.mjs measured act 04's `.hero-word` ("Stories") overflowing its
     box by 73px on a 390px viewport: a display word sized in vw with a
     negative letter-spacing and no wrap. The vw sizing is right for a hero,
     wrong for a phone. Clamp it to the viewport and let long words break. */
  .act[data-template='books'] .hero-word {
    font-size: clamp(38px, 15vw, 96px) !important;
    overflow-wrap: break-word;
    max-width: 100%;
  }
  /* The same shape appears in the other acts' display words. One rule, scoped
     to the class the templates actually use, rather than a guess per act. */
  .act [class*='-word'] { max-width: 100%; overflow-wrap: break-word; }

  /* ---- FP-MOBILE-03: a visible way through the act ----------------------
     .act__hud is display:none on a phone, which removed the only progress cue
     the reader had. The snap track scrolls silently otherwise — you cannot
     tell there are five panels until you have swiped through four. A slim
     counter above the fixed .hud restores that without reintroducing the
     desktop chrome. */
  .act__track { position: relative; }
  .act[data-template] .act__hud { display: none; }

  .act__hud { display: none; }
  .rail { display: none; }
  .hud { left: 16px; right: 16px; width: auto; bottom: 14px; }
  /* the WebGL engine holds a pinned wordmark on narrow screens (journey
     reports no scroll progress here), so the hero is one static viewport;
     copy and chrome stay absolute over the particles as before. */
  .mark { height: auto; }
  .mark__stage { position: static; height: 78vh; overflow: hidden; }
  /* mobile never scrubs, so --mp sits at 0 and the opacity formula would hide
     the copy: with a pinned wordmark the headline should be up from frame one */
  .mark__copy { opacity: 1; transform: none; }
  .mark__cue { display: none; }
  .t-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
  .q-board { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .s-posts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .s-cal__row, .s-legend { grid-template-columns: 92px repeat(7, minmax(0, 1fr)); gap: 5px; }
  .s-cell { height: 26px; }
  .sy-grid { grid-template-columns: repeat(12, minmax(0, 1fr)); }

  /* Act 05 (calcom): the opener is absolutely-positioned copy inside a
     height:100% wrapper. With .panel at height:auto that wrapper collapses to
     zero, so the opener goes back into normal flow on a phone. */
  .act[data-template='cal'] .cal-open { height: auto; }
  .cal-word { position: relative; top: auto; margin-bottom: 20px; font-size: clamp(40px, 13vw, 96px); }
  .cal-open__copy { position: relative; bottom: auto; width: 100%; }
  .cal-split { grid-template-columns: minmax(0, 1fr); height: auto; }
  .cal-call, .cal-book { padding: 16px; }
  .act[data-template='cal'] .phases-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cal-nums { grid-template-columns: minmax(0, 1fr); }
  .act[data-template='cal'] .cal-fit > * { transform: none !important; }

  /* Act 03 (rejouice): every panel interior is height:100% of a 100vh panel.
     On the phone the panel is height:auto, so each interior has to take a
     real height of its own or it collapses to nothing. */
  .act[data-template='rej'] .rej-open { height: 68vh; min-height: 420px; }
  .act[data-template='rej'] .rej-state,
  .act[data-template='rej'] .rej-band,
  .act[data-template='rej'] .rej-foot-sec { height: auto; gap: 4.5vh; padding-top: 26px; padding-bottom: 26px; }
  .act[data-template='rej'] .rej-work { height: auto; gap: 3vh; padding-top: 26px; padding-bottom: 26px; }
  .act[data-template='rej'] .rej-elems { flex-direction: column; align-items: stretch; margin-top: 2vh; }
  .act[data-template='rej'] .rej-box { width: 100%; height: 56vw; }
  .act[data-template='rej'] .rej-plate { height: 58vh; min-height: 360px; }
  .act[data-template='rej'] .rej-cursor { display: none; }
  .act[data-template='rej'] .rej-contact { grid-template-columns: minmax(0, 1fr); gap: 26px; }
  .act[data-template='rej'] .rej-word span { font-size: clamp(38px, 11vw, 64px); }
  .act[data-template='rej'] .rej-state .rej-elem h1 { font-size: clamp(17px, 4.6vw, 22px); }
}

@media (prefers-reduced-motion: reduce) {
  .act { height: auto !important; }
  .act__stage { position: static; height: auto; overflow: visible; }
  .act__track {
    display: flex; flex-direction: column; height: auto;
    transform: none !important; overflow: visible; padding: 40px 0;
  }
  .panel { width: 100% !important; height: auto; }
  .p-card, .p-open { opacity: 1 !important; filter: none !important; transform: none !important; }
  [data-parallax] { transform: none !important; }
  .mark { height: auto; }
  .mark__stage { position: static; height: 76vh; }
  .mark__copy { position: static; padding: 40px var(--gut); opacity: 1; transform: none; }
  .mark__cue { display: none; }
  *, *::before, *::after { animation: none !important; transition: none !important; }
  .q-task, .q-step, .s-post, .s-cell.on { opacity: 1 !important; transform: none !important; }
  .t-bars i { transform: scaleX(var(--v)) !important; }
  .s-bars i { height: calc(var(--v) * 100%) !important; }
  .t-spark .t-line { stroke-dashoffset: 0 !important; }
  /* Act 05: the transcript starts at opacity 0 and is revealed by an
     animation, which this block has just switched off. */
  .cal-line { opacity: 1 !important; transform: none !important; }
  .act[data-template='cal'] .cal-open { height: auto; }
  .cal-word { position: relative; top: auto; margin-bottom: 20px; }
  .cal-open__copy { position: relative; bottom: auto; width: 100%; }
  .cal-split { height: auto; }
  .act[data-template='cal'] .cal-fit > * { transform: none !important; }
}

/* ========================== §5f TEMPLATE: KAGE =============================
   Act 05 — AI voice & chat agents. The catalog's WebGL flagship: a fully
   procedural Kyoto temple at night, walked by a six-waypoint camera rig that
   the act's scroll drives. assets/kage.css carries the whole ported
   stylesheet (re-rooted under this act); everything here is only what the
   journey itself has to arrange.

   The scene stack, bottom to top, all inside the pinned stage:
     0   .kage-gl      the three.js canvas (absolute, injected by kage-app.js)
     3   .fg           the cut-out near plane          -> .kage-sky (52)
     10  .act__track   the reading, travelling sideways
     52  .kage-sky     whichever chapter owns the near plane
     55  .kage-vig     vignette
     60  .kage-grain   film grain
     80  .kage-cursor  the hover ring (desktop, fine pointer only)

   Every one of those is absolute rather than fixed. In the template they were
   fixed to the viewport; here `fixed` would resolve against .act__track, which
   is translated thousands of pixels sideways as the act plays — the canvas
   would leave the frame and the grain would ride along with the reading. The
   stage is sticky at 100vh, so absolute-to-stage is the same box on screen
   with none of the trap.                                                      */

.act[data-template='kage'] {
  --t-bg: #05070a;
  --t-ink: #dfe7e0;
  --t-dim: rgba(223,231,224,.62);
  --t-line: rgba(223,231,224,.13);
  --t-accent: #e0231c;
  --t-accent2: #c9a24a;
  --t-fd: 'Onest', var(--f-display);
}
/* the scene is the darkest thing on the page — the HUD has to stay legible
   over a black hall without being washed out over a lit one */
.act[data-template='kage'] .act__hud { color: rgba(223,231,224,.56); }
.act[data-template='kage'] .act__seam {
  background: linear-gradient(90deg, #e0231c, #c9a24a 46%, rgba(223,231,224,.10));
}

/* the reading has to sit above the canvas but below the near plane, and the
   track is a flex row — give it a stacking context of its own so a card can
   never climb over the vignette */
.act[data-template='kage'] .act__stage { background: #05070a; }
.act[data-template='kage'] .act__track { position: relative; z-index: 10; }

/* One chapter per panel, and each panel is a full-height column. The template
   lays its sections out as a tall document; here each one is the whole frame,
   so the section padding comes down and the copy centres vertically. */
.act[data-template='kage'] .panel { align-items: stretch; }
.act[data-template='kage'] .kage-sect {
  position: relative;
  display: flex; flex-direction: column; justify-content: center;
  width: 100%; height: 100%;
  padding: 0 var(--gut);
  overflow: visible;
}
/* the hero and the closing frame are their own compositions, not centred copy */
.act[data-template='kage'] .kage-sect.hero,
.act[data-template='kage'] .kage-sect.fin { padding: 0 var(--gut); }
.act[data-template='kage'] .kage-sect.sec { justify-content: center; }
/* The footer is a chapter like any other inside an act: bottom-anchoring it
   was carried over from the template, where it sits at the end of a vertical
   document, but in a 100vh panel that just parks the columns under a tall
   empty band. Centre it with the rest. */
.act[data-template='kage'] .kage-sect.foot { justify-content: center; }

/* The template's own scroll-reveal is replaced by act progress (see
   checkReveals in kage-app.js), so [data-rv] starts hidden either way — this
   just guarantees the journey's generic panel fade does not fight it. */
.act[data-template='kage'] .panel > * { width: 100%; }

/* the ghost wordmark the template shows when WebGL is unavailable */
.act[data-template='kage'] .word-fb { display: none; }

/* ---------- mobile -------------------------------------------------------
   Under 900px the journey un-pins the act: panels become a scroll-snapped
   row and nothing scrubs. The scene is not built there (kage-app.js falls
   back to the template's CSS-only night sky), so every layer it injects is
   hidden and the sections simply stack as readable cards. */
@media (max-width: 900px) {
  .act[data-template='kage'] .kage-gl,
  .act[data-template='kage'] .kage-vig,
  .act[data-template='kage'] .kage-grain,
  .act[data-template='kage'] .kage-sky,
  .act[data-template='kage'] .kage-cursor { display: none; }
  .act[data-template='kage'] .kage-sect { height: auto; padding: 26px var(--gut); }
  .act[data-template='kage'] .kage-sect.foot { justify-content: flex-start; }
  .act[data-template='kage'] .gate-grid,
  .act[data-template='kage'] .cur-head { grid-template-columns: 1fr; gap: 18px; }
  .act[data-template='kage'] .cards { grid-template-columns: 1fr; gap: 14px; }
  .act[data-template='kage'] .card:nth-child(2),
  .act[data-template='kage'] .card:nth-child(3) { transform: none; }
  .act[data-template='kage'] .gate-stats { flex-wrap: wrap; gap: 16px 30px; }
  .act[data-template='kage'] .foot-grid { grid-template-columns: 1fr; }
  .act[data-template='kage'] .peek,
  .act[data-template='kage'] .hero-side { display: none; }
  .act[data-template='kage'] .fg { display: none; }
}

/* ========================= §5g TEMPLATE: NAMA ==============================
   Act 06 — Motion & interaction. FP-ACT06-01 (Al, 2026-09-26): act 06 is now
   the Studio Namma design — giant masked display type over a portfolio grid in
   which EVERY slot is a looping film. It replaced the old pointer-motion port.

   The act's stylesheet lives in assets/nama.css, loaded on approach by ACT_APPS
   exactly like every other act (journey.js mountAct -> loadActCss). Nothing in
   this file targets data-template='nama': the per-act sheet is inserted BEFORE
   journey.css precisely so that any journey-level override can win, and this
   act needs no override. See nama.css for the ported rules and the two traps
   (fixed/scroll dependencies, and section padding) documented at its head. */

/* ========================= §5h TEMPLATE: XORA =============================
   Act 07 — Paid ads. Ported from the catalog template `footprint-xora`.

   Xora is the one template in the catalog that is a CLIENT-RENDERED React app:
   a 498-byte shell (`<div id="root"></div>`) around a 232 KB bundle. There is
   no static markup to port, so this act's panels are a HYDRATED DOM SNAPSHOT
   (see acts.js), and the two interactions React used to own — the FAQ
   accordion and the Monthly/Annual toggle — are re-implemented in xora-app.js.

   assets/xora.css carries the whole ported stylesheet, re-rooted under this
   act. Everything below is only what the JOURNEY has to re-arrange, and there
   are three traps:

   1. `position: fixed`. The template's nav is fixed to the viewport. Inside an
      act the track is translated thousands of pixels sideways, so `fixed`
      resolves against the transformed track and the bar sails off with the
      reading. The header is dropped from the snapshot entirely — the journey
      owns one chrome (the "Calcom nav trap", bug 6 in the port skill).

   2. Section padding. Xora lays these out as a tall vertical document — the
      opener alone carries 15rem above and 10rem below. Each panel here is a
      single 100vh frame, so the vertical rhythm comes down and the frame
      centres. `.xo-frame` is the one box per panel that FPXora.fit() measures
      and scales if anything still outgrows the frame.

   3. Two top-level children. The close panel is a <section> plus a <footer>.
      `.panel` is a flex container, so two children would lay out SIDE BY SIDE;
      build/xora_panels.py wraps every panel in a single `.xo-frame` to stop
      that, and the wrapper is also what fit() measures.

   The template's own class vocabulary (container / pricing-* / faq-* /
   testimonials_* / h-num / g1-g7 …) is preserved verbatim in the act markup,
   so its scoped rules apply unchanged.

   4. Type. Xora declares `font-family: Poppins` and (for the display number)
      `font-family: Inter`, and the template fetched both from Google Fonts with
      an @import — which scope_act.py drops, because a scoped @import would be
      hoisted to the top of the bundle and leak into every other act. Without a
      face behind them both families resolved to the browser default, so every
      heading in the act was set in the wrong typeface. The faces below are
      self-hosted (build/xora_fonts.py); Inter ships as one variable face per
      subset, hence the 400 700 range. */

/* ---- self-hosted type for act 07 · no third-party font host ---- */
@font-face{font-family:'Inter';font-style:normal;font-weight:400 700;font-display:swap;src:url(fonts/Inter-400-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Inter';font-style:normal;font-weight:400 700;font-display:swap;src:url(fonts/Inter-400-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Poppins';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/Poppins-400-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Poppins';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/Poppins-400-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Poppins';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/Poppins-500-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Poppins';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/Poppins-500-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Poppins';font-style:normal;font-weight:600;font-display:swap;src:url(fonts/Poppins-600-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Poppins';font-style:normal;font-weight:600;font-display:swap;src:url(fonts/Poppins-600-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Poppins';font-style:normal;font-weight:700;font-display:swap;src:url(fonts/Poppins-700-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Poppins';font-style:normal;font-weight:700;font-display:swap;src:url(fonts/Poppins-700-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Poppins';font-style:normal;font-weight:900;font-display:swap;src:url(fonts/Poppins-900-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Poppins';font-style:normal;font-weight:900;font-display:swap;src:url(fonts/Poppins-900-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}

.act[data-template='xora'] {
  --t-bg: #080d27;
  --t-ink: #c4cbf5;
  --t-dim: rgba(196,203,245,.70);
  --t-line: rgba(196,203,245,.14);
  --t-accent: #2ef2ff;
  --t-accent2: #c8ea80;
}

.act[data-template='xora'] .act__stage { background: #080d27; }
/* the track is a flex row that gets translated; give it its own stacking
   context so a panel's backdrop can never climb over the neighbouring frame */
.act[data-template='xora'] .act__track { position: relative; z-index: 1; }

/* Xora brings its own inset — `.container` is max-width:1252px with a 4rem
   gutter — so the journey's own panel gutter would double it. `overflow` is
   clipped per panel because the template's backdrops are wider than a frame
   (a 1230px hero, a 960px pricing outline) and would otherwise bleed across
   the panels beside them on the shared track. */
.act[data-template='xora'] .panel {
  padding: 0;
  overflow: hidden;
}

/* one frame per panel: natural height, centred by the panel, scaled by fit().
   The bottom margin is the journey's own chrome band — .act__hud sits 74px up
   and the fixed .hud / .hud__hint stack below it — so the flex centring centres
   the frame in the space ABOVE the chrome instead of under it. FPXora.fit()
   uses the same 100px as its available height. */
.act[data-template='xora'] .panel > .xo-frame {
  width: 100%;
  margin-bottom: 100px;
  transform-origin: center center;
}

/* Vertical rhythm. Xora's generous block padding assumes a scrolling page;
   inside a pinned frame it only pushes content out of view. Only the vertical
   axis is touched — the horizontal inset stays .container's job. Note the
   testimonial cards' own pt-11 / pb-14 are NOT in this list: they are the card
   spacing and the `after:` divider, not document rhythm. */
.act[data-template='xora'] .panel .pt-60,
.act[data-template='xora'] .panel .pt-52,
.act[data-template='xora'] .panel .pt-36,
.act[data-template='xora'] .panel .pt-28,
.act[data-template='xora'] .panel .pt-24,
.act[data-template='xora'] .panel .py-28,
.act[data-template='xora'] .panel .py-24,
.act[data-template='xora'] .panel .py-16,
.act[data-template='xora'] .panel .pb-40,
.act[data-template='xora'] .panel .pb-36,
.act[data-template='xora'] .panel .pb-32,
.act[data-template='xora'] .panel .pb-24 {
  padding-top: 0;
  padding-bottom: 0;
}

/* ---------- FAQ accordion -------------------------------------------------
   React owned this: the body is `.react-slidedown.closed` (display:none) and
   React measured the child to animate the height. The template ships
   `transition-property: none`, so the transition is re-enabled here and
   xora-app.js drives the height. `overflow: hidden` is what makes a mid-flight
   height actually clip the answer instead of spilling it over the glow. */
.act[data-template='xora'] .react-slidedown {
  overflow: hidden;
  transition: height .5s ease-in-out;
}
.act[data-template='xora'] .xo-faq__body {
  padding: 0 1.75rem .875rem;      /* matches the template's px-7 py-3.5 */
  color: var(--t-dim);
}
/* the open item's own glow reads as a state, so let it stay lit while open —
   `.opacity-100` alone is beaten by the base `opacity-0` on hover-out */
.act[data-template='xora'] .panel [data-open='true'] .g5 { opacity: 1; }
.act[data-template='xora'] .panel [data-open='true'] .faq-icon { border-color: #4a5f8f; }

/* ---------- pricing toggle ------------------------------------------------
   The bundle marks the selected tier with `text-p4`. In the compiled
   stylesheet `.pricing-head_btn` is declared AFTER `.text-p4` at equal
   specificity, so the selected colour never applies — the toggle's knob still
   slides, but both labels stay dim. The journey's sheet loads after the act's,
   so this restores the state the bundle intends without touching its markup. */
.act[data-template='xora'] .pricing-head_btn.text-p4 { color: rgb(234 237 255); }

/* the three plan cards are 33.33% of a 1252px container and the first/last
   carry a -top-11 floating icon; give the row room so nothing is clipped */
.act[data-template='xora'] .panel .scroll-hide { padding-top: 3rem; }

/* Ten questions in two columns is the tallest frame in this act, and the one
   frame whose height changes as you use it. The template's 4rem between items
   assumed a scrolling page; at 2rem the FAQ fits a 900px frame outright and
   fit() stays a safety net rather than the mechanism. `mb-16` appears in this
   act ONLY on the ten FAQ items, so this cannot leak to anything else. */
.act[data-template='xora'] .panel .mb-16 { margin-bottom: 2rem; }

/* ---------- frames get a little air ---------------------------------------
   The template's own section padding used to provide this inset; stripping it
   so the frames could fit a pinned panel left the content flush against the
   frame edges, which reads as clipped even when nothing is actually cut.
   fit() measures the frame WITH this padding, so it costs a few percent of
   scale and buys the inset. */
.act[data-template='xora'] .panel > .xo-frame {
  padding-top: 1.75rem;
  padding-bottom: 1.75rem;
}

/* ---------- the close panel's product mock-up -----------------------------
   A 955px screenshot is the tallest single thing in the act — 785px on its own
   inside a frame that has 800. Pinning its width scales the image down with the
   box instead of cropping it, which is the difference between this frame
   fitting at ~1.0 and fitting at ~0.73. */
.act[data-template='xora'] .panel .download_preview-before {
  width: 560px;
  max-width: 100%;
}

/* ---------- the testimonial cards ----------------------------------------
   Six quotes in two columns of three. At the template's 44px above / 56px
   below, each card is ~368px and the column ~1104px — the tallest frame after
   the close panel. Scoped to the testimonial container so the same utilities
   used elsewhere in the act (pb-14 and mb-8 both appear in the channels panel)
   are untouched. */
.act[data-template='xora'] .testimonials_inner-before .pt-11 { padding-top: 1.25rem; }
.act[data-template='xora'] .testimonials_inner-before .pb-14 { padding-bottom: 1.5rem; }
.act[data-template='xora'] .testimonials_inner-before .mb-8 { margin-bottom: 1rem; }
/* -my-12 (-48px) exists to bleed the first card's top padding and the last
   card's bottom padding to the document edges. Against a pinned frame it just
   pulls the column out of the frame's top, so it comes back to -24px — still
   a bleed, no longer an overflow. */
.act[data-template='xora'] .panel .-my-12 { margin-top: -1.5rem; margin-bottom: -1.5rem; }

/* ---------- the three plan cards -----------------------------------------
   p-7 / pb-9 / space-y-4 appear ONLY inside the pricing panel, so these cannot
   leak; together they take the card row from 768px to ~700px. */
.act[data-template='xora'] .panel .p-7 { padding: 1.25rem; }
.act[data-template='xora'] .panel .pb-9 { padding-bottom: 1.5rem; }
.act[data-template='xora'] .panel .space-y-4 > :not([hidden]) ~ :not([hidden]) {
  margin-top: .625rem;
}
/* 56px under the pricing headline and under the opener's standfirst. Only the
   opener and the pricing panel use mb-14, and the opener is nowhere near its
   frame, so this is free height for the one frame that needs it most. */
.act[data-template='xora'] .panel .mb-14 { margin-bottom: 2rem; }

/* ---------- phone: the journey un-pins the act at 900px (see §8), so the
   frame is auto-height and there is nothing to fit — the scaling transform
   must go, and the vertical rhythm comes back so the cards breathe. --------- */
@media (max-width: 900px) {
  .act[data-template='xora'] .panel > .xo-frame { transform: none !important; }
  .act[data-template='xora'] .panel { overflow: visible; }
  .act[data-template='xora'] .panel .pt-60,
  .act[data-template='xora'] .panel .pt-52,
  .act[data-template='xora'] .panel .pt-36,
  .act[data-template='xora'] .panel .pt-28,
  .act[data-template='xora'] .panel .pt-24,
  .act[data-template='xora'] .panel .py-28,
  .act[data-template='xora'] .panel .py-24,
  .act[data-template='xora'] .panel .py-16,
  .act[data-template='xora'] .panel .pb-40,
  .act[data-template='xora'] .panel .pb-36,
  .act[data-template='xora'] .panel .pb-32,
  .act[data-template='xora'] .panel .pb-24 {
    padding-top: 1.5rem;
    padding-bottom: 1.5rem;
  }
  /* the two half-width testimonial columns stack */
  .act[data-template='xora'] .testimonials_inner-before,
  .act[data-template='xora'] .testimonials_inner-after { display: block; }
  .act[data-template='xora'] .testimonials_group-after,
  .act[data-template='xora'] .testimonials_inner-after > .flex-50 { flex: 1 1 auto; }
  /* the wide backdrops are decoration tuned for a 1252px column */
  .act[data-template='xora'] .pricing-bg,
  .act[data-template='xora'] .hero-img_res { display: none; }
}

/* ---------- reduced motion: the accordion still opens, it just does not
   animate; xora-app.js snaps the height instead of transitioning it -------- */
@media (prefers-reduced-motion: reduce) {
  .act[data-template='xora'] .react-slidedown { transition: none; }
  .act[data-template='xora'] .pricing-head_btn_before { transition: none; }
}

/* ========================= §5i TEMPLATE: KONONENKO ========================
   Act 09 — the growth bureau. Ported from the catalog template
   `footprint-kononenko` (a pure HTML+CSS+vanilla-JS document: no libraries,
   no frameworks, 140 CSS rules and a 20 KB scroll engine).

   assets/kono.css carries the whole ported stylesheet, re-rooted under
   `.kono-root` by work/build/kono_port.py. Everything below is only what the
   JOURNEY has to re-arrange. Four traps, all of which bit during the port:

   1. `position: fixed` — the template's header.kbp is fixed to the viewport
      and inverts itself with mix-blend-mode:difference. Inside an act the
      track is translated thousands of pixels sideways, so `fixed` resolves
      against the transformed track and the bar sails off with the reading.
      Dropped from the markup entirely (the "Calcom nav trap", bug 6 in the
      port skill): the journey owns one chrome for all acts.

   2. `position: sticky` / tall pinned sections. The template's hero, its
      manifesto (.nmc) and its carousel (.nwv) are all PINNED: the section is
      200–300vh tall and an inner `.pin` is sticky, so a scrub() can walk the
      scroll through it. A panel here is one 100vh frame and the window never
      scrolls, so a sticky pin has nothing to stick to and the content lands
      300vh tall inside a 100vh box. Each panel is therefore cut down to its
      own frame and the scrubbed choreography is re-expressed against the
      panel's own `--lp` (see kono-app.js): same sequence, same easing, same
      staging, keyed on the act's horizontal progress instead of the page's.

   3. `:root` scoping. The template declares its palette and its two type
      families on `:root` — which IS `html`. A scoped `.kono-root :root { … }`
      can therefore NEVER match (an element cannot be its own ancestor), so
      every single custom property died and the act rendered in the browser's
      default serif. kono_port.py rewrites `:root` to `.kono-root` so the vars
      are redeclared ON the act root and inherit down. This is the single most
      invisible port bug in the catalogue.

   4. Section padding. The template lays out as a tall vertical document with
      `padding: 2em 0` on every section plus 8–11em screeds on the headline
      blocks. In a fixed-height frame that overflow must come down. `.kono-fit`
      is the one box per panel that FPKono.fit() measures and scales.

   The reveals. The template fires them from an IntersectionObserver watching
   DOCUMENT scroll, adding `.in` to each `.rev`. Inside an act the window never
   scrolls, so the observer never fires and every masked line stays parked
   105% below its mask. Rather than ship an observer to fake it, the SAME
   result is expressed in CSS against the panel's own liveness — journey.js
   adds `.is-live` to a panel within 0.9 of centre — with the same 1.05s
   cubic-bezier(.22,1,.36,1) and the same per-line --dd stagger the template
   used. Panel-liveness IS the trigger.
   ------------------------------------------------------------------------- */

.act[data-template='kono'] {
  --t-bg: #ffffff;
  --t-ink: #000000;
  --t-dim: rgba(0,0,0,.55);
  --t-line: #eeeeee;
  --t-accent: #000000;
}
.act[data-template='kono'] .act__stage { background: #ffffff; }
/* Panel 00 of this act is a full-bleed photograph of a white building under a
   pale sky, and the HUD sits at bottom:74px — straight across the glass. The
   act's own --t-dim is rgba(0,0,0,.55), so the inherited dark HUD landed on the
   lightest part of the image and became unreadable. The acts that needed a HUD
   colour override (ge, books, kage) each got one for exactly this reason; kono
   was simply missed, because its defect is only visible on the one panel with
   a photo under the chrome. rgba(0,0,0,.62) is the --t-dim family so the HUD
   still belongs to the act, just dark enough to hold against the sky. */
.act[data-template='kono'] .act__hud { color: rgba(0,0,0,.62); }

/* ---- trap 4, done properly: the frame, and the document heights -----------
   The template sizes itself for a tall scrolling document. Measured on the
   live act, three of its sections arrive at a height that has nothing to do
   with a 100vh frame:

     .nmc  1800px   (hard-coded — it exists to give its sticky pin something
                      to stick to while the ground fades and the fan turns)
     .lxo  2575px   (ten masonry projects in two columns, 88px apart)
     .nwv  2940px   (the conveyor's arc + the essay + the headline, stacked)
     .uod   876px   (the orbit needs the extra height for its ellipse)

   None of those can be scrolled inside a panel, so all of that height is dead
   space that pushes content out of frame. Each section is therefore clamped to
   the frame, and `.m-fit` — the ONE box per panel that FPKono.fit() measures —
   is what absorbs whatever still overflows, by scaling.

   The `.kono-fit` name used before this was a plain mistake: the markup wraps
   in `.kono-root.m-fit`, and a stylesheet rule for a class the markup never
   carries is a rule that silently does nothing. `.m-fit` is the real box. */
.kono-root.m-fit {
  width: 100%;
  max-height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  transform-origin: 50% 50%;
}
/* the panel is already `display:flex; align-items:center` from §3 — that is
   what lets `.m-fit { max-height: 100% }` actually constrain the box instead
   of the box growing to its content and being clipped by the stage. */
.act[data-template='kono'] .panel { align-items: stretch; }
.act[data-template='kono'] .panel > .kono-root.m-fit { justify-content: center; }

/* ===========================================================================
   THE SECTION-HEIGHT POLICY, STATED ONCE.

   The template sizes every section for a TALL SCROLLING DOCUMENT and lets the
   reader scroll each one past. Two of its sections even hard-code a height
   (`.ean{height:900px}`, `.nmc 1800px`). Inside an act nothing scrolls, so all
   of that height is dead space that pushes content out of the frame — and
   removing it naively collapses a section to 0 (the hero, which is nothing but
   absolutely-positioned children).

   Three earlier attempts each failed in a different, visible way, and the
   failures are worth naming because each produced a plausible-looking bug:

     1. `height: auto` for everything       -> hero h=0, starburst h=0
     2. `height: 62vh` for the hero         -> hero rendered, but letterboxed
                                               (a 1296x558 plate floating in a
                                               1440x900 black frame)
     3. nothing for the content sections    -> `.phv` measured 289px in a 900px
                                               frame, so its content sat in a
                                               narrow band with 300px of white
                                               above and below. `.nmc` stayed 0.

   The rule that actually matches the template's design:

     * `.m-fit` is the frame's full height (100% of the panel's content box).
     * Every direct child section gets `min-height: 100%`, so the template's own
       internal layout (its grids, its `justify-content`, its padding) has a
       full frame to distribute WITHIN — which is what it was designed against.
     * Sections whose children are ALL absolutely positioned (hero, starburst)
       get an explicit `height: 100%` too, because `min-height` cannot give them
       content to fill and they would still collapse.
     * Only one section is allowed to exceed the frame (`.nwv`, the conveyor +
       essay + headline stack, 78vh of content in a 900px frame) — `.m-fit`
       scales that one down, and that is the single intended use of the scale.

   SPECIFICITY NOTE, because this cost the most time. The broad clamp
   `… .panel .kono-root > section` is (0, 4, 2) — `> section` is a TYPE
   selector, so it counts in the third slot. `… .panel .ean` is only (0, 4, 1),
   so the broad clamp BEATS the specific-looking one. Every per-section
   override below therefore repeats the section element (`section.ean`), which
   reaches (0, 4, 2) and wins on source order. Verified with CDP
   `CSS.getMatchedStylesForNode` — the matched-declaration list is the only
   honest answer to "which rule won", and reading the file was misleading.
   =========================================================================== */
.act[data-template='kono'] .panel .kono-root > section,
.act[data-template='kono'] .panel .kono-root > footer {
  min-height: 100%;
  height: auto;
  max-height: 100%;
  padding-top: 0;
  padding-bottom: 0;
  box-sizing: border-box;
}
/* pure absolute-positioning stages: no content height of their own, so they
   need an explicit height rather than a minimum. */
.act[data-template='kono'] .panel .kono-root > section.ean,
.act[data-template='kono'] .panel .kono-root > section.nmc {
  height: 100%;
  min-height: 0;
  flex: 0 0 auto;
}
/* .nwv is the conveyor + essay + headline stack, and every one of its three
   layers is ABSOLUTELY positioned inside `.stage`/`.essayblock` (`top:11%`,
   `top:50%`). It therefore has no intrinsic height to justify a vh value, and
   at 78vh it left a 198px white band above the black stage — visible in the
   panel-07 screenshot as the black ground failing to reach the top edge. It
   fills the frame like every other section; the layers redistribute. */
.act[data-template='kono'] .panel .kono-root > section.nwv {
  height: 100%;
  min-height: 0;
  flex: 0 0 auto;
}
.act[data-template='kono'] .panel .nmc .pin {
  position: absolute;             /* was `sticky; top: 0` — nothing to stick to */
  inset: 0;
  height: 100%;
}
/* panel 05 · selected work. The template's masonry is a two-column stack
   2568px tall (ten projects, 88px apart) inside a document that scrolls. In a
   648px band under the headline, `max-height:72vh; overflow:hidden` showed the
   first three projects and hid the other seven — measured, not guessed:
   masonry 648px vs columns 2568px and 2370px.

   Rather than clip, the grid is turned into FOUR columns of smaller cards, so
   all ten projects are present in the frame. The `aspect-ratio` caps on `.img`
   keep each card shallow enough that two rows fit the band. */
.act[data-template='kono'] .panel .lxo .masonry {
  max-height: none;
  overflow: visible;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 20px 18px;
}
.act[data-template='kono'] .panel .lxo .mcol {
  display: contents;              /* the two template columns dissolve */
}
.act[data-template='kono'] .panel .lxo .project .img { aspect-ratio: 3 / 2; }
.act[data-template='kono'] .panel .lxo .project.tall .img { aspect-ratio: 3 / 2; }
.act[data-template='kono'] .panel .lxo .project .meta { font-size: 10px; }
.act[data-template='kono'] .panel .lxo .project .metric { font-size: 10px; }
.act[data-template='kono'] .panel .lxo h2 { margin-bottom: 20px; font-size: clamp(38px, 5.6vw, 76px); }
.act[data-template='kono'] .panel .nwv .stage { height: 100%; }
/* panel 07 is three stacked layers in the template — the conveyor arc (a
   `.stage` behind), the essay block, and the centred headline — laid out down
   a 2940px section so they never met. Crammed into one 900px frame they
   overprint: the essay's third line landed on top of the headline's two body
   paragraphs (visible in the panel-07 screenshot as doubled text). Rather than
   shrink everything until it happens to clear, the three layers are given
   explicit non-overlapping zones: essay in the upper third, centred headline
   below it, conveyor as the ground behind both. */
.act[data-template='kono'] .panel .nwv .essayblock {
  position: absolute;
  left: 0; right: 0;
  top: 9%;
  z-index: 3;
  padding-inline: var(--gut);
}
.act[data-template='kono'] .panel .nwv .essayblock .essay {
  font-size: clamp(26px, 3.6vw, 50px);
  max-width: 34ch;
}
.act[data-template='kono'] .panel .nwv .headline h2 { font-size: clamp(34px, 5.2vw, 76px); }
/* the headline's own centre sits low in the frame once the essay owns the top */
.act[data-template='kono'] .panel .nwv .headline { top: 58%; }
.act[data-template='kono'] .panel .uod { min-height: 74vh; }
.act[data-template='kono'] .panel .uod h2 { font-size: clamp(28px, 4.2vw, 62px); }
/* the giant wordmark and the multi-column footer both arrive sized for a wide
   document; inside the frame they need to come down or they push the columns
   out the bottom */
.act[data-template='kono'] .panel footer.anj .word { font-size: clamp(60px, 12vw, 168px); }
.act[data-template='kono'] .panel footer.anj .top { gap: 28px; }

/* every `.w1440` takes the frame's width rather than insisting on 1440px.
   At a 1440 viewport its max-width exactly equalled the panel's content box,
   which is why a headline sized with `clamp(…, 9.4vw, 131px)` was clipped on
   the left: the section laid out at 1440 and the panel clipped at 1296. */
.act[data-template='kono'] .panel .w1440 {
  max-width: 100%;
  padding-left: 0;
  padding-right: 0;
}

/* THE GUTTER, MOVED OFF THE PANEL.
   §3 gives every `.panel` `padding: 0 var(--gut)`. That is right for acts whose
   content is a centred column, and wrong for this one: the hero plate, the
   starburst, the conveyor and the orbit are all FULL-BLEED stages that must
   reach the frame edge. With the panel's 72px gutter in place the hero plate
   rendered 1296px wide inside a 1440px frame — measured — which is not a
   design choice, it is the gutter showing. The first screenshot of the fix
   showed exactly that: a letterboxed plate with black bars down both sides.
   So the padding comes off the panel here and is re-applied as an inset on the
   content sections, which is where the template put it in the first place. */
.act[data-template='kono'] .panel { padding: 0; }
.act[data-template='kono'] .panel .kono-root > section.ean,
.act[data-template='kono'] .panel .kono-root > section.nmc,
.act[data-template='kono'] .panel .kono-root > section.lxo,
.act[data-template='kono'] .panel .kono-root > section.uod,
.act[data-template='kono'] .panel .kono-root > footer.anj {
  padding-inline: var(--gut);
}
/* the hero is the one stage with NO inset — its plate, badge and caption are
   positioned against the frame edge in the template's own CSS. */
.act[data-template='kono'] .panel .kono-root > section.ean { padding-inline: 0; }

/* the hero is full-bleed by design (its GF badge and caption sit outside the
   text gutter), and the star / conveyor / orbit are deliberately wider than
   the frame — the fan is 1500px against a 900px frame because its rays reach
   past the edges. All of that is intended, and all of it must be CLIPPED to
   the panel or it shows through into the neighbouring panel's frame (which is
   exactly what the first screenshots of this act showed: panel 01's headline
   and panel 02's index visible inside panel 00's shot). The stage already has
   `overflow:hidden`, but the panel is 100vw and the NEXT panel starts at
   exactly 100vw — so a 1px rounding leak is enough to bleed. Clipping on the
   section as well costs nothing and removes the leak entirely. */
.act[data-template='kono'] .panel .ean,
.act[data-template='kono'] .panel .nmc,
.act[data-template='kono'] .panel .nwv,
.act[data-template='kono'] .panel .uod {
  overflow: hidden;
}

/* the display faces are sized with `clamp(…, 9.4vw, 131px)` — 9.4vw of a
   1440 viewport is 135px, i.e. the CLAMP CEILING, tuned for a 1440px document
   column. The panel's content box is 1296px, and the template's own
   `max-width: 14ch` allows the line to run wider than that, so a headline
   measured 844px wide inside a 1296px box but still clipped its last glyph
   against the frame edge and pushed the body copy off the right. Cap the
   display sizes against vw so they track the frame rather than the viewport. */
.act[data-template='kono'] .panel .phv h2,
.act[data-template='kono'] .panel .lxo h2 { font-size: clamp(40px, 7.6vw, 104px); }
.act[data-template='kono'] .panel .pvg .head h2 { font-size: clamp(34px, 6vw, 82px); }
.act[data-template='kono'] .panel .lmr .essay { font-size: clamp(30px, 5vw, 62px); }
.act[data-template='kono'] .panel .aie .essay { font-size: clamp(22px, 2.6vw, 38px); }
.act[data-template='kono'] .panel .pvg .essay { font-size: clamp(20px, 2.4vw, 34px); }
.act[data-template='kono'] .panel .ean .caption { font-size: clamp(30px, 4.4vw, 68px); }
.act[data-template='kono'] .panel .ean .badge { font-size: clamp(70px, 11vw, 160px); }

/* --- the reveals, keyed on liveness instead of on document scroll --------- */
.act[data-template='kono'] .panel.is-live .rev > .ln {
  transform: translateY(0);
  transition: transform 1.05s cubic-bezier(.22,1,.36,1);
  transition-delay: var(--dd, 0ms);
}
/* --- the hero's dolly zoom, keyed on how far the panel has travelled ------
   These use abs(--lp) so they agree with kono-app.js's orientation: the hero
   is AT REST when centred (|lp| = 0) and dolly-zooms as it leaves. The app
   writes these inline and wins; this block is the CSS-only fallback and the
   pre-first-frame state, so it must not disagree with the app about which
   direction is "at rest". */
.act[data-template='kono'] .ean .pin {
  transform: translate3d(0, calc(abs(var(--lp, 0)) * 180px), 0);
}
.act[data-template='kono'] .ean .dolly {
  transform: scale(calc(1 + abs(var(--lp, 0)) * 0.55));
  opacity: calc(1 - abs(var(--lp, 0)) * 1.15);
}
.act[data-template='kono'] .ean .vig {
  opacity: calc(.15 + abs(var(--lp, 0)) * .85);
}

/* --- the manifesto fan — rotates as the panel comes to centre -------------
   kono-app.js writes --kono-spin / --kono-dark / --kono-inhead on the panel
   every frame; these rules are what consume them, and they are also the
   complete fallback if the act's JS never lands (the panel simply holds the
   fan at rest on white). */
.act[data-template='kono'] .nmc .star { opacity: var(--kono-fan, 1); }
.act[data-template='kono'] .nmc .fan {
  transform: rotate(calc(var(--kono-spin, 0) * 1rad));
}
.act[data-template='kono'] .nmc .scrim { background: #000; opacity: var(--kono-dark, 0); }
.act[data-template='kono'] .nmc .core h2 { color: var(--kono-inhead, #000); }
.act[data-template='kono'] .nmc .nmc-b .slide > span { transform: translateY(105%); }
.act[data-template='kono'] .nmc .slide > span { transition: transform .8s cubic-bezier(.22,1,.36,1); }
/* the ray colour variables the app sets inline, declared here so the leaf
   rules in kono.css have something to resolve to on the CSS-only path */
.act[data-template='kono'] .nmc {
  --rayline: rgba(0,0,0,.30);
  --raylabel: rgba(0,0,0,.75);
}

/* --- the photo conveyor: tiles are placed by kono-app.js, only the frame -- */
.act[data-template='kono'] .nwv .stage { position: relative; overflow: hidden; }

/* --- the projected images: settle from 1.06 rather than popping in --------
   NOTE: no animation here on purpose. kono.css already ships `.project .img
   { transform: scale(1.06) }` and kono-app.js writes the settle inline every
   frame. A third writer — a keyframe animation with `fill: both` — would take
   precedence over the inline style for as long as it ran, which is exactly the
   class of bug that makes a "settle" look like a stutter. The template's own
   1.06 seed plus the app's per-frame lerp is the whole effect. */
@media (prefers-reduced-motion: reduce) {
  .act[data-template='kono'] .ean .pin,
  .act[data-template='kono'] .ean .dolly { transform: none; }
  .act[data-template='kono'] .ean .vig { opacity: .85; }
}
/* ========================= §5j TEMPLATE: SKETCHBOOK ========================
   ACT 10 · FIELD NOTES  (FP-ACT10-04, Al, 2026-09-26)

   The template is a book showroom: a 100vh hero holding a 3D sketchbook, then
   a rule, an about, a rule, and a nine-plate index, laid out as one scrolling
   document. Re-cut into four panels on a horizontal track, three of its
   document behaviours stop meaning anything and one becomes a hard
   requirement:

     * nothing scrolls, so every `scrollIntoView` is a no-op
       (the engine drops them; see sketch-app.js)
     * the hero's `.hero-down` chevron pointed at `#about` — removed
     * the four panels must each be a single frame, so the hero's stage has to
       be told its height rather than deriving one from a document

   The block below is therefore small on purpose. Almost everything the act
   needs is already in sb.css; what lives here is (a) the white stage, (b) the
   per-panel frame sizing, and (c) the one number the template could not know:
   how tall the book's stage may be.

   A NOTE ON `.sb-root`. The stylesheet is scoped, so its own rules only ever
   override what they declare. Any property it leaves unset falls through to
   whatever journey.css says about the same element — which is how act 09's
   founder grid ended up 2137px off-frame through a shared `.rail`. The
   collision audit in sb_port.py reports the overlap; it is currently clean.
   ========================================================================== */
/* sb.css already declares the palette it needs (--paper, --ink, --display,
   --font, --earth, --hairline). It does so on `.sb-root`, which is exactly the
   right scope — a second alias set here would be a second source of truth for
   the same colours, and the two would drift. The only thing this act adds is
   the stage height, which the engine writes and no stylesheet could know. */
/* the template paints a paper wash behind everything; the act wants its own
   white stage so the panels read as pages of the same document */
.act[data-template='sketch'] .act__stage { background: #f6f3ec; }
.act[data-template='sketch'] .panel { padding: 0; }

/* --- the four panels, sized to the frame ---------------------------------- */
.act[data-template='sketch'] .panel > .sb-root {
  width: 100%;
  max-height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  transform-origin: 50% 50%;
}
/* The hero needs the whole frame: the book is the point of the panel. The
   stage height is written by FPSketch.fit() as `--sb-stage-h` — a number the
   template never had to compute because its stage sat in a 100vh document
   with the kicker above it and nothing below. */
.act[data-template='sketch'] .sb-hero-wrap {
  height: 100%;
  /* the book is the point of this panel: let it take the slack, and keep the
     kicker above and the hint below at their natural size. `flex-start` plus a
     fixed top pad left a dead band under the book; centring the whole stack
     and letting the stage absorb the difference is what fills the frame. */
  justify-content: center;
  align-items: stretch;
  padding-top: clamp(22px, 3.6vh, 44px);
  position: relative;
  overflow: hidden;
}
.act[data-template='sketch'] .sb-hero-kicker {
  flex: 0 0 auto;
  margin: 0 0 clamp(10px, 1.6vh, 20px);
  text-align: center;
}
.act[data-template='sketch'] .sb-wrap {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.act[data-template='sketch'] .sb-stage {
  flex: 1 1 auto;
  min-height: 0;
  height: var(--sb-stage-h, 62vh);
}
/* the botany plates are the template's corner ornament — they sit behind the
   book and must not intercept a drag aimed at it */
.act[data-template='sketch'] .sb-botany { pointer-events: none; z-index: 0; }
.act[data-template='sketch'] .sb-wrap,
.act[data-template='sketch'] .sb-captions,
.act[data-template='sketch'] .sb-tools,
.act[data-template='sketch'] .sb-hint { position: relative; z-index: 2; }

/* --- the about panel ------------------------------------------------------ */
.act[data-template='sketch'] .sb-about-wrap {
  height: 100%;
  padding-inline: var(--gut);
  justify-content: center;
}
.act[data-template='sketch'] .sb-about-wrap .sb-about {
  position: relative;
  max-width: 1180px;
  margin-inline: auto;
  width: 100%;
}
/* the bloom plate is absolutely placed by the template and can spill; the
   panel clips it rather than letting it cross into the neighbouring frame */
.act[data-template='sketch'] .sb-about-wrap { overflow: hidden; }
.act[data-template='sketch'] .sb-about-wrap .sb-bloom { pointer-events: none; }

/* --- the entries panel ---------------------------------------------------- */
.act[data-template='sketch'] .sb-plates-wrap {
  height: 100%;
  padding-inline: var(--gut);
  justify-content: center;
  overflow: hidden;
}
.act[data-template='sketch'] .sb-plates-wrap .sb-plates {
  width: 100%;
  /* The template caps its prose column at 1080px because a 1080px measure is
     right for running text. A nine-cell index is not running text: at 1180px
     across three columns each cell was 339px, which left the table looking
     sparse while the frame held 130px of dead margin either side. The index
     gets the frame; the about and colophon panels keep the narrower measure. */
  max-width: 1320px;
  margin-inline: auto;
}
/* The template's plate list is one long column down a scrolling document. In a
   single frame it has to be a grid, or eight of the nine entries fall below the
   fold — the same failure act 09's masonry had, and the same fix: re-lay it and
   assert that every entry is actually inside the frame. */
.act[data-template='sketch'] .sb-plates-wrap .plate-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0 34px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.act[data-template='sketch'] .sb-plates-wrap .plate-list > li { min-width: 0; }
/* The template's row is one line across a 1080px document:
     grid-template-columns: 3.4em minmax(0,1fr) auto
   In a 339px grid cell that same rule crushes the title to 25px (measured)
   while the uppercase label keeps 215px, so "Phase 03 · Create Demand" wraps
   to four lines and the row swells to 201px. The parts are not competing for
   one line any more, so they stop sharing one line: the number + title lead,
   the label sits under them as a caption. */
.act[data-template='sketch'] .sb-plates-wrap .plate-list .sb-plate {
  display: grid;
  grid-template-columns: 2.6em minmax(0, 1fr);
  grid-template-areas:
    "n t"
    ". p";
  gap: 2px 10px;
  align-items: baseline;
  padding: 13px 4px;
}
.act[data-template='sketch'] .sb-plates-wrap .plate-list .sb-plate .n { grid-area: n; }
.act[data-template='sketch'] .sb-plates-wrap .plate-list .sb-plate .t {
  grid-area: t;
  font-size: clamp(17px, 1.5vw, 22px);
  line-height: 1.18;
}
.act[data-template='sketch'] .sb-plates-wrap .plate-list .sb-plate .p {
  grid-area: p;
  text-align: left;
  font-size: 10.5px;
  letter-spacing: .13em;
  line-height: 1.35;
}
/* every row lands on the same rhythm, so the nine read as a table rather
   than nine different-sized blocks */
.act[data-template='sketch'] .sb-plates-wrap .plate-list .sb-plate { min-height: 92px; }

/* --- the colophon panel --------------------------------------------------- */
.act[data-template='sketch'] .sb-foot-wrap {
  height: 100%;
  padding-inline: var(--gut);
  justify-content: center;
  overflow: hidden;
}
.act[data-template='sketch'] .sb-colophon {
  width: 100%;
  max-width: 1180px;
  margin-inline: auto;
}
.act[data-template='sketch'] .sb-colophon .sb-section-label { margin: 0 0 clamp(8px, 1.4vh, 16px); }
.act[data-template='sketch'] .sb-foot-title {
  font-family: var(--display, Georgia, serif);
  font-weight: 400;
  font-size: clamp(30px, 4.2vw, 62px);
  line-height: 1;
  letter-spacing: -.02em;
  color: var(--ink);
  margin: 0 0 clamp(14px, 2.4vh, 30px);
}
.act[data-template='sketch'] .sb-foot-rule {
  width: 100%;
  height: 1px;
  background: var(--hairline);
  margin: 0 0 clamp(20px, 3.4vh, 40px);
}
/* the ledger: six ruled entries across the frame, the way the whole act is a
   record of builds, not a brochure */
.act[data-template='sketch'] .sb-foot-grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: clamp(16px, 2.4vw, 34px);
  margin: 0 0 clamp(22px, 4vh, 48px);
}
.act[data-template='sketch'] .sb-foot-grid > div { min-width: 0; }
.act[data-template='sketch'] .sb-foot-grid dt {
  font-family: var(--font, system-ui, sans-serif);
  font-size: 10px;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 8px;
}
.act[data-template='sketch'] .sb-foot-grid dd {
  font-family: var(--font, system-ui, sans-serif);
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-soft, var(--ink));
  margin: 0;
}
.act[data-template='sketch'] .sb-foot-grid dd a { color: var(--earth); text-decoration: underline; text-underline-offset: 3px; }
/* the wordmark runs the width of the frame and is clipped by it, exactly as
   act 09's footer does — it reads as a printer's mark on the last leaf */
.act[data-template='sketch'] .sb-foot-word {
  display: block;
  font-family: var(--display, Georgia, serif);
  font-size: clamp(64px, 13.5vw, 200px);
  line-height: .82;
  letter-spacing: .04em;
  color: var(--ink);
  opacity: .1;
  white-space: nowrap;
  overflow: hidden;
  text-align: center;
  user-select: none;
  /* The wordmark runs the full frame, like act 09's, rather than being boxed
     inside the 1180px ledger. `text-align:center` centres the line inside the
     box, but letter-spacing leaves a trailing gap after the last glyph that
     drags the whole line left, so that gap is pulled back. */
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

/* --- clearing the journey's fixed chrome ---------------------------------- */
/* `.hud` is journey-wide chrome: position:fixed, left:var(--gut), bottom:26px,
   width:min(320px,60vw), mix-blend-mode:difference. It is identical on every
   act, so it is not this act's to restyle. What IS this act's job is to keep
   its own furniture out of that box — the template's `.foot` sat bottom-left
   at 11.5px, directly under the HUD, and the two overprinted into an unreadable
   smear ("CHAPTER 10 · THE JOURNAL IN INK" behind "10 · FIELD NOTES").

   So: the corpus footer is centred and lifted, and the hero's hint line is
   cleared off the bottom edge. Nothing in this act is anchored bottom-left. */
.act[data-template='sketch'] .sb-hint {
  /* the template pins this to the panel's bottom-left — inside the HUD box */
  position: static;
  margin: clamp(14px, 2.2vh, 26px) auto 0;
  text-align: center;
  inset: auto;
  left: auto; bottom: auto;
  max-width: 100%;
}
.act[data-template='sketch'] .sb-hero-wrap { padding-bottom: clamp(18px, 3.4vh, 40px); }

/* --- reduced motion: the book stays a book, it just stops shuffling -------
   The engine already short-circuits the riffle and commits a turn instantly
   when the media query matches; this block settles the purely decorative
   motion that CSS owns. */
@media (prefers-reduced-motion: reduce) {
  .act[data-template='sketch'] .sb-caption,
  .act[data-template='sketch'] .sb-hint { transition: none; }
  .act[data-template='sketch'] .sb-3d { transition: none; }
}
/* ========================= §5k TEMPLATE: SHELF ===========================
   ACT 08 · THE LIBRARY  (FP-ACT08-01, Al, 2026-09-26)

   The template is a 3D bookcase you orbit: a `position:fixed; inset:0`
   application holding one canvas, a masthead, a selection strip, a marker rail
   and a detail dialog — all of it ONE state machine. Six books, a spine
   marker per book, a click that flies the camera in and opens the cover.

   The cut is two panels, and it is cut along the template's own seam: panel 00
   is the whole interactive application unchanged, panel 01 is the template's
   `static-fallback` section promoted from a hidden no-WebGL state to a real
   panel. Splitting the application across four frames would mean
   re-implementing the state machine, so the act keeps its shape and the
   journey adapts to it instead.

   Four things below are the JOURNEY's job, not the template's:

   1. ACT TOKENS. `--t-*` is what the shared act furniture (the seam, the HUD
      tint, the reserved gutters) reads. The act's own palette stays on
      `.sh-root` in shelf.css — `--paper: #171a24`, `--ink: #f4eee6`,
      `--accent: #c87046`. These tokens are the same colours under the
      journey's names, so the seam gradient and the HUD do not fight the act.

   2. `position: fixed` -> `absolute` (THE ONE THAT MATTERS). `.sh-experience`
      and `.sh-pointer-label` are both `fixed` in the template. Inside the
      journey the act track is translated thousands of pixels sideways, and
      `fixed` resolves against the VIEWPORT — so the hover chip would float
      over whichever act happened to be on screen, thousands of pixels from the
      book it names. Both become `absolute` so the act becomes their containing
      block. The JS side of the same bug is fixed in shelf-app.js
      (`positionPointerLabel` subtracts the act's own rect), because with an
      absolute containing block the coordinates have to be act-local anyway.

   3. THE FALLBACK IS A PANEL, NOT A STATE. The template hides
      `.sh-static-fallback` behind `.sh-webgl-ready`. Here it is panel 01, and
      the engine only ever adds `sh-webgl-ready` to panel 00's root — the two
      are siblings, so the descendant selector cannot match and panel 01 is
      visible unconditionally. The rule is restated scoped to panel 00 anyway,
      so a future edit to the engine cannot quietly hide half the act.

   4. CLEARING THE FIXED CHROME. `.hud` is journey-wide: `position:fixed`,
      `left:var(--gut)`, `bottom:26px`, `min(320px,60vw)`, `mix-blend-mode:
      difference`. The template's `.sh-browse-ui` is pinned `bottom:38px; left:
      42px` — directly on top of it. "01 / 07 · CLARIFY" in 0.66rem mono
      inverted against "08 · THE LIBRARY" is unreadable. So the act's own
      bottom-left furniture is lifted clear and the counter/label is centred.
   ========================================================================== */

.act[data-template='shelf'] {
  --t-bg: #171a24;
  --t-ink: #f4eee6;
  --t-dim: #b9b4ae;
  --t-line: rgba(244, 238, 230, 0.24);
  --t-accent: #c87046;
  --t-accent2: #a8b6c9;
  --t-fd: var(--f-display);
  --t-fm: var(--f-mono);
  background: #171a24;
}

/* --- 1 · the act is the containing block, not the viewport ---------------- */
.act[data-template='shelf'] .sh-experience {
  /* was `position: fixed; inset: 0` — see note 2 above */
  position: absolute;
  inset: 0;
}
.act[data-template='shelf'] .sh-pointer-label {
  /* same trap, and worse: this one carries the book's name, so a wrong
     position is not just ugly, it is a lie about what the visitor is
     pointing at. shelf-app.js measures against the act box to match. */
  position: absolute;
}

/* the panel is a 100vh frame, so the experience fills it exactly */
.act[data-template='shelf'] .panel { padding: 0; }
.act[data-template='shelf'] .sh-root { min-height: 100%; }

/* --- 2 · the fallback is panel 01, so it is never hidden ------------------
   The engine sets `sh-webgl-ready` on panel 00's `.sh-experience` only. Panel
   01's `.sh-static-fallback` is a SIBLING of that, never a descendant, so
   this rule cannot match it. Restated explicitly against the panel-00 root so
   the guarantee is legible here rather than resting on a structural accident. */
.act[data-template='shelf'] .sh-experience.sh-webgl-ready .sh-static-fallback {
  display: none;
}
.act[data-template='shelf'] .panel .sh-static-fallback {
  display: grid;
}

/* --- 3 · clearing the journey's fixed chrome -----------------------------
   The HUD is journey-wide: position:fixed, left:var(--gut), bottom:26px,
   width:min(320px,60vw), mix-blend-mode:difference. Measured at 1440x900 it
   occupies x 72–392, y 831–874.

   The template's `.sh-browse-ui` is `bottom:38px; left:42px` — the same
   corner, 98px tall, so its lower half lands squarely inside the HUD box. The
   strip's left cell (`.sh-selection`) is what actually collides: at
   x 256–394, y 772–870 it overprints the HUD's "08 / 10" counter with
   "01 / 07 · CLARIFY" in 0.66rem mono, and `mix-blend-mode:difference` turns
   the overlap into an unreadable smear rather than a clean stack.

   The fix is VERTICAL, not horizontal. Centring the selection over the frame
   was tried first and is wrong: the HUD is 320px wide at the LEFT, so a
   centred cell still crosses it, and the act's own three-column composition
   (selection · actions · marker rail) is a deliberate balance that should not
   be broken to dodge a 43px chrome box. Lifting the strip above the HUD's top
   edge keeps the template's composition intact and clears the collision for
   any HUD width up to the act's full frame.

   96px puts the strip's bottom edge at y=804 in a 900px viewport, which is
   27px above the HUD's top. The strip is `align-items:end`, so the buttons and
   the marker rail rise with it. */
.act[data-template='shelf'] .sh-browse-ui {
  bottom: 96px;
  left: 42px;
  right: 42px;
}

/* The counter sits above the title in the template's own vertical stack, and
   the title is the act's headline — it stays flush left with the note, which
   is what the template's composition reads as. Only the strip moved. */
.act[data-template='shelf'] .sh-counter { padding-bottom: 0; }
.act[data-template='shelf'] .sh-selection__note { max-width: 520px; }

/* The marker rail is bottom-RIGHT and never reaches x<392, so it is not in
   collision. It only needs to not crowd the frame edge. */
.act[data-template='shelf'] .sh-index-nav { padding-bottom: 0; }

/* Short viewports: the HUD does not move and the strip must not be pushed up
   off the top of the books. Below ~700px of height the gap is reduced rather
   than dropped, so the collision stays impossible. */
@media (max-height: 760px) {
  .act[data-template='shelf'] .sh-browse-ui { bottom: 84px; }
}

/* --- 4 · the frame and the masthead need the journey's gutters ------------ */
.act[data-template='shelf'] .sh-editorial-header {
  padding: 22px 42px;
}

/* --- 5 · reduced motion: the shelf holds still, it stays a shelf ----------
   The engine already stops damping and commits a page turn instantly when
   the query matches. This is the CSS half: no dust drift, no marker pulse. */
@media (prefers-reduced-motion: reduce) {
  .act[data-template='shelf'] .sh-experience,
  .act[data-template='shelf'] .sh-pointer-label,
  .act[data-template='shelf'] .sh-browse-ui,
  .act[data-template='shelf'] .sh-detail-panel { transition: none; }
  #shelf-scene { transition: none; }
}

/* ============================================================================
   FP-MOBILE-08 (Al, 2026-09-28): the Shelf phone fix has to live HERE, last in
   the file, and the reason is a CSS rule I had to be reminded of.

   *** A @media block adds NO specificity. ***

   FP-MOBILE-07 put these same overrides inside the `@media (max-width: 900px)`
   block at line ~2175, reasoning that a media query "wins because it is more
   specific". It does not. A media query is a CONDITION, not a selector — the
   declarations inside it compete at exactly the specificity of the selector
   itself. So `.act[data-template='shelf'] .sh-experience` in my media block and
   `.act[data-template='shelf'] .sh-experience` at line 3379 are the same
   selector with the same weight, and the tie is broken by SOURCE ORDER alone.
   Line 3379 is 1200 lines LATER, so it won, and shelf p0 and p1 stayed 0px
   while kono — whose base rules happen to sit EARLIER at 2846-2867 — started
   passing. The two acts behaved differently for one reason: file position.

   This block is last in journey.css, so it is last in the cascade. Two more
   things had to be true for it to work, both learned the hard way today:

     - journey.css is stylesheet index 9, AFTER shelf.css (6) and kono.css (7).
       The act sheets are injected by loadScript on approach, which appends
       them to <head> after the static <link>s are parsed. Reading index.html
       and concluding "journey.css is first" is wrong; only
       document.styleSheets is authoritative.
     - and it must therefore beat `[data-template]` attribute selectors, which
       are worth a whole class each.

   The mechanism being undone is the desktop fix at 3379-3393, which correctly
   makes `.sh-experience` fill a `height: 100vh` panel. On a phone that panel
   is `height: auto`, so "fill the panel" computes to zero. Same class of bug
   as the kono sections: a desktop act-fix that silently assumes a fixed-height
   panel. */
@media (max-width: 900px) {
  .act[data-template='shelf'] .sh-experience {
    position: relative;
    inset: auto;
    height: auto;
    min-height: 70vh;
    overflow: visible;
  }
  .act[data-template='shelf'] .sh-root { min-height: 70vh; }
  .act[data-template='shelf'] .sh-experience .sh-scene-shell,
  .act[data-template='shelf'] .sh-scene-shell {
    position: relative;
    inset: auto;
    min-height: 44vh;
    height: auto;
  }
  /* the fixed chrome has no meaning without a fixed viewport, and the frame
     outline is drawn against a viewport-sized box that no longer exists */
  .act[data-template='shelf'] .sh-frame,
  .act[data-template='shelf'] .sh-masthead,
  .act[data-template='shelf'] .sh-hero-word,
  .act[data-template='shelf'] .sh-folio { display: none; }
  /* panel 01 is the static bookcase fallback; it is a grid, and a grid with no
     height for its rows collapses the same way */
  .act[data-template='shelf'] .panel .sh-static-fallback {
    min-height: 60vh;
    height: auto;
  }
  /* diag-shelf-p1.mjs measured the fallback's CONTENT at 506x343 — three real
     grid rows (167.8 / 68.2 / 54.4) of real copy — while the panel measured
     0px. So this was never a height problem at all: `.sh-static-fallback` is
     `position: absolute`, and an absolutely-positioned box is out of flow, so
     it contributes NOTHING to its parent's height no matter how tall it is.
     `height: auto` cannot fix that; only `position: relative` can.

     Worth stating plainly because I nearly shipped a `min-height` here, which
     would have left the panel at 0 and the fallback invisible. The tell was
     the two numbers disagreeing: content 506, parent 0. A min-height on the
     child is the wrong instrument for a child that is not in the layout. */
  .act[data-template='shelf'] .panel .sh-static-fallback,
  .act[data-template='shelf'] .sh-static-fallback {
    position: relative;
    inset: auto;
    height: auto;
    min-height: 0;
  }
  /* its rows are sized in vh for a fixed viewport; let them size to content */
  .act[data-template='shelf'] .panel .sh-static-fallback {
    grid-template-rows: auto auto auto;
    align-content: start;
  }
  .act[data-template='shelf'] .sh-fallback__header,
  .act[data-template='shelf'] .sh-fallback__grid,
  .act[data-template='shelf'] .sh-fallback__footer {
    min-height: 0;
    height: auto;
  }

  /* --- kono, same principle ---------------------------------------------
     journey.css's own desktop fix at 2846-2867 turned each kono section into
     an absolutely-positioned stage filling a `height: 100vh` panel:
     `section.ean/.nmc/.nwv { height: 100% }` and `.nmc .pin { position:
     absolute; inset: 0; height: 100% }`. Both are correct on a desktop and
     both compute to ZERO on a phone, where `.panel` is `height: auto` and
     there is no fixed viewport to fill. A sticky child does not grow its
     parent, and neither does an absolutely-positioned one.

     Placed here for the same reason as the Shelf above, not up in §8: these
     must be able to beat the rules at 2846-2867, and only source order
     settles that. */
  .act[data-template='kono'] .panel .kono-root > section.ean,
  .act[data-template='kono'] .panel .kono-root > section.nmc,
  .act[data-template='kono'] .panel .kono-root > section.nwv {
    height: auto;
    min-height: 68vh;
  }
  .act[data-template='kono'] .panel .nmc .pin {
    position: relative;
    inset: auto;
    height: auto;
    min-height: 60vh;
  }
  /* the starburst is a tall scroll-scrubbed sequence, so it needs more than
     one screen of run to play out */
  .act[data-template='kono'] .panel .kono-root > section.nmc { min-height: 120vh; }
  .act[data-template='kono'] .panel .nmc .star { max-width: 96vw; max-height: 96vw; }
  .act[data-template='kono'] .kono-root.m-fit { min-height: 68vh; }
}

/* ══════════════════════════════════════════════════════════════════════════
   §11  DUAL CALL TO ACTION  (FP-CTA-01, 2026-09-28)

   The meeting: every act must end by offering BOTH "select that template for
   their own website" AND "get additional information, which takes them off the
   homepage to the page designed for those specific services". journey.js
   appends .act__cta to the last panel of each act; these are its styles.

   WHY IT IS OVERLAYED AND NOT INLINE
   ----------------------------------
   `.panel > * { width: 100% }` and `.panel { display:flex; align-items:center }`
   mean any element appended to a panel becomes a flex sibling of the template's
   own root and is stretched to full width. Left in flow it would either squash
   the template's layout or stack a second full-width block under it, and the
   four templates do not agree on how tall their roots are (sylva 100%, rej
   auto, kage scene, nama m-fit). So the CTA is taken out of flow and pinned to
   the bottom of the panel, where it reads as a footer strip belonging to the
   act rather than another content block competing with it.

   IT MUST BE LEGIBLE ON FOUR DIFFERENT BACKGROUNDS
   -------------------------------------------------
   Act 01 sylva is a dark moss scene, act 02 rej is black-and-white photography,
   act 03 kage is full-bleed nature imagery, act 04 nama is near-black #1c1c1c.
   A transparent CTA would be invisible on two of them. So it carries its own
   dark plate with a blur backdrop and a hairline top border, which reads on a
   light photo and on a dark scene alike.

   POINTER EVENTS: the plate is `pointer-events:none` with `auto` restored on
   the two links, because the panel underneath is interactive (hover states,
   video tiles, drag). An invisible full-width capture layer over act 04's
   playground grid would kill the very interaction the tiles exist to show.
   ══════════════════════════════════════════════════════════════════════════ */
.act__cta {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 6;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 18px var(--gut) calc(18px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(to top,
    rgba(8, 10, 14, .88) 0%,
    rgba(8, 10, 14, .74) 62%,
    rgba(8, 10, 14, 0) 100%);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border-top: 1px solid rgba(255, 255, 255, .10);
  pointer-events: none;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .6s ease, transform .6s ease;
}
/* the plate only exists on the leg that carries it, and only once that leg is
   the one on screen — a CTA floating over leg 01 would be a lie about where
   the act ends */
.panel.is-live .act__cta { opacity: 1; transform: none; }

/* FP-CTA-02: the leg that carries the plate reserves room for it, so the
   plate never sits on top of the act's last line. The reservation matches the
   plate's own height per breakpoint — 92px on desktop (one row of two
   side-by-side doors, 80px tall plus slack) and 152px on a phone (two stacked
   doors, 125px tall plus slack).

   Getting this wrong is not cosmetic. A first attempt applied the 152px
   mobile reservation at EVERY width, which on desktop pushed the plate 14px
   past the bottom of three of the four panels (measured: -14.0 on rej, kage
   and nama) — the phone passed and the desktop failed, which is the reverse
   of the usual pattern and is why the reservation is tied to the same
   breakpoint as the plate's own layout. */
.panel--cta { padding-bottom: 92px; }

.act__cta a {
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 13px 22px;
  border-radius: 999px;
  font: 600 15px/1 var(--sans, system-ui, sans-serif);
  letter-spacing: .01em;
  text-decoration: none;
  white-space: nowrap;
  transition: transform .25s ease, background .25s ease, border-color .25s ease;
}
.act__cta a i { font-style: normal; transition: transform .25s ease; }
.act__cta a:hover { transform: translateY(-2px); }
.act__cta a:hover i { transform: translateX(3px); }

/* door 1 — buy the service. The act's own swatch, so each act's primary
   action is the colour that act is identified by in the rail.

   FP-CTA-04 (2026-09-28) — --cta-ink is SET BY journey.js from the swatch's own
   luminance, not hardcoded here. The four swatches do not agree on an answer and
   the vermilion is the awkward one: #e0231c measures 4.22:1 with dark text and
   4.38:1 with the plate's #f4f6f8, so both of the obvious choices fail AA. Only
   pure white clears it, at 4.74:1. The previous hardcoded pair (dark, with a
   one-off override for nama) was wrong for kage and would have been wrong again
   for the next act added. Computing it means no act can ship an unreadable
   button. Full derivation and the four measured pairs are at the FP-CTA-04 note
   in journey.js.

   FP-CTA-03 — SPECIFICITY IS LOAD-BEARING HERE
   -------------------------------------------
   These selectors are `.act .act__cta a.act__cta-*` and not the obvious
   `.act__cta-buy`, and that is not style. Every act stylesheet is re-rooted by
   build/scope_act.py and each one blanket-resets its anchors:

     kage.css:47   .act[data-template='kage'] a { color: inherit; ... }
     xora.css:55   .act[data-template='xora'] a { ... }
     books.css:46  .act[data-template='books'] a { ... }
     ge.css:43     .act[data-template='ge'] a { ... }
     cal.css:32    .act[data-template='cal'] a { ... }

   That selector is specificity (0,2,1) — one class, one attribute, one type.
   A plain `.act__cta-buy` is (0,1,0). So kage's reset WON, the CTA threw away
   its own colour, and both doors inherited `--bone` (#dfe7e0) from
   `.act[data-template='kage'] { color: var(--bone) }`. Measured result: pale
   sage text on vermilion, which fails AA and looked entirely correct in the
   source. `.act .act__cta a.act__cta-buy` is (0,3,1) and wins against any
   (0,2,1) blanket reset, in every act, including the five currently only
   reinstated.

   The trade is honest: these rules are now specific enough that a future
   `!important` in a template could still beat them. The alternative — stripping
   the resets out of five generated files — means editing files whose header
   says "do not hand-edit", so the reset is out-weighed at the point of use. */
.act .act__cta a.act__cta-buy {
  background: var(--cta, #2ef2ff);
  color: var(--cta-ink, #06080c);
  box-shadow: 0 6px 24px -8px var(--cta, #2ef2ff);
}

/* door 2 — view the template. Deliberately quieter: it is the comparison path,
   not the commitment, and the meeting wants the sale to be the easy one.
   #eef1f5 on the plate's composited backdrop measures ~15:1. */
.act .act__cta a.act__cta-tpl {
  background: rgba(255, 255, 255, .06);
  border: 1px solid rgba(255, 255, 255, .26);
  color: #eef1f5;
}
.act .act__cta a.act__cta-tpl:hover { background: rgba(255, 255, 255, .12); }

.act__cta a:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
}

/* On a phone the two doors stack; side by side they would each be too narrow
   to read and would wrap mid-label. */
@media (max-width: 900px) {
  .act__cta { flex-direction: column; gap: 10px; padding: 14px 18px calc(14px + env(safe-area-inset-bottom, 0px)); }
  .act__cta a { width: 100%; justify-content: center; font-size: 14px; padding: 14px 18px; }

  /* FP-CTA-02 (2026-09-28): the mobile CTA was `position: relative`, on the
     reasoning that a mobile panel is height:auto so "bottom of the panel" is
     no longer the viewport bottom and the plate should sit under the content
     instead of over it.

     That was wrong, and it is the same class of error as the panel-height
     collapse: it reasoned about the panel's height without checking the
     panel's AXIS. `.panel` is `display: flex` with the default row direction,
     so any in-flow child becomes a SIBLING BESIDE the template's root, not a
     block beneath it. Dropping absolute positioning therefore did not stack
     the CTA -- it made it a second flex item that competed for the same row.

     Measured on a 390px phone (work/diag-cta-mobile.mjs), every act was
     damaged: sylva's content 163px wide (was 343), rej's 176px, kage's 218px,
     and nama's root was crushed to 169px inside a 343px panel. The CTA itself
     took the remainder and measured 264-328px, sitting beside the act rather
     than under it. Four acts, one line.

     The fix is to keep the CTA out of flow in BOTH layouts and let the mobile
     panel grow to fit it: the plate stays absolute and the panel reserves
     room for it with padding-bottom instead. `.panel--cta` is set by
     journey.js on the leg that carries the plate. */
  .panel--cta { padding-bottom: 152px; }
  .act__cta { gap: 10px; }
}

@media (prefers-reduced-motion: reduce) {
  .act__cta { opacity: 1; transform: none; transition: none; }
  .act__cta a, .act__cta a i { transition: none; }
  .act__cta a:hover { transform: none; }
  .act__cta a:hover i { transform: none; }
}
