/* ═══════════════════════════════════════════════════════════════════════════
   Cathy Huang — digital business card.

   Mobile-first: every base rule targets the phone, and the only breakpoints add
   desktop layout on top. The brief pins the visual direction (light, navy,
   LinkedIn-professional), so this follows the reference and spends its effort on
   type rhythm, tap targets, and the poster→video transition instead.

   Fonts are self-hosted (subset woff2, 15KB each) rather than loaded from Google
   Fonts: this page gets opened in WeChat's in-app browser in mainland China,
   where fonts.googleapis.com is unreachable and would leave the page in a
   fallback face after a long timeout.
   ═════════════════════════════════════════════════════════════════════════ */

@font-face{
  font-family:"Instrument Sans";src:url("../fonts/is-400.woff2") format("woff2");
  font-weight:400;font-style:normal;font-display:swap}
@font-face{
  font-family:"Instrument Sans";src:url("../fonts/is-700.woff2") format("woff2");
  font-weight:700;font-style:normal;font-display:swap}

:root{
  /* Navy is the anchor, sampled from the reference. One accent only — the
     "professional, restrained, credible" brief rules out a second hue. */
  --navy:      #0f2c56;
  --navy-deep: #0a1f3d;
  --ink:       #12181f;
  --ink-2:     #4a5460;
  --ink-3:     #6b7684;
  --line:      #e3e7ec;
  --line-2:    #eef1f5;
  --page:      #f7f8fa;
  --card:      #ffffff;
  --tint:      #f0f4fa;   /* navy at low opacity, for the eyebrow pill */

  --sans:"Instrument Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",
         "PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans SC",
         sans-serif;

  --r:   12px;   /* card radius */
  --r-s: 9px;    /* control radius */
  --gut: 20px;   /* page gutter, phone */
}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

body{
  margin:0;background:var(--page);color:var(--ink);
  font-family:var(--sans);font-size:16px;line-height:1.6;
  -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
  font-feature-settings:"kern" 1,"liga" 1;
  /* Notch-safe: this page is opened from QR codes on phones. */
  padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right)}

h1,h2{margin:0;font-weight:700;letter-spacing:-.021em;line-height:1.15}
p{margin:0}
ul{margin:0;padding:0;list-style:none}
a{color:inherit}
img,video,svg{display:block;max-width:100%}
button{font:inherit;color:inherit}

/* Visible focus everywhere — part of the quality floor, not decoration. */
:focus-visible{outline:2px solid var(--navy);outline-offset:2px;border-radius:4px}

.skip{position:absolute;left:-9999px;top:0;background:var(--navy);color:#fff;
  padding:10px 16px;z-index:50;border-radius:0 0 var(--r-s) 0;text-decoration:none}
.skip:focus{left:0}

/* ── Bar ─────────────────────────────────────────────────────────────────── */
.bar{position:sticky;top:0;z-index:20;
  display:flex;align-items:center;justify-content:space-between;
  padding:14px var(--gut);background:rgba(247,248,250,.88);
  backdrop-filter:saturate(180%) blur(12px);
  -webkit-backdrop-filter:saturate(180%) blur(12px);
  border-bottom:1px solid var(--line)}
.wordmark{font-weight:700;font-size:17px;letter-spacing:-.02em;color:var(--navy-deep)}
.lang{display:flex;align-items:center;gap:7px}
.lang-btn{background:none;border:0;padding:5px 3px;cursor:pointer;
  font-size:14px;color:var(--ink-3);border-bottom:2px solid transparent;
  min-height:34px}
.lang-btn.is-on{color:var(--navy);font-weight:700;border-bottom-color:var(--navy)}
.lang-sep{color:#c3cad3;font-size:14px}

/* ── Hero ────────────────────────────────────────────────────────────────── */
/* Phone: portrait first, then identity. One column, no cleverness. */
.hero{padding:22px var(--gut) 4px;display:grid;gap:22px}

.twin{margin:0}
/* Phone: a small portrait, not a full-bleed one. At full width the 7:12 clip was
   350×600 and pushed the name and the lede off the first screen; 3:4 at 244px is
   large enough to read a face and leaves the identity block above the fold.
   The play pill sits *inside* the frame, so the caption stays 9px under the
   picture whether the clip is stopped or running — an under-the-frame control
   leaves a hole the width of itself the moment it hides. */
.twin-frame{position:relative;border-radius:var(--r);overflow:hidden;
  background:#dcdcda;aspect-ratio:3/4;width:min(72vw,244px);
  box-shadow:0 1px 2px rgba(16,24,40,.05),0 10px 26px -12px rgba(16,24,40,.18)}
/* Poster and video are stacked, object-fit:cover on the same box at the same
   object-position, so the swap on play has no reflow and no visible jump. The
   poster is a deliberate open-eyed frame — the clip's own frame 0 is mid-blink,
   which is why the still is not simply cut from the video. */
.twin-poster,.twin-video{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:50% 14%}
.twin-video{opacity:0;transition:opacity .18s linear}
.twin-frame.is-playing .twin-video{opacity:1}
.twin-frame.is-playing .twin-play{opacity:0;pointer-events:none}

/* The play control is a labelled pill, not a bare glyph: "Watch AI Twin (0:14)"
   tells you what it is and how long it costs before you spend the tap. */
.twin-play{position:absolute;left:10px;bottom:10px;
  display:inline-flex;align-items:center;gap:8px;
  padding:7px 13px 7px 7px;border:0;border-radius:999px;cursor:pointer;
  background:rgba(255,255,255,.94);color:var(--navy-deep);
  box-shadow:0 2px 10px rgba(16,24,40,.22);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  transition:opacity .2s ease,transform .2s ease;min-height:44px}
.twin-icon{display:grid;place-items:center;width:26px;height:26px;flex:none;
  border-radius:50%;background:var(--navy)}
.twin-icon svg{width:11px;height:11px;fill:#fff;margin-left:1.5px}
.twin-label{font-size:12.5px;font-weight:700;letter-spacing:-.005em;white-space:nowrap}
.twin-play:hover{transform:translateY(-1px)}
.twin-sound{position:absolute;right:12px;bottom:12px;
  padding:6px 11px;border-radius:999px;font-size:12px;font-weight:700;
  background:rgba(10,31,61,.82);color:#fff;letter-spacing:.01em}
.twin-sound[hidden]{display:none}
.twin figcaption{margin-top:9px;font-size:12px;color:var(--ink-3);
  letter-spacing:.005em}

.eyebrow{display:inline-block;margin:0 0 12px;padding:4px 11px;
  border-radius:999px;background:var(--tint);color:var(--navy);
  font-size:11.5px;font-weight:700;letter-spacing:.075em;text-transform:uppercase}
h1{font-size:clamp(34px,10vw,46px);color:var(--navy-deep);letter-spacing:-.032em}
.role{margin-top:9px;font-size:16px;font-weight:700;color:var(--ink);
  letter-spacing:-.012em;line-height:1.35}
.lede{margin-top:13px;font-size:15.5px;color:var(--ink-2);line-height:1.62}

.proof{display:flex;flex-wrap:wrap;align-items:center;gap:7px 0;
  margin-top:18px;font-size:13.5px;color:var(--ink-2)}
.proof li{display:flex;align-items:center}
/* Separator lives in CSS, not in the markup: keeps the translated strings clean
   and the dot never orphans onto its own line. */
.proof li+li::before{content:"·";margin:0 9px;color:#c3cad3}
.proof b{color:var(--navy);font-weight:700}
/* Phone shows the short forms so the row stays on one line. */
.pf-full{display:none}
.pf-short{display:inline}

/* ── Actions ─────────────────────────────────────────────────────────────── */
/* Phone: Save Contact full-width on its own row (it's the primary job), the
   other three share a row. All ≥44px tall. */
.acts{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-top:20px}
.act{grid-column:span 1;
  display:inline-flex;align-items:center;justify-content:center;gap:7px;
  min-height:46px;padding:11px 12px;border-radius:var(--r-s);
  background:var(--card);border:1px solid var(--line);
  color:var(--ink);text-decoration:none;
  font-size:14px;font-weight:700;letter-spacing:-.005em;
  transition:border-color .18s ease,background .18s ease,transform .18s ease}
.act:hover{border-color:#c9d2dd;background:#fcfdfe}
.act:active{transform:translateY(1px)}
.act-1{grid-column:1/-1;background:var(--navy);border-color:var(--navy);color:#fff}
.act-1:hover{background:var(--navy-deep);border-color:var(--navy-deep)}
.ico{width:16px;height:16px;flex:none;fill:none;stroke:currentColor;
  stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}

.chips{display:flex;flex-wrap:wrap;gap:7px;margin-top:16px}
.chips li{padding:6px 11px;border-radius:999px;background:var(--card);
  border:1px solid var(--line);font-size:12.5px;font-weight:700;color:var(--ink-2)}

/* ── Triad ───────────────────────────────────────────────────────────────── */
.triad{display:grid;gap:11px;padding:26px var(--gut) 0}
.card{background:var(--card);border:1px solid var(--line);
  border-radius:var(--r);padding:19px 18px 20px;
  box-shadow:0 1px 2px rgba(16,24,40,.03)}
/* A 2px navy rule reading as an underline on the heading. Same device across all
   three cards — no per-card accent colours, which would read as decoration. */
.card h2{font-size:16.5px;color:var(--navy-deep);padding-bottom:11px;
  position:relative;letter-spacing:-.018em}
.card h2::after{content:"";position:absolute;left:0;bottom:0;
  width:26px;height:2px;background:var(--navy);border-radius:2px}
.card p{margin-top:12px;font-size:14.5px;color:var(--ink-2);line-height:1.6}

/* ── Projects ────────────────────────────────────────────────────────────
   One card per built thing: demo media on top (phone) / left (desktop),
   title + one-paragraph story + repo link. Same card language as .cred. */
.projects{margin:26px var(--gut) 0}
.proj-card{margin-top:15px;background:var(--card);
  border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.proj-media{display:block;background:#0b1220}
.proj-media img{display:block;width:100%;height:auto}
.proj-body{padding:18px 18px 20px}
.proj-body h3{margin:0;font-size:17px;font-weight:700;letter-spacing:-.015em}
.proj-tag{margin-top:4px;font-size:12.5px;font-weight:700;
  letter-spacing:.02em;color:var(--navy)}
.proj-desc{margin-top:10px;font-size:14.5px;color:var(--ink-2);line-height:1.6}
.proj-links{margin-top:14px;display:flex;gap:10px;flex-wrap:wrap}
.proj-links .act{flex:none}

/* ── Credibility ─────────────────────────────────────────────────────────── */
.cred{display:grid;gap:24px;margin:26px var(--gut) 0;padding:22px 18px;
  background:var(--card);border:1px solid var(--line);border-radius:var(--r)}
.cred-h{font-size:11.5px;font-weight:700;letter-spacing:.085em;
  text-transform:uppercase;color:var(--ink-3);
  padding-bottom:11px;border-bottom:1px solid var(--line-2)}
.bg,.pubs{margin-top:15px;display:grid;gap:14px}
.bg-d{display:block;font-size:14.5px;font-weight:700;letter-spacing:-.008em}
.bg-m{display:block;font-size:13px;color:var(--ink-3);margin-top:2px;line-height:1.5}
.pubs a{display:block;font-size:14.5px;font-weight:700;color:var(--navy);
  text-decoration:none;line-height:1.45;letter-spacing:-.008em}
.pubs a:hover{text-decoration:underline;text-underline-offset:2px}
.venue{display:block;font-size:12.5px;color:var(--ink-3);margin-top:3px}

/* ── CTA ─────────────────────────────────────────────────────────────────── */
.cta{margin:26px var(--gut) 0;padding:22px 18px;border-radius:var(--r);
  background:var(--navy);color:#fff}
.cta-t{font-size:16px;font-weight:700;line-height:1.45;letter-spacing:-.012em}
.cta-acts{display:grid;gap:9px;margin-top:17px}
/* Inverted here: on navy, the filled white button is the emphatic one. */
.cta .act{background:transparent;border-color:rgba(255,255,255,.28);color:#fff}
.cta .act:hover{background:rgba(255,255,255,.09);border-color:rgba(255,255,255,.45)}
.cta .act-1{background:#fff;border-color:#fff;color:var(--navy-deep)}
.cta .act-1:hover{background:#eef2f8;border-color:#eef2f8}

.foot{display:flex;justify-content:space-between;flex-wrap:wrap;gap:6px;
  margin-top:28px;padding:18px var(--gut) calc(26px + env(safe-area-inset-bottom));
  border-top:1px solid var(--line);font-size:12px;color:var(--ink-3)}

/* ── Copy toast ───────────────────────────────────────────────────────────────
   Confirms the email address landed on the clipboard — mailto: alone does
   nothing on devices without a mail client. z-index above the sheet, since the
   sheet also carries a mailto link. */
.copy-toast{position:fixed;left:50%;bottom:calc(22px + env(safe-area-inset-bottom));
  transform:translate(-50%,8px);z-index:60;max-width:calc(100vw - 2*var(--gut));
  padding:10px 16px;border-radius:999px;background:rgba(10,31,61,.92);color:#fff;
  font-size:13px;font-weight:700;letter-spacing:.005em;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;
  opacity:0;pointer-events:none;
  transition:opacity .2s ease,transform .2s ease}
.copy-toast.is-on{opacity:1;transform:translate(-50%,0)}

/* ── Save Contact sheet ───────────────────────────────────────────────────────
   A bottom sheet on the phone and a centred dialog from tablet up — the two
   shapes people already expect on each. */
.sheet{position:fixed;inset:0;z-index:50;display:flex;align-items:flex-end;
  justify-content:center}
.sheet[hidden]{display:none}
.sheet-veil{position:absolute;inset:0;background:rgba(10,31,61,.44);
  -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);
  animation:veil .2s ease}
.sheet-panel{position:relative;width:100%;max-width:420px;
  background:var(--card);border-radius:18px 18px 0 0;
  padding:26px 22px calc(24px + env(safe-area-inset-bottom));
  text-align:center;animation:rise .26s cubic-bezier(.16,.9,.3,1);
  /* dvh with a vh fallback: on a phone the URL bar eats real viewport, and this
     sheet is exactly as tall as a short screen allows. */
  max-height:92vh;max-height:92dvh;overflow-y:auto;
  -webkit-overflow-scrolling:touch}
@keyframes veil{from{opacity:0}}
@keyframes rise{from{transform:translateY(14px)}}

.sheet-x{position:absolute;top:11px;right:11px;display:grid;place-items:center;
  width:38px;height:38px;border-radius:50%;border:0;background:none;
  cursor:pointer;color:var(--ink-3)}
.sheet-x:hover{background:#f2f4f7;color:var(--navy)}
.sheet-x svg{width:17px;height:17px;fill:none;stroke:currentColor;
  stroke-width:1.9;stroke-linecap:round}
.sheet-h{font-size:20px;color:var(--navy-deep);letter-spacing:-.022em}
.sheet-p{margin-top:8px;font-size:13.5px;color:var(--ink-2);line-height:1.55}

/* The code is the load-bearing element here, so it gets the space and a plain
   white field — anything printed behind a QR costs scan reliability. */
.sheet-qr{display:grid;place-items:center;margin:18px auto 0;padding:14px;
  width:min(232px,72vw);background:#fff;border:1px solid var(--line);
  border-radius:14px}
.sheet-qr img{width:100%;height:auto}

.sheet-vc{display:grid;gap:9px;margin:18px 0 0;text-align:left;
  padding:14px 15px;background:#fafbfc;border-radius:var(--r);
  border:1px solid var(--line-2)}
.sheet-vc>div{display:grid;grid-template-columns:52px 1fr;gap:10px;
  align-items:baseline}
.sheet-vc dt{font-size:11px;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink-3)}
.sheet-vc dd{margin:0;font-size:13.5px;color:var(--navy-deep);line-height:1.45}
.sheet-vc a{color:var(--navy);text-decoration:none}
.sheet-vc a:hover{text-decoration:underline;text-underline-offset:2px}

.sheet-dl{display:flex;align-items:center;justify-content:center;gap:9px;
  margin-top:14px;min-height:48px;border-radius:var(--r-s);
  background:var(--navy);color:#fff;text-decoration:none;
  font-size:14.5px;font-weight:700;letter-spacing:-.008em;
  transition:background .18s ease}
.sheet-dl:hover{background:var(--navy-deep)}
.sheet-note{margin-top:11px;font-size:11.5px;color:var(--ink-3);line-height:1.5}

/* ═══ Tablet ═══════════════════════════════════════════════════════════════ */
@media (min-width:640px){
  :root{--gut:32px}
  .pf-full{display:inline}
  .pf-short{display:none}
  .acts{grid-template-columns:repeat(4,1fr)}
  .act-1{grid-column:span 1}
  .triad{grid-template-columns:repeat(3,1fr);gap:14px}
  .proj-card{display:grid;grid-template-columns:1.15fr 1fr;align-items:stretch}
  .proj-media{display:flex;align-items:center}
  .proj-body{padding:24px 26px;align-self:center}
  .cred{grid-template-columns:1fr 1.15fr;gap:34px;padding:26px 26px 28px}
  .cta{display:grid;grid-template-columns:1fr auto;gap:26px;align-items:center;
    padding:26px 28px}
  .cta-t{font-size:17px;max-width:46ch}
  .cta-acts{grid-auto-flow:column;margin-top:0}

  .sheet{align-items:center}
  .sheet-panel{border-radius:18px;padding:30px 28px;max-width:400px;
    animation:pop .22s cubic-bezier(.16,.9,.3,1)}
  @keyframes pop{from{transform:scale(.97);opacity:.4}}
}

/* ═══ Desktop: identity left, portrait right ══════════════════════════════ */
@media (min-width:900px){
  :root{--gut:40px}
  body{font-size:16.5px}
  /* Card-on-page: the whole thing reads as one physical card, which is the
     metaphor the brief asks for. */
  main{max-width:1120px;margin:0 auto;padding:26px 0 0}
  .bar{max-width:1120px;margin:0 auto;padding:16px var(--gut)}

  .hero{grid-template-columns:1fr 340px;gap:52px;align-items:center;
    padding:14px var(--gut) 6px}
  /* The clip is 7:12 (0.583) — very tall. At full ratio beside a shorter text
     column it left a dead band above and below the copy, so the frame is 3:4
     throughout, biased upward so the head keeps its headroom. Here it fills its
     grid track instead of the phone's fixed width. */
  .twin-frame{width:auto}
  /* Back to full size: the pill was trimmed to suit the 244px phone frame. */
  .twin-play{left:12px;bottom:12px;gap:9px;padding:8px 15px 8px 8px}
  .twin-icon{width:28px;height:28px}
  .twin-icon svg{width:12px;height:12px}
  .twin-label{font-size:13.5px}
  /* Portrait is second visually but first in the DOM — order restores the
     designed desktop layout without hurting the mobile reading order. */
  .twin{order:2}
  .idn{order:1}
  h1{font-size:56px}
  .role{font-size:18px;margin-top:11px}
  /* 52ch, not 54: at 54 the zh lede pushed "产品。" onto a line of its own. Set
     against the actual translated copy, not guessed. */
  .lede{font-size:16.5px;margin-top:15px;max-width:52ch}
  .acts{max-width:600px}
  .triad{padding-top:34px}
  .card{padding:22px 22px 24px}
  .cred,.cta{margin-left:var(--gut);margin-right:var(--gut)}
  .foot{padding-left:var(--gut);padding-right:var(--gut)}
}

@media (min-width:1100px){
  .hero{grid-template-columns:1fr 372px;gap:64px}
}

/* Respect the OS setting — the only motion here is a fade and a 1px lift, but
   they still get switched off. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms!important;
    transition-duration:.01ms!important}
  .act:hover,.twin-play:hover{transform:none}
}

/* Print: someone will PDF this card. Drop the video control and dark panels. */
@media print{
  body{background:#fff}
  .bar,.twin-play,.twin-sound,.skip,.sheet{display:none}
  .cta{background:#fff;color:var(--ink);border:1px solid var(--line)}
  .cta .act,.cta .act-1{color:var(--ink);border-color:var(--line);background:#fff}
  .card,.cred,.twin-frame{box-shadow:none}
}
