/* Forked Page home film. Storyboard, timing and capture sources: Website/home/README.md section 02.
   Ported from the trit site's home.css: the scroll skeleton (view timelines, animation ranges, the scrub contract,
   the fallback delay formula, steps and snapping, the end card timings and the keyframes) keeps
   trit's values; only the scene ids, the hero (a static section here, see site.css) and the light palette differ.

   Behaviour layers:
   1. Base styles are the composed final frame of every scene: what visitors see with
      prefers-reduced-motion, without JavaScript, and when no scroll timeline is available.
   2. html.film runs the scroll film: tall scenes with sticky stages, and every [data-k] element
      scrubs its keyframes between --from and --to, in percent of its scene's view timeline.
      Native: CSS scroll-driven animations. Fallback (html.film-fallback, set by home.js): the same
      keyframes, paused, scrubbed through animation-delay by --p (0 to 100) per scene.
   3. Steps (html.film with native scroll timelines only): the root scroller snaps (y mandatory) to 1px
      .beat anchors at the resting frames (hero at rest, every hold of the film scenes, every finished title,
      the page end), each with scroll-snap-stop: always.
   The app captures are real simulator screenshots; motion moves and crossfades them, and the spotlight is an
   overlay on the device frame: no capture pixel is ever altered. The film scenes (#reading, #endings) and their
   beats are generated by Tools/build-film.py from Tools/film-story.json between the @gen markers. */

@property --from { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --to { syntax: "<number>"; inherits: false; initial-value: 100; }
@property --p { syntax: "<number>"; inherits: true; initial-value: 0; }

.home-page { --vignette: rgba(255, 255, 255, .9); --gutter: clamp(24px, 4vw, 72px); overflow-x: clip; }

/* ---------------------------------------------------------------- stage */
.scene__stage {
  --stage-max: 1440px;
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(24px, 5vw, 80px);
  min-height: 100svh;
  padding: clamp(72px, 10vh, 112px) calc(var(--gutter) + max(0px, (100% - var(--stage-max)) / 2));
  overflow: clip;
  /* Opaque paper under the glow: the stage is sticky at the viewport top, and iOS Safari can tint its bars from
     edge-touching fixed/sticky boxes (README section 02). */
  background: radial-gradient(42% 40% at 50% 52%, var(--vignette), transparent 70%) var(--paper);
}
.scene__title {
  margin: 0;
  color: var(--ink);
  font-family: var(--serif);
  font-weight: 400;
  letter-spacing: .06em;
  line-height: 1.5;
  text-wrap: balance;
}
.scene__title { position: relative; z-index: 2; font-size: clamp(28px, 2.6vw, 40px); }
/* Each clause of a title is one set line. */
.scene__title span { display: block; white-space: nowrap; }
.scene__visual { position: relative; z-index: 1; display: flex; justify-content: center; grid-column: 2; grid-row: 1; }

/* Reading: title on the left of the centred device. Endings: title on the right. */
.scene--reading .scene__title { grid-column: 1; grid-row: 1; justify-self: end; }
.scene--endings .scene__title { grid-column: 3; grid-row: 1; }

/* ---------------------------------------------------------------- device: real captures */
/* A device frame holds a scene's captures as stacked shots (390 / --h aspect, --h the tallest crop in points,
   iPhone 13 class at 390 pt). Without the film only .shot--final shows; the others stay display: none, so their
   lazy images are never requested. Per-scene values are CSS rules in the generated frames block below. */
.device {
  --h: 797;
  --device-w: min(340px, calc((100svh - 190px) * 390 / var(--h)));
  position: relative;
  width: var(--device-w);
  aspect-ratio: 390 / var(--h);
  margin: 0;
  overflow: hidden;
  border-radius: 5.5% 5.5% 14% 14% / 2.9% 2.9% 7.5% 7.5%;
  background: #FFFFFF;
  box-shadow: 0 0 0 1px rgba(26, 24, 21, .1), 0 32px 64px -24px rgba(26, 24, 21, .32);
}
.shot { position: absolute; inset: 0; display: block; background: var(--bg, var(--paper)); }
.shot img { display: block; width: 100%; height: 100%; object-fit: contain; object-position: 50% 100%; }
.shot:not(.shot--final), .spot { display: none; }
/* Spotlight: dims everything outside one horizontal band (--y0 to --y1, % of the frame height). */
.spot {
  --spot: rgba(26, 24, 21, .58);
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(to bottom, var(--spot) calc(var(--y0) - 3%), transparent var(--y0),
    transparent var(--y1), var(--spot) calc(var(--y1) + 3%));
}
.wipe { position: absolute; inset: 0; overflow: hidden; box-shadow: 0 14px 28px rgba(26, 24, 21, .2); }
.spot--x { background: linear-gradient(to right, var(--spot) calc(var(--x0) - 3%), transparent var(--x0),
    transparent var(--x1), var(--spot) calc(var(--x1) + 3%)); }
.camera { position: relative; }
/* Step anchors: inert outside the film (the static frames scroll freely). */
.beat { display: none; }
/* Scene frames: device height, shot backgrounds and spotlight bands (generated from Tools/film-story.json). */
/* @gen:frames:start */
.scene--reading .device { --h: 799; }
.shot--reading-narration { --bg: #464443; }
.shot--reading-dialogue { --bg: #4B4947; }
.shot--reading-choice { --bg: #474644; }
.scene--reading .spot--1 { --y0: 40.5%; --y1: 93%; }
.scene--reading .spot--2 { --y0: 40.5%; --y1: 74%; }
.scene--reading .spot--3 { --y0: 70%; --y1: 94%; }
.scene--endings .device { --h: 799; }
.shot--ending { --bg: #595654; }
.shot--records { --bg: #16181D; }
.scene--endings .spot--1 { --y0: 7.5%; --y1: 37%; }
.scene--endings .spot--2 { --y0: 75.5%; --y1: 93.5%; }
.scene--endings .spot--3 { --y0: 42%; --y1: 81%; }
/* @gen:frames:end */

/* ---------------------------------------------------------------- end card */
/* One horizontal lockup: the app icon on the left; 「岔頁」 over the store badges (and 「即將推出」 beside them,
   the hero's .store-badges markup and rules) on the right. The section fills the last viewport above the footer.
   --footer-h mirrors .site-footer in site.css: desktop 16 + 44 + 16 padding/target + 1 border; phones (max-width
   640px, column) 12 + 11 * 1.6 + 4 + 44 + 12 + 1, plus the bottom safe area on both. The icon spans the name line
   box, the gap and the badge row. */
.endcard {
  --footer-h: calc(77px + env(safe-area-inset-bottom, 0px));
  --badge-h: 48px;
  --name-size: clamp(44px, 4.4vw, 60px);
  --name-gap: clamp(14px, 1.4vw, 18px);
  --lockup-gap: clamp(18px, 2.2vw, 28px);
  display: grid;
  place-content: center;
  min-height: calc(100vh - var(--footer-h));
  min-height: calc(100svh - var(--footer-h));
  padding: 40px var(--gutter);
  background: radial-gradient(44% 38% at 50% 46%, var(--vignette), transparent 70%) var(--paper);
}
.lockup {
  display: grid;
  grid-template-columns: auto auto;
  grid-template-areas: "mark name" "mark store";
  column-gap: var(--lockup-gap);
  align-items: start;
  justify-items: start;
}
.lockup__mark {
  grid-area: mark;
  width: calc(var(--name-size) + var(--name-gap) + var(--badge-h));
  height: auto;
  border-radius: 22.5%;
  box-shadow: 0 0 0 1px rgba(26, 24, 21, .08), 0 16px 32px -16px rgba(26, 24, 21, .3);
}
.lockup__title { grid-area: name; margin-bottom: var(--name-gap); }
.lockup__name { margin: 0; color: var(--ink); font: 600 var(--name-size)/1 var(--serif); letter-spacing: .06em; }
.lockup__store { grid-area: store; }
.lockup .store-badges__row { --store-badge-height: var(--badge-h); }
@media (max-width: 640px) {
  .endcard { --footer-h: calc(90.6px + env(safe-area-inset-bottom, 0px)); --badge-h: 40px; --name-size: clamp(36px, 10vw, 44px); }
  /* Phones keep the icon on the left (owner decision): a smaller icon, and in the right column the badges stack
     like the hero's with 「即將推出」 below them, so the lockup fits 320px (24 + 80 + 16 + 135 + 24 = 279px). */
  .lockup { --lockup-gap: 16px; }
  .lockup__mark { width: clamp(72px, 22vw, 88px); }
  .lockup .store-badges { grid-template-columns: max-content; }
}

/* ---------------------------------------------------------------- phones (portrait) */
@media (max-width: 899px) and (orientation: portrait), (max-width: 599px) {
  .scene__stage { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); align-content: start; justify-items: center; gap: 24px; padding: 76px 16px 28px; }
  .scene__visual { grid-column: 1; grid-row: 2; }
  .scene__title, .scene--reading .scene__title, .scene--endings .scene__title { grid-column: 1; grid-row: 1; justify-self: center; max-width: 14em; font-size: clamp(24px, 6.4vw, 32px); text-align: center; }
  .device { --device-w: min(78vw, calc((100svh - 210px) * 390 / var(--h))); }
  /* The endings title fits one line on phones (10 characters, about 254px at 320px wide); in the desktop side
     column it keeps two lines. */
  .scene--endings .scene__title span { display: inline; }
}
@media (orientation: landscape) and (max-height: 560px) {
  .scene__stage { padding-block: 56px 16px; }
  .device { --device-w: calc((100svh - 80px) * 390 / var(--h)); }
}

/* ================================================================= motion */
@media (prefers-reduced-motion: no-preference) {
  /* Film layout: pinned scenes and their scroll lengths. The hero is not pinned here (static, site.css). */
  .film .scene--film { height: var(--len); }
  .film .scene--film > .scene__stage { position: sticky; top: 0; height: 100svh; min-height: 0; }
  .film [data-range="contain"] { --rn: contain; }

  /* iOS Safari bottom bar (README section 02). The pinned stage is 100svh tall; once a swipe collapses the toolbar
     the viewport grows towards 100lvh and the stage's bottom edge sits just above the obscured bottom inset, so
     Safari extends a tint below this sticky box sampled from its bottom edge. On phones the camera's close-ups fill
     the stage edge to edge, so that edge was a dark capture. On phone layouts and touch screens a solid paper floor
     (the stage's bottom padding, above the device) keeps the stage's bottom edge paper, and a fixed paper strip
     pinned to the visible viewport's bottom edge keeps that edge paper while captures scroll past it. Desktop
     (fine pointer, wide layout) is unchanged. */
  @media (max-width: 899px) and (orientation: portrait), (max-width: 599px), (pointer: coarse) {
    .film .scene--film > .scene__stage::after {
      content: "";
      position: absolute;
      right: 0;
      bottom: 0;
      left: 0;
      z-index: 3;
      height: var(--stage-floor, 28px);
      background: var(--paper);
      pointer-events: none;
    }
  }
  @media (orientation: landscape) and (max-height: 560px) {
    .film .scene--film > .scene__stage { --stage-floor: 16px; } /* the landscape stage's bottom padding */
  }
  @media (hover: none) and (pointer: coarse) {
    .film body::after {
      content: "";
      position: fixed;
      right: 0;
      bottom: 0;
      left: 0;
      z-index: 10; /* above the stages (device 1, title 2, floor 3), below the header and its menu overlay (20) */
      height: calc(12px + env(safe-area-inset-bottom, 0px));
      background: var(--paper);
      pointer-events: none;
    }
  }

  /* Steps (owner decision, 2026-10-06: one gesture, one step). The root scroller snaps with y mandatory, so
     the page only ever rests on a step, and every snap position has scroll-snap-stop: always, so no gesture,
     however fast, passes a step (CSS Scroll Snap 1, sections 4.1.2 and 5.3; README section 02). The snap
     positions are the 1px .beat anchors (start-aligned), the page top (the hero's anchor) and the page
     end (the footer, end-aligned). No other box on the page is a snap area: a snap area taller than the
     scrollport would make every position inside it a valid rest (section 5.2.2), and the page could stop
     between steps. Each film and Private anchor's top is the offset inside its section where the scene's
     cover timeline reaches --at percent: scroll - section top = at% * (section + scrollport) - scrollport, the
     same mapping as animation-range. The scrollport is 100dvh: on iOS Safari with the toolbar shown it is
     shorter than 100vh (549 against 667px on the iPhone SE simulator), and the view timeline follows the real
     scrollport; 100vh is the fallback where dvh is unknown. Only the native film snaps: the JavaScript
     fallback (html.film-fallback, browsers without scroll timelines), reduced motion and no JavaScript scroll
     freely. */
  html.film:not(.film-fallback) { scroll-snap-type: y mandatory; }
  .film :is(.story-intro, .scene--film) { position: relative; }
  .film .beat {
    position: absolute;
    top: calc(var(--at) * (var(--len) + 100vh) / 100 - 100vh);
    top: calc(var(--at) * (var(--len) + 100dvh) / 100 - 100dvh);
    left: 0;
    display: block;
    width: 1px;
    height: 1px; /* not 0: WebKit ignores an empty snap area (measured on iOS Safari 26.5, README section 03) */
    pointer-events: none;
    scroll-snap-align: start;
    scroll-snap-stop: always;
  }
  .film .story-intro .beat { top: 0; } /* hero at rest: the page top */
  .film .site-footer { scroll-snap-align: end; scroll-snap-stop: always; } /* the page end: end card and footer */

  /* Scrub contract for every [data-k] element. */
  .film [data-k] {
    animation-duration: 1ms;
    animation-duration: auto;
    animation-fill-mode: both;
    animation-timing-function: linear;
    animation-range: var(--rn, cover) calc(var(--from) * 1%) var(--rn, cover) calc(var(--to) * 1%);
  }
  .film #reading { view-timeline: --s-reading block; }
  .film #endings { view-timeline: --s-endings block; }
  .film #download { view-timeline: --s-download block; }
  .film #reading [data-k] { animation-timeline: --s-reading; }
  .film #endings [data-k] { animation-timeline: --s-endings; }
  .film #download [data-k] { animation-timeline: --s-download; }
  .film-fallback [data-k] {
    animation-duration: 1s;
    animation-play-state: paused;
    animation-delay: calc(clamp(0, (var(--p) - var(--from)) / max(.001, var(--to) - var(--from)), 1) * -1s);
  }


  /* Film scenes: every shot and spotlight takes part; beats come from the generated block below. */
  .film .scene--film :is(.shot, .spot) { display: block; }
  /* @gen:motion:start */
  /* 02 Reading (cover, 360vh: entry 0-21.74, pinned 21.74-78.26, exit 78.26-100).
     Authored for 360vh (pinned 21.74-78.26); every percent below is remapped to 360vh.
     Authored beats: close-up on the narration card 0-30 (spotlight 12-30) | to 吳文德's name tag and lines 30-36 while they crossfade in, hold 36-44 (spotlight 36-44) | to the three choices 44-50 while they crossfade in, hold 50-58 (spotlight 50-58) | pull back 58-66 | title 64-71
     Steps (authored): 25, 40, 54, 76; built: 25, 40, 54, 76. */
  .film .scene--reading { --len: 360vh; }
  .film .scene--reading .camera { --from: 0; --to: 66; animation-name: reading-camera; }
  @media (max-width: 899px) and (orientation: portrait), (max-width: 599px) { .film .scene--reading .camera { animation-name: reading-camera-compact; } }
  .film .scene--reading .shot:nth-of-type(2) { --from: 30; --to: 36; animation-name: shot-in; }
  .film .scene--reading .shot:nth-of-type(3) { --from: 44; --to: 50; animation-name: shot-in; }
  .film .scene--reading .spot--1 { --from: 12; --to: 30; animation-name: reading-spot-1; }
  .film .scene--reading .spot--2 { --from: 36; --to: 44; animation-name: reading-spot-2; }
  .film .scene--reading .spot--3 { --from: 50; --to: 58; animation-name: reading-spot-3; }
  .film .scene--reading .scene__title { --from: 64; --to: 71; animation-name: rise-in; }
  .film .scene--reading .beat--1 { --at: 25; } /* step: narration close-up, spotlight on the text (authored 25) */
  .film .scene--reading .beat--2 { --at: 40; } /* step: 吳文德's line, spotlight on (authored 40) */
  .film .scene--reading .beat--3 { --at: 54; } /* step: three choices, spotlight on (authored 54) */
  .film .scene--reading .beat--4 { --at: 76; } /* step: title shown, whole device (authored 76) */

  /* 03 Endings (cover, 360vh: entry 0-21.74, pinned 21.74-78.26, exit 78.26-100).
     Authored for 360vh (pinned 21.74-78.26); every percent below is remapped to 360vh.
     Authored beats: close-up on the ending title and text 0-30 (spotlight 12-30) | to the 68% exploration card and the replay button 30-36, hold 36-44 (spotlight 36-44) | to the reading records 44-50 while they crossfade in, hold 50-58 (spotlight 50-58 on records 4-7, three of them completed) | pull back 58-66 | title 64-71
     Steps (authored): 25, 40, 54, 76; built: 25, 40, 54, 76. */
  .film .scene--endings { --len: 360vh; }
  .film .scene--endings .camera { --from: 0; --to: 66; animation-name: endings-camera; }
  @media (max-width: 899px) and (orientation: portrait), (max-width: 599px) { .film .scene--endings .camera { animation-name: endings-camera-compact; } }
  .film .scene--endings .shot:nth-of-type(2) { --from: 44; --to: 50; animation-name: shot-in; }
  .film .scene--endings .spot--1 { --from: 12; --to: 30; animation-name: endings-spot-1; }
  .film .scene--endings .spot--2 { --from: 36; --to: 44; animation-name: endings-spot-2; }
  .film .scene--endings .spot--3 { --from: 50; --to: 58; animation-name: endings-spot-3; }
  .film .scene--endings .scene__title { --from: 64; --to: 71; animation-name: rise-in; }
  .film .scene--endings .beat--1 { --at: 25; } /* step: ending title and text, spotlight on (authored 25) */
  .film .scene--endings .beat--2 { --at: 40; } /* step: 68% exploration card and replay button, spotlight on (authored 40) */
  .film .scene--endings .beat--3 { --at: 54; } /* step: reading records, spotlight on records 4-7 (three completed) (authored 54) */
  .film .scene--endings .beat--4 { --at: 76; } /* step: title shown, whole device (authored 76) */
  /* @gen:motion:end */


  /* 04 End card (normal flow; every beat ends before the page end can stop the timeline). */
  .film .lockup__mark { --from: 6; --to: 30; animation-name: rise-in; }
  .film .lockup__title { --from: 12; --to: 32; animation-name: rise-in; }
  .film .lockup__store { --from: 16; --to: 34; animation-name: rise-in; }
}


/* ---------------------------------------------------------------- keyframes */
@keyframes rise-in { 0% { opacity: 0; transform: translateY(24px); animation-timing-function: cubic-bezier(.16, 1, .3, 1); } 100% { opacity: 1; transform: none; } }
/* @gen:keyframes:start */
@keyframes shot-in { 0% { opacity: 0; animation-timing-function: cubic-bezier(.33, 0, .67, 1); } 100% { opacity: 1; } }
@keyframes wipe-window { 0% { transform: translateY(-100%); animation-timing-function: cubic-bezier(.45, 0, .55, 1); } 100% { transform: none; } }
@keyframes wipe-content { 0% { transform: translateY(100%); animation-timing-function: cubic-bezier(.45, 0, .55, 1); } 100% { transform: none; } }
@keyframes reading-camera {
  0% { transform: translate(0%, -25.6%) scale(1.6); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  45.45% { transform: translate(0%, -25.6%) scale(1.6); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  54.55% { transform: translate(0%, -8%) scale(1.6); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  66.67% { transform: translate(0%, -8%) scale(1.6); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  75.76% { transform: translate(0%, -39%) scale(1.5); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  87.88% { transform: translate(0%, -39%) scale(1.5); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  100% { transform: none; }
}
@keyframes reading-camera-compact {
  0% { transform: translate(0%, -20.48%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  45.45% { transform: translate(0%, -20.48%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  54.55% { transform: translate(0%, -6.4%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  66.67% { transform: translate(0%, -6.4%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  75.76% { transform: translate(0%, -33.28%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  87.88% { transform: translate(0%, -33.28%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  100% { transform: none; }
}
@keyframes reading-spot-1 { 0% { opacity: 0; } 33.33% { opacity: 1; } 54.11% { opacity: 1; } 77.78% { opacity: 1; } 100% { opacity: 0; } }
@keyframes reading-spot-2 { 0% { opacity: 0; } 25% { opacity: 1; } 75% { opacity: 1; } 100% { opacity: 0; } }
@keyframes reading-spot-3 { 0% { opacity: 0; } 25% { opacity: 1; } 75% { opacity: 1; } 100% { opacity: 0; } }
@keyframes endings-camera {
  0% { transform: translate(0%, 30%) scale(1.5); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  45.45% { transform: translate(0%, 30%) scale(1.5); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  54.55% { transform: translate(0%, -42%) scale(1.5); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  66.67% { transform: translate(0%, -42%) scale(1.5); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  75.76% { transform: translate(0%, -15%) scale(1.5); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  87.88% { transform: translate(0%, -15%) scale(1.5); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  100% { transform: none; }
}
@keyframes endings-camera-compact {
  0% { transform: translate(0%, 25.6%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  45.45% { transform: translate(0%, 25.6%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  54.55% { transform: translate(0%, -35.84%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  66.67% { transform: translate(0%, -35.84%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  75.76% { transform: translate(0%, -12.8%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  87.88% { transform: translate(0%, -12.8%) scale(1.28); animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  100% { transform: none; }
}
@keyframes endings-spot-1 { 0% { opacity: 0; } 33.33% { opacity: 1; } 54.11% { opacity: 1; } 77.78% { opacity: 1; } 100% { opacity: 0; } }
@keyframes endings-spot-2 { 0% { opacity: 0; } 25% { opacity: 1; } 75% { opacity: 1; } 100% { opacity: 0; } }
@keyframes endings-spot-3 { 0% { opacity: 0; } 25% { opacity: 1; } 75% { opacity: 1; } 100% { opacity: 0; } }
/* @gen:keyframes:end */
