/* Trit home page. Storyboard, timing and capture sources: Website/home/README.md section 02.

   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.intro-on (inline head script, motion allowed) fades the intact logo in on load.
   3. 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.
   Every beat of a scene reads one timeline, so a later beat can never start before an earlier one
   ends, at any viewport size or scroll speed. Only transform and opacity animate.
   The app captures are real simulator screenshots; motion moves, crossfades and wipes them, and the
   spotlight and wipe shadow are overlays on the device frame: no capture pixel is ever altered.
   The three film scenes (#review, #fix, #undo) and their beats are generated by Tools/build-film.py
   from Tools/film-story.json between the @gen markers.
   4. Steps (html.film with native scroll timelines only): the page scrolls one step at a time. 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 private statement, the page end), and every anchor
      has scroll-snap-stop: always, so a gesture of any speed ends on the next step and never passes one.
      README section 02, "Steps".
   Scroll lengths: every pinned scene is about 30% shorter in its pinned phase than the length its timings
   were authored for, and its percentages are remapped so each frame keeps its place in the pinned phase
   (film scenes: Tools/build-film.py; hero and private: the rules below). README section 02. */

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

.home-page { --vignette: rgba(39, 71, 214, .14); --mark-glow: rgba(47, 108, 255, .38); overflow-x: clip; }
.site-header--overlay { position: absolute; inset: 0 0 auto; }

/* ---------------------------------------------------------------- stage */
.scene__stage {
  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) var(--gutter);
  overflow: clip;
  background: radial-gradient(42% 40% at 50% 52%, var(--vignette), transparent 70%);
}
.scene__title, .statement {
  margin: 0;
  font-weight: 700;
  letter-spacing: -.035em;
  line-height: 1.05;
  text-wrap: balance;
}
.scene__title { position: relative; z-index: 2; max-width: 11em; font-size: clamp(2rem, 3.6vw, 3.4rem); }
.scene__visual { position: relative; z-index: 1; display: flex; justify-content: center; grid-column: 2; grid-row: 1; }

/* Review and Undo: title on the left of the centred device. Fix: title on the right. */
.scene--review .scene__title, .scene--undo .scene__title { grid-column: 1; grid-row: 1; }
.scene--fix .scene__title { grid-column: 3; grid-row: 1; }

/* ---------------------------------------------------------------- hero */
.hero__stage { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); padding-top: 88px; }
.hero__copy { position: relative; z-index: 2; }
/* Two set lines: each fits its column at these sizes (about 9.6em for "Nothing in between."). */
.hero h1 { margin: 0 0 32px; font-size: clamp(2.4rem, 4vw, 4rem); font-weight: 750; letter-spacing: -.045em; line-height: 1.02; }
.hero__line { display: block; white-space: nowrap; }
.hero__line--quiet { color: var(--text-secondary); }
.mark { position: relative; justify-self: center; width: min(42vw, 64svh, 620px); aspect-ratio: 1; }
.mark__glow { position: absolute; inset: -24%; background: radial-gradient(closest-side, var(--mark-glow), transparent 72%); pointer-events: none; }
.mark__logo { position: relative; width: 100%; height: 100%; }
.mark__still, .mark__video { display: block; width: 100%; height: 100%; object-fit: contain; }
.mark__video { position: absolute; inset: 0; opacity: 0; pointer-events: none;
  -webkit-mask-image: url("assets/hero-icon-mask-e0374cc36a.svg?v=e0374cc36a"); mask-image: url("assets/hero-icon-mask-e0374cc36a.svg?v=e0374cc36a");
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; }
.mark__logo[data-video-state="playing"] .mark__video { opacity: 1; }
.mark__logo[data-video-state="playing"] .mark__still { opacity: 0; }

/* ---------------------------------------------------------------- device: real captures */
/* A device frame holds a scene's captures as stacked shots. Every capture is a real simulator screenshot
   with its status and navigation bars cropped; --h is the tallest crop of the scene in points (390 pt wide, iPhone 13),
   so the frame's aspect is 390 / --h. A shorter crop sits at the bottom on its own top-row colour
   (--bg, sampled from the capture), never stretched. Without the film only the shot marked
   .shot--final shows; the others stay display: none, so their lazy images are never requested.
   Per-scene and per-shot values (--h, --bg, spotlight bands) are CSS rules in the generated frames block
   below, not style attributes, so the Content-Security-Policy needs no inline-style allowance. */
.device {
  --h: 737;
  --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: var(--surface);
  box-shadow: 0 0 0 1px var(--separator), var(--shadow);
}
.shot { position: absolute; inset: 0; display: block; background: var(--bg, var(--canvas)); }
.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). An overlay
   on the frame; it never paints into the capture. Hidden at rest. */
.spot {
  --spot: rgba(4, 7, 14, .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: an overflow window slides down while its content slides up by the same amount, so the capture
   inside stays put and is revealed top to bottom (transform only). */
.wipe { position: absolute; inset: 0; overflow: hidden; box-shadow: 0 14px 28px rgba(2, 6, 18, .28); }
.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 */

/* @gen:frames:end */

/* ---------------------------------------------------------------- private */
.private { grid-template-columns: minmax(0, 1fr); align-content: center; justify-items: start; gap: clamp(40px, 7vh, 72px); max-width: 1180px; margin: 0 auto; background: none; }
.statement { max-width: 15ch; font-size: clamp(2.4rem, 5.4vw, 5rem); }
.statement span { display: inline-block; }
.facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 clamp(20px, 3vw, 40px); width: 100%; margin: 0; padding: 0; list-style: none; }
.facts li { padding-top: 16px; border-top: 1px solid var(--separator); color: var(--text-secondary); font-size: clamp(17px, 1.4vw, 19px); line-height: 1.4; }
/* Reading order of the statement words and the facts (--i), as rules rather than style attributes. */
.statement span:nth-child(2) { --i: 1; }
.statement span:nth-child(3) { --i: 2; }
.statement span:nth-child(4) { --i: 3; }
.statement span:nth-child(5) { --i: 4; }
.statement span:nth-child(6) { --i: 5; }
.statement span:nth-child(7) { --i: 6; }
.statement span:nth-child(8) { --i: 7; }
.statement span:nth-child(9) { --i: 8; }
.statement span:nth-child(10) { --i: 9; }
.facts li:nth-child(2) { --i: 1; }
.facts li:nth-child(3) { --i: 2; }

/* ---------------------------------------------------------------- end card */
/* One horizontal closing lockup: the app icon on the left; on the right "Trit" over the disabled badge.
   Two items hang to the right of that column without taking part in layout, so the centred group is
   exactly icon + gap + max(name, badge): "Coming soon" sits on the name's baseline as a zero-width
   flex item.
   The section fills the last viewport above the footer, so at the page end the lockup is centred
   between the viewport top and the footer's top edge.
   --footer-h mirrors .site-footer in site.css (px font size, so it does not scale with text):
   desktop 16 + 44 + 16 padding/target + 1 border; phones (max-width 760px, column) 12 + 13 * 1.55
   + 4 + 44 + 16 + 1, plus the bottom safe area on both.
   The icon spans the name and the badge: the name's line box is set to about its cap height
   (line-height .72), so the icon's top meets the top of "Trit" and its bottom meets the badge's
   bottom. --hang is how far "Coming soon" reaches past the badge; the group is centred, so the same
   room is kept on the left, and the icon shrinks (--mark-room) before "Coming soon" would enter the
   gutter. */
.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);
  --status-gap: 14px;
  --hang: 48px; /* "Coming soon" past the badge: 82 + 14 + 92 - 144 = 45px at 1440px, rounded up */
  --mark-room: calc(100vw - 2 * (var(--gutter) + var(--hang)) - var(--lockup-gap) - 3 * var(--badge-h));
  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%);
}
.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;
  --mark: calc(var(--name-size) * .72 + var(--name-gap) + var(--badge-h));
  width: min(var(--mark), var(--mark-room));
  height: auto;
}
.lockup__title { grid-area: name; display: flex; align-items: baseline; margin-bottom: var(--name-gap); }
.lockup__name {
  margin: 0;
  font-size: var(--name-size);
  font-weight: 700;
  letter-spacing: -.035em;
  line-height: .72;
}
/* Zero-width item on the name's baseline: it never widens the column, and the negative block margins
   keep its descent from growing the row (the name's cap-height box stays the row height). */
.lockup .lockup__status { position: relative; left: var(--status-gap); flex: none; width: 0; margin-block: -1em; line-height: 1; }
.lockup__store { grid-area: store; }
/* No padded hit area here: disabled, the badge is never a target; released, the link is the badge itself
   (48px tall, 44px on phones, both at or above the 44px target size). */
.lockup .store__badge { margin: 0; padding: 0; }
/* Released (no "Coming soon" in the lockup): nothing hangs past the badge, so the group is centred on
   icon + gap + badge alone. */
.endcard:not(:has(.lockup__status)) { --hang: 0px; }
.lockup .store__badge img { height: var(--badge-h); }
@media (max-width: 760px) {
  .endcard { --footer-h: calc(97.15px + env(safe-area-inset-bottom, 0px)); }
}

/* ---------------------------------------------------------------- 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--review .scene__title, .scene--undo .scene__title, .scene--fix .scene__title { grid-column: 1; grid-row: 1; max-width: 14em; font-size: clamp(1.9rem, 8.4vw, 2.5rem); text-align: center; }
  .device { --device-w: min(78vw, calc((100svh - 210px) * 390 / var(--h))); }
  .hero__stage { grid-template-columns: 1fr; grid-template-rows: auto auto; align-content: center; justify-items: start; gap: 28px; padding: 76px 16px 40px; }
  .hero .mark { order: -1; justify-self: center; width: min(76vw, 42svh); }
  /* Stacked under the centred logo, the copy block (as wide as its longest line) is centred on the
     page while its lines and the badge row stay left-aligned with each other. */
  .hero__copy { justify-self: center; }
  .hero h1 { margin-bottom: 24px; font-size: clamp(1.9rem, 9.2vw, 3.2rem); }
  .store__badge img { height: 44px; }
  /* Hero badge row: "Coming soon" hangs directly below the badge, its left edge on the badge artwork's left
     edge (the badge's 10px hit padding, offset by its -10px margin, is the space between them). It is taken
     out of flow, so the copy and the badge keep exactly the position they have without it (released, the row
     holds the badge link alone); the stage's bottom padding leaves room for it down to 320 x 700. */
  .hero .store { position: relative; }
  .hero .store__status { position: absolute; top: 100%; left: 0; }
  .endcard { --badge-h: 44px; --name-size: clamp(40px, 11vw, 48px); --lockup-gap: 16px; --status-gap: 12px; --hang: 30px; } /* "Coming soon" 30px past the badge with the 40px name (up to 364px wide); wider phones never reach --mark-room */
  .private { justify-items: start; gap: 36px; padding: 72px 20px 40px; }
  .statement { font-size: clamp(2.1rem, 10vw, 3rem); }
  .facts { grid-template-columns: 1fr; }
  .facts li { padding: 14px 0; }
  .private { grid-template-rows: auto auto; align-content: center; }
}
@media (orientation: landscape) and (max-height: 560px) {
  .scene__stage { padding-block: 56px 16px; }
  .device { --device-w: calc((100svh - 80px) * 390 / var(--h)); }
  .mark { width: min(36vw, 62svh); }
}

/* ================================================================= motion */
@media (prefers-reduced-motion: no-preference) {
  /* Load: the intact logo fades in. Copy is visible immediately (it is the LCP candidate). */
  .intro-on .mark__logo { animation: logo-in .7s cubic-bezier(.33, 1, .68, 1) both; }

  /* Film layout: pinned scenes and their scroll lengths. */
  .film .hero { --len: 148vh; }
  .film .scene--private { --len: 190vh; }
  .film :is(.hero, .scene--film, .scene--private) { height: var(--len); }
  .film :is(.hero, .scene--film, .scene--private) > .scene__stage { position: sticky; top: 0; height: 100svh; min-height: 0; }
  .film [data-range="contain"] { --rn: contain; }

  /* 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(.hero, .scene--film, .scene--private) { 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 .hero .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 #top { view-timeline: --s-top block; }
  .film #review { view-timeline: --s-review block; }
  .film #fix { view-timeline: --s-fix block; }
  .film #undo { view-timeline: --s-undo block; }
  .film #private { view-timeline: --s-private block; }
  .film #coming { view-timeline: --s-coming block; }
  .film #top [data-k] { animation-timeline: --s-top; }
  .film #review [data-k] { animation-timeline: --s-review; }
  .film #fix [data-k] { animation-timeline: --s-fix; }
  .film #undo [data-k] { animation-timeline: --s-undo; }
  .film #private [data-k] { animation-timeline: --s-private; }
  .film #coming [data-k] { animation-timeline: --s-coming; }
  .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);
  }

  /* 01 Title (contain range: the 48vh while the stage is pinned; authored for 170vh, 70vh pinned: the percentages
     are of the contain range, so they keep their place in the shorter pin unchanged). */
  .film .hero__copy { --from: 0; --to: 45; animation-name: fade-out-up; }
  .film .mark { --from: 0; --to: 70; animation-name: logo-out; }

  /* 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 */
  /* Hero only (every section below it is in hidden_sections, Tools/film-story.json): the hero rests in the first
     viewport with the footer below. No scroll length, pin, fade-out or step, and the page does not snap; the
     load-in logo fade (.intro-on) stays. home.js drops html.film once it runs; these rules cover the first paint. */
  .film .hero { height: auto; }
  .film .hero > .scene__stage { position: relative; height: auto; min-height: 100svh; }
  .film .hero__copy, .film .mark { animation: none; }
  .film .hero .beat { display: none; }
  html.film:not(.film-fallback) { scroll-snap-type: none; }
  .film .site-footer { scroll-snap-align: none; scroll-snap-stop: normal; }
  /* @gen:motion:end */

  /* 05 Private (cover, 190vh: entry 0-34.48, pinned 34.48-65.52, exit 65.52-100). Words read in, then the facts.
     Authored for 230vh (pinned 30.3-69.7): words 26 + 2.4i to 32 + 2.4i, facts 52 + 3i to 60 + 3i, step 67.85.
     Remapped as build-film.py remaps the film scenes: the 100vh entry keeps its distance (authored percent
     x 330/290), the pinned phase is scaled from 130vh to 90vh. The first two words start brightening during the
     entry, so a word's start is the smaller of the entry and the pinned mapping. */
  .film .statement span { --from: min(29.59 + var(--i) * 2.731, 31.09 + var(--i) * 1.891); --to: calc(35.82 + var(--i) * 1.891); animation-name: word-in; }
  .film .facts li { --from: calc(51.58 + var(--i) * 2.363); --to: calc(57.88 + var(--i) * 2.363); animation-name: rise-in; }
  .film .scene--private .beat--1 { --at: 64.06; } /* step: statement and facts complete (62.6) before the pin ends (65.52) */

  /* 06 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 logo-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes logo-out { 0% { opacity: 1; transform: none; animation-timing-function: cubic-bezier(.5, 0, .75, 0); } 100% { opacity: 0; transform: scale(.96); } }
@keyframes fade-out-up { 0% { opacity: 1; transform: none; animation-timing-function: cubic-bezier(.5, 0, .75, 0); } 100% { opacity: 0; transform: translateY(-6vh); } }
@keyframes rise-in { 0% { opacity: 0; transform: translateY(24px); animation-timing-function: cubic-bezier(.16, 1, .3, 1); } 100% { opacity: 1; transform: none; } }
@keyframes word-in { from { opacity: .16; } to { opacity: 1; } }
/* @gen:keyframes:start */

/* @gen:keyframes:end */
