/* Hero — full-bleed stage for the brand artwork, now in motion.
   The illustration is still the star. What changed (operator, 2026-08-17):

     · the frame is gone — no border, no 18px radius, no drop shadow, and no
       1180px column. The artwork reaches the edges instead of sitting in a
       card. `.hero-banner` has no max-width/padding of its own and its only
       ancestor is <body>, so edge-to-edge here is the ABSENCE of a
       constraint, not a negative-margin breakout.
     · the artwork plays once. `banner.jpg` / `banner_mobile.jpg` are now
       TYPE-FREE plates (the 繁中 lockup and the script mark were lifted out
       to hero-type-a/b.png), so the video model never had to render Chinese
       type or the logo — and so the fallback image cannot show the type
       twice.
     · the lockup arrives on its own two beats, over whichever layer won.

   NOTHING IS EVER CROPPED (operator, 2026-08-17). The stage carries the
   artwork's exact aspect ratio, so `object-fit:cover` is a no-op fit rather
   than a crop, and the type layer's percentage positions land on the same
   pixels they occupy in the source painting. On viewports too wide for the
   artwork's natural size the stage stops growing and cream — the artwork's
   own ground — continues to the edges. On a phone that means a shallow
   ~2:1 band; that is the accepted trade for never chopping the picture.

   RETIRED: the `.hero-stage::after` curtain wipe and the 24s Ken Burns
   drift. Both existed to give a single still image one flourish. That job
   now belongs to the video and the lockup entrance, and running all three
   in the same second reads as busy rather than reverent.

   Compositor-only (transform/opacity); decorative motion gated behind
   prefers-reduced-motion: no-preference; `will-change` never set here.
   See MOTION-CONTRACT.md. */

.hero-banner{
  /* sampled from the artwork's own ground: rgb(252,247,244). Deliberately the
     measured value rather than --cc-bg (#FCF7F3), which is one step off in
     blue — at this size a 1-value seam down the edge of the plate is visible.
     Declared here, on the outer element, so both the band and the stage
     inherit it. */
  --hero-cream:#fcf7f4;
  position:relative;
  width:100%;
  margin:0;
  padding:0;
  background:var(--hero-cream);
}

/* Truly edge-to-edge at every width (operator, 2026-08-17). There was a height cap here,
   and on a 1851px-wide window it held the stage to 1173px — 63% of the viewport, i.e. the
   same width as the old 1180px framed banner. The frame was gone but the COLUMN was not,
   which was half the complaint.

   Since the artwork is never cropped, full width and bounded height cannot both hold: at
   2:1 a 1851px viewport implies a 925px-tall hero. The operator chose width. So the hero
   is as tall as its own ratio dictates and the tabs may fall below the fold on short
   windows — a deliberate trade, not an oversight. */
.hero-stage{
  position:relative;
  width:100%;
  aspect-ratio:1600 / 800;
  overflow:hidden;
  background:var(--hero-cream);
}

/* the mobile plate is a slightly tighter composition (1480×800), so the stage
   takes its ratio too — otherwise `cover` would start cropping it */
@media (max-width:767px){
  .hero-stage{aspect-ratio:1480 / 800}
}

/* ---- layer 1: the plate. Always rendered. This is the base state, not a
   poster — if the video never plays, this IS the hero. ---- */
.hero-stage picture{position:absolute;inset:0;display:block}
.hero-stage picture img{
  position:absolute;inset:0;
  width:100%;height:100%;
  object-fit:cover;
  display:block;
  z-index:1;
}

/* The clip is generated from a 1422×800 crop of the plate — an exact 16:9, because
   Seedance ignores the start image's ratio and renders 1920×1080 regardless (a 2:1 plate
   made the job fail outright). That crop discards only right-hand cream; the artwork ends
   at x=967, so nothing painted is lost. The box below maps those 1422 source pixels back
   onto their true position in the 1600-wide composition, which is what keeps the video and
   the still plate in register. The cream strip it doesn't cover is painted by .hero-stage
   in the identical sampled colour, so the join is invisible. */
.hero-video{
  position:absolute;
  top:0;left:0;height:100%;
  width:88.875%;                     /* 1422 / 1600 */
  object-fit:cover;
  display:block;
}
/* banner-plate-mobile.jpg is measurably banner-plate.jpg cropped 120px off the LEFT
   and 0 off the right (verified pixel-wise: mean abs diff 0.50/255). The clip covers
   source x 0–1421, so on mobile its first 120px fall outside the frame: shift it left
   by exactly that much and let the stage's overflow clip it. Without this the two
   women visibly jump sideways the moment the clip fades in.
   NOTE: this block must stay AFTER the base .hero-video rule — same specificity, so
   source order is what decides it. */
@media (max-width:767px){
  .hero-video{
    left:-8.1081%;                   /* −120 / 1480 */
    width:96.0811%;                  /* 1422 / 1480 */
  }
}

/* ---- layer 2: the clip. Revealed only by hero-video.js, and only on a
   RESOLVED play() promise. Never revealed by CSS alone. ---- */
.hero-video{
  z-index:2;
  opacity:0;
  transition:opacity 520ms var(--e-out);
}
.hero-video.is-playing{opacity:1}
@media (prefers-reduced-motion:reduce){
  .hero-video{transition-duration:.01ms}
}

/* ---- layer 3: the lockup, lifted out of the painting and put back on top.
   Positioned by percentages measured off the source art, so it lands on the
   same pixels it was painted on. ---- */
.hero-type{position:absolute;inset:0;z-index:3;pointer-events:none}

/* The clip's slow push-in drags the street — and the coral tree — rightward into the
   cream margin the lockup sits on. Measured on the delivered clip: by the final frame
   the blossom occupies ~65–77% of the stage, directly behind 閨. So the video path gets
   a wash that returns that margin to paper.

   Gated on .hero--video-on (set by hero-video.js) rather than applied unconditionally:
   the static plates' cream is already clean, and an always-on wash would fade the coral
   tree in the SHIPPED artwork — changing a picture nobody asked to change. Fallback
   visitors therefore see today's painting untouched.

   A soft 9% ramp rather than a hard mask, because dissolving into bare paper is the
   source artwork's own idiom — the street already does exactly this on the right. */
.hero-type::before{
  content:"";position:absolute;inset:0;
  background:linear-gradient(to right,
    rgba(252,247,244,0) 55%, var(--hero-cream) 64%);
  opacity:0;
  transition:opacity 520ms var(--e-out);
}
.hero--video-on .hero-type::before{opacity:1}
@media (prefers-reduced-motion:reduce){
  .hero-type::before{transition-duration:.01ms}
}

.hero-type img{position:absolute;display:block;height:auto}
/* left/width from make-hero-assets.mjs — the measured lockup box as a
   fraction of the plate. height stays auto: at these widths the intrinsic
   ratio reproduces the source placement exactly at both breakpoints. */
.hero-type-a{left:64.1875%;top:19.25%;width:34.5%}
.hero-type-b{left:64.1875%;top:53.5%;width:34.5%}
@media (max-width:767px){
  .hero-type-a,.hero-type-b{left:61.2838%;width:37.2973%}
}

/* final-state-first: with no JS, or no CSS animation, the lockup is simply
   present. `.hero--seq` is added by hero-video.js ONLY when it is going to
   drive the sequence — so reduced-motion never hides it and then fails to
   bring it back. */
.hero-type img{opacity:1;transform:none}
.hero--seq .hero-type img{opacity:0}
@media (prefers-reduced-motion:reduce){
  .hero--seq .hero-type img{opacity:1}
}
/* The lockup arrives at the START, over the opening frames, rather than waiting for the
   clip to finish (operator, 2026-08-17). Timing is therefore pure CSS on a delay — it no
   longer depends on the video's `ended` event, so it lands identically whether the clip
   plays, is refused by iOS, or was never configured. `both` fill holds the from-state
   through the delay, so the delay itself is what hides it, not a separate rule. */
@keyframes hero-type-a-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
@keyframes hero-type-b-in{from{opacity:0;transform:scale(.945)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:no-preference){
  .hero--seq .hero-type-a{
    animation:hero-type-a-in 760ms var(--e-out) 320ms both;
  }
  .hero--seq .hero-type-b{
    animation:hero-type-b-in 820ms var(--e-spring) 520ms both;
  }
}

/* ---- retired sky layer (clouds/sparkles) and balloon motifs — hooks stay inert ---- */
.hero-sky,.hero-bubble,.hero-dots{display:none}

/* ---- retro marquee for the LIVE strip (markup adds .marquee wrap) ---- */
.yt-strip{overflow:hidden}
.yt-strip .marquee{display:flex;white-space:nowrap;will-change:auto}
.yt-strip .marquee > *{flex:0 0 auto;padding:0 28px}
@media (prefers-reduced-motion: no-preference){
  .yt-strip .marquee{animation:yt-marquee 18s linear infinite}
  .yt-strip .marquee:hover{animation-play-state:paused}
  @keyframes yt-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
}
