/* ═══════════════════════════════════════════════════════════════════════════
   金烟 JINYAN — v2, video-driven
   Static HTML + CSS + anime.js v4. No framework, no build step, no canvas.

   Three states on <body data-state>: offer | playing | held.
   ═══════════════════════════════════════════════════════════════════════════ */

:root{
  /* Google Sans Flex carries Latin only, so the CJK faces sit behind it and
     pick up 抽一根 / 持有者 / 买入 without a second request. */
  --sans: "Google Sans Flex", "PingFang SC", "Hiragino Sans GB",
          "Microsoft YaHei", "Noto Sans SC", system-ui, -apple-system, sans-serif;
  /* monospace is kept ONLY where it is functional: the contract address, where
     telling 0 from O matters. Everything else is the brand face. */
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --gold:     #d8b45c;
  --gold-hot: #f4d98a;
  --ink:      #0a0806;
  --ember:    #ff6a1e;
  --paper:    #efe6cf;

  /* Ember position in frame-last.jpg, measured off the real frame (the lit tip
     sits at roughly 0.615, 0.665 of the 16:9 image). Drives the smoke origin
     and the held-content keep-out zone. */
  --cig-x: 61.5%;
  --cig-y: 66%;

  /* Crop focus. MEASURED, not assumed: the old man's face is at x~0.43 and the
     background smoker's at x~0.62, so the pair straddles the CENTRE. Biasing
     left (my first guess) throws the smoker away, and the second man is what
     makes the scene read as 散烟 rather than one man holding a cigarette. A
     near-centred focus keeps both. See the portrait note in the media query. */
  --focus: 51% 50%;

  --ui-pad: clamp(14px, 4vw, 32px);
}

*,*::before,*::after{ box-sizing:border-box; }

html,body{
  margin:0; height:100%;
  background:var(--ink); color:var(--paper);
  font-family:var(--sans);
  overscroll-behavior:none;
}
body{
  position:fixed; inset:0; overflow:hidden;
  -webkit-tap-highlight-color:transparent;
}
.sr{ position:absolute; width:1px; height:1px; overflow:hidden;
     clip:rect(0 0 0 0); white-space:nowrap; }

/* ── stage ──────────────────────────────────────────────────────────────── */
.stage{ position:absolute; inset:0; overflow:hidden; background:var(--ink); }

.held-pic{ position:absolute; inset:0; display:block; z-index:1; }
.clip,.held-frame{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover; object-position:var(--focus);
  display:block;
  /* the escalation scales this element and nothing else */
  transform-origin:50% 50%;
  will-change:transform;
  user-select:none; -webkit-user-drag:none;
}
/* nothing may interrupt the clip while it runs */
.clip{ pointer-events:none; }

/* The held frame is stacked ON TOP of the video and revealed at the swap.
   Same box, same object-fit, same object-position, so the handover is a
   pixel-for-pixel replacement rather than a re-layout. */
.held-frame{ z-index:1; opacity:0; visibility:hidden; }
body[data-state="held"] .held-frame{ opacity:1; visibility:visible; }
/* The video is hidden from JS, AFTER the held frame reports decoded — doing it
   here would race the decode and could flash the stage background. The frame
   sits above the video anyway, so it covers it either way. */

/* placeholders while the real assets are absent */
.stage[data-missing-video] .clip{ background:#1b1610; }
.stage[data-missing-frame] .held-frame{ background:#221a12; }
.stage[data-missing-video]::after{
  content:'OFFER.MP4 / .WEBM — 16:9';
  position:absolute; inset:0; z-index:0;
  display:flex; align-items:center; justify-content:center;
  font:600 11px/1 var(--sans); letter-spacing:.28em;
  color:rgba(239,230,207,.24);
  background:linear-gradient(160deg,#241d13,#12100b 70%);
}
body[data-state="held"] .stage[data-missing-frame]::after{
  content:'FRAME-LAST.JPG — 16:9';
  background:linear-gradient(160deg,#2a2116,#141009 70%);
  z-index:2;
}

/* dark vignette so type stays legible over the photograph */
.vignette{
  position:absolute; inset:0; z-index:3; pointer-events:none;
  background:
    radial-gradient(120% 90% at 50% 42%, transparent 34%, rgba(6,5,3,.62) 100%),
    linear-gradient(to top, rgba(6,5,3,.86) 0%, transparent 46%);
}

/* ── smoke off the ember, HELD only ─────────────────────────────────────── */
.smoke{
  position:absolute; inset:0; z-index:4; pointer-events:none;
  opacity:0; visibility:hidden;
}
body[data-state="held"] .smoke{ opacity:1; visibility:visible; }
.puff{
  position:absolute; display:block; border-radius:50%;
  left:var(--cig-x); top:var(--cig-y);
  width:26vmin; height:26vmin; margin:-13vmin 0 0 -13vmin;
  background:radial-gradient(50% 50% at 50% 55%,
             rgba(228,222,208,.26), rgba(228,222,208,.07) 56%, transparent 74%);
  filter:blur(16px);
  opacity:0;
  will-change:transform,opacity;
}
.puff[data-puff="1"]{ width:34vmin; height:34vmin; margin:-17vmin 0 0 -17vmin; filter:blur(22px); }
.puff[data-puff="2"]{ width:20vmin; height:20vmin; margin:-10vmin 0 0 -10vmin; filter:blur(12px); }
.puff[data-puff="3"]{ width:42vmin; height:42vmin; margin:-21vmin 0 0 -21vmin; filter:blur(28px); }

/* ── UI ─────────────────────────────────────────────────────────────────── */
.ui{ position:absolute; inset:0; z-index:5; pointer-events:none; }
.ui button,.ui a{ pointer-events:auto; }
button{ font:inherit; color:inherit; background:none; border:0; padding:0; cursor:pointer; }

/* every overlay is gone while the clip runs — nothing covers the video */
body[data-state="playing"] .ui{ opacity:0; pointer-events:none; }

/* The offer is removed from the layout in HELD rather than merely animated to
   opacity 0. Relying on an animation's end state for layout is fragile: if the
   fade is interrupted, or rAF is throttled (backgrounded tab), or reduced
   motion shortens it, the offer stays over the held content — and an invisible
   YES button still takes clicks. */
body[data-state="held"] .offer{ display:none; }

/* language toggle */
.lang{
  position:absolute; top:var(--ui-pad); right:var(--ui-pad);
  font:600 11px/1 var(--sans); letter-spacing:.16em;
  color:rgba(239,230,207,.62);
  border:1px solid rgba(239,230,207,.22); border-radius:3px;
  min-width:44px; min-height:44px;
  padding:0 13px;
  display:flex; align-items:center; justify-content:center;
  backdrop-filter:blur(6px);
}
.lang:hover{ color:var(--gold); border-color:rgba(216,180,92,.5); }
.lang > span{ display:flex; align-items:center; gap:9px; }
.flag{
  width:18px; height:12px; display:block; flex:none;
  border-radius:1.5px; box-shadow:0 0 0 1px rgba(0,0,0,.35);
}
/* letter-spacing adds a trailing gap after the LAST character, which makes the
   right padding read wider than the left. Pull it back so the box looks even. */
.lang-code{ font:inherit; letter-spacing:.16em; margin-right:-.16em; }

/* ── OFFER ──────────────────────────────────────────────────────────────── */
.offer{
  position:absolute; left:0; right:0; bottom:clamp(46px,9vh,88px);
  text-align:center; padding:0 var(--ui-pad);
}
.prompt{ text-shadow:0 2px 22px rgba(0,0,0,.8); }
.prompt-zh{
  display:block;
  font-size:clamp(38px,11vmin,84px); font-weight:700;
  letter-spacing:.14em; line-height:1; color:var(--paper);
}
.prompt-en{
  display:block; margin-top:clamp(8px,1.6vmin,14px);
  font:600 clamp(10px,1.5vmin,13px)/1 var(--sans);
  letter-spacing:.42em; color:rgba(239,230,207,.66); text-transform:uppercase;
}

.actions{
  margin-top:clamp(22px,4vh,40px);
  display:flex; flex-direction:column; align-items:center;
  gap:clamp(12px,2vh,18px);
}
/* YES is the primary target: big, warm, unmissable */
.yes{
  position:relative;
  min-width:clamp(168px,44vw,232px); min-height:56px;
  padding:16px 30px;
  border:1px solid rgba(216,180,92,.55); border-radius:2px;
  background:rgba(216,180,92,.10);
  backdrop-filter:blur(8px);
  color:var(--gold-hot);
  font-size:clamp(17px,2.6vmin,21px); font-weight:700; letter-spacing:.2em;
  transition:background .18s ease, border-color .18s ease;
}
.yes:hover{ background:rgba(216,180,92,.2); border-color:rgba(216,180,92,.9); }
.yes:focus-visible{ outline:2px solid var(--gold); outline-offset:4px; }
.yes .yes-zh{ display:inline-flex; align-items:baseline; gap:.5em; }
/* buffering state — shown instead of playing a stuttering clip */
.yes-load{
  position:absolute; left:50%; bottom:9px; width:34px; height:2px;
  margin-left:-17px; overflow:hidden; opacity:0;
  background:rgba(216,180,92,.25);
}
.yes-load::after{
  content:''; position:absolute; inset:0; width:40%;
  background:var(--gold-hot);
  animation:buf 900ms linear infinite;
}
@keyframes buf{ from{ transform:translateX(-100%);} to{ transform:translateX(250%);} }
.yes[data-loading] .yes-load{ opacity:1; }
.yes[data-loading]{ color:rgba(244,217,138,.55); }

/* decline: small, quiet, exists to be funny */
.decline{
  padding:10px 14px; min-height:44px;
  font-size:clamp(12px,1.8vmin,15px);
  color:rgba(239,230,207,.44);
  text-decoration:underline; text-underline-offset:4px;
  text-decoration-color:rgba(239,230,207,.22);
  transform-origin:50% 50%;
}
.decline:hover{ color:rgba(239,230,207,.8); }
.decline:focus-visible{ outline:2px solid var(--gold); outline-offset:3px; }

/* ── HELD ───────────────────────────────────────────────────────────────── */
/* Anchored LEFT and LOWER: the cigarette is held centre-right, so content
   wraps around it rather than across it. The keep-out zone is derived from
   --cig-x so retuning the ember position moves the copy with it. */
.held{
  position:absolute; left:var(--ui-pad); bottom:clamp(34px,7vh,68px);
  display:none; flex-direction:column; align-items:flex-start;
  gap:clamp(16px,3vh,26px);
  max-width:min(46ch, calc(var(--cig-x) - 8%));
}
body[data-state="held"] .held{ display:flex; }
.held-item{ opacity:0; }

.ca{
  display:flex; flex-direction:column; align-items:flex-start; gap:5px;
  padding:12px 14px; min-height:44px;
  border:1px solid rgba(216,180,92,.3); border-radius:2px;
  background:rgba(10,8,6,.42); backdrop-filter:blur(8px);
  text-align:left; max-width:100%;
}
.ca:hover{ border-color:rgba(216,180,92,.65); }
.ca:focus-visible{ outline:2px solid var(--gold); outline-offset:3px; }
.ca-label{
  font:600 9px/1 var(--sans); letter-spacing:.24em;
  color:rgba(239,230,207,.5); text-transform:uppercase;
}
.ca-value{
  font:600 clamp(11px,1.7vmin,14px)/1.3 var(--mono);
  color:var(--gold-hot); letter-spacing:.02em;
  word-break:break-all; max-width:100%;
}
.ca-done{
  font:600 9px/1 var(--sans); letter-spacing:.22em;
  color:#8fc06a; text-transform:uppercase;
  opacity:0; transition:opacity .18s ease;
}
.ca[data-copied] .ca-done{ opacity:1; }
/* the address stays selectable so a webview that blocks the clipboard can
   still be copied from by hand */
.ca-value{ user-select:text; -webkit-user-select:text; }
.ca-fail{
  font:600 9px/1 var(--sans); letter-spacing:.22em;
  color:#e0a14e; text-transform:uppercase;
  opacity:0; transition:opacity .18s ease;
}
.ca[data-copyfail] .ca-fail{ opacity:1; }

.held-links{
  display:flex; flex-wrap:wrap; gap:clamp(12px,2.6vmin,26px);
}
.held-links a{
  display:flex; flex-direction:column; gap:4px;
  min-height:44px; justify-content:center;
  text-decoration:none; color:var(--paper);
}
.held-links .zh{ font-size:clamp(15px,2.3vmin,19px); font-weight:600; letter-spacing:.1em; }
.held-links .en{
  font:600 clamp(9px,1.1vmin,11px)/1 var(--sans);
  letter-spacing:.22em; color:rgba(239,230,207,.55);
}
.held-links a:hover .zh,.held-links a:focus-visible .zh{ color:var(--gold); }

/* ── locale ─────────────────────────────────────────────────────────────
   Both languages stay in the DOM and both stay visible; data-lang only picks
   which is primary. The toggle's own label is the one thing that swaps. */
[data-lang="zh"] .lang-en{ display:none; }
[data-lang="en"] .lang-zh{ display:none; }

[data-lang="zh"] .en,
[data-lang="en"] .zh{ font-size:.7em; opacity:.5; letter-spacing:.2em; font-weight:500; }

[data-lang="en"] .prompt{ display:flex; flex-direction:column; align-items:center; }
[data-lang="en"] .prompt-zh{ font-size:clamp(15px,2.2vmin,20px); order:2; opacity:.6;
                             letter-spacing:.2em; margin-top:12px; }
/* "Take a Golden Cig" is more than twice the width of "Take one", so the
   headline needs a smaller floor and permission to wrap; at the old
   clamp(26px..) it overflowed a 390px phone. */
[data-lang="en"] .prompt-en{ font-size:clamp(21px,6.2vmin,50px); order:1; margin-top:0;
                             letter-spacing:.1em; color:var(--paper);
                             line-height:1.08; text-wrap:balance;
                             max-width:14ch; margin-inline:auto; }
[data-lang="en"] .held-links a .en{ font-size:clamp(14px,2.1vmin,18px); font-weight:600;
                                    color:var(--paper); letter-spacing:.12em; opacity:1; order:1; }
[data-lang="en"] .held-links a .zh{ order:2; }

/* Inline bilingual pairs. Flex + order rather than display swaps or a ::before
   separator: the DOM is always zh-then-en, so in EN mode the primary language
   has to be REORDERED to come first. A gap plus the size/opacity difference
   separates them; no punctuation needed. */
.decline,.ca-label,.ca-done,.ca-fail,.yes-zh{
  display:inline-flex; align-items:baseline; gap:.45em;
}
.decline{ display:inline-flex; }
[data-lang="en"] .decline .zh,
[data-lang="en"] .ca-label .zh,
[data-lang="en"] .ca-done .zh,
[data-lang="en"] .ca-fail .zh,
[data-lang="en"] .yes-zh .zh{ order:2; }
[data-lang="zh"] .decline .en,
[data-lang="zh"] .ca-label .en,
[data-lang="zh"] .ca-done .en,
[data-lang="zh"] .ca-fail .en,
[data-lang="zh"] .yes-zh .en{ order:2; }

/* ── responsive ─────────────────────────────────────────────────────────
   A 16:9 push-in centre-cropped into a portrait phone loses the background
   smoker entirely, so narrow viewports bias further left. */
/* PORTRAIT CONSTRAINT, measured: a 16:9 source cover-cropped into a ~9:19.5
   phone shows only ~26% of the source width, while the two faces span ~28.5%
   (x 0.38 to 0.665). Both cannot fit. 51% clips a sliver off the outer edge of
   each face rather than losing either one. The real fix is a portrait crop of
   the clip; see assets/README.md. */
@media (max-width:620px){
  :root{ --focus:51% 50%; }
  .held{ max-width:calc(100% - var(--ui-pad)*2); }
}
@media (max-width:400px){
  .held-links{ gap:10px 16px; }
}
@media (orientation:portrait){
  /* held content sits under the cigarette rather than beside it */
  .held{ max-width:calc(100% - var(--ui-pad)*2); }
}
@media (max-height:520px) and (orientation:landscape){
  .offer{ bottom:14px; }
  .prompt-zh{ font-size:clamp(26px,8vmin,44px); }
  .actions{ flex-direction:row; margin-top:14px; }
  .held{ bottom:14px; gap:10px; }
}

/* ── reduced motion ─────────────────────────────────────────────────────── */
@media (prefers-reduced-motion:reduce){
  *{ animation:none !important; }
  .clip{ transform:none !important; }
  .yes{ transition:none; }
}
