/* ── Boris Tsang portfolio — shared styles ─────────────────────────────
   Design system. Apple-grade spatial rhythm:
   - 8px space scale (nothing off-grid)
   - modular type scale, tight tracking on display, open on labels
   - tokenised palette; dark theme redefines tokens, never components
   ---------------------------------------------------------------------- */

:root{
  /* Space scale — 8px base, used for every gap / padding / margin */
  --s-1:4px;  --s-2:8px;  --s-3:12px; --s-4:16px; --s-5:24px;
  --s-6:32px; --s-7:48px; --s-8:64px; --s-9:96px; --s-10:128px;

  /* Section rhythm */
  --section-y:112px;         /* vertical padding between sections   */
  --section-x:40px;          /* page gutter                          */
  --measure:960px;           /* content column                       */
  --measure-read:820px;      /* long-form reading column             */
  --radius:18px;
  --radius-sm:12px;

  /* Type scale (px) */
  --t-display:80px; --t-h1:52px; --t-h2:38px; --t-h3:26px;
  --t-lg:21px; --t-body:17px; --t-sm:15px; --t-xs:13px; --t-2xs:12px;

  /* Neutrals — warm-biased light ground, cool-blue-biased dark */
  --bg:#fbfbfa;
  --surface:#ffffff;
  --ink:#1d1d1f;         /* primary text            */
  --ink-2:#5f5f66;       /* secondary text          */
  --ink-3:#8a8a90;       /* muted / captions        */
  --hair:#eae8e3;        /* hairline borders        */
  --hair-2:#f2f1ee;      /* faint inner rules       */
  --accent:#3b6fd6;
  --accent-strong:#2c5bb8;
  --accent-soft:#e7eefc; /* accent tint background  */

  color-scheme:light;
}
/* Dark redefines tokens only — components never change. theme.js (in <head>)
   stamps data-theme before first paint; without JS the site stays light. */
:root[data-theme="dark"]{
  --bg:#0e1013; --surface:#161a20;
  --ink:#e8e8ed; --ink-2:#a3a8b0; --ink-3:#6f757e;
  --hair:#262b32; --hair-2:#20242b;
  --accent:#7aa5f2; --accent-strong:#8fb4f6; --accent-soft:#1a2740;
  color-scheme:dark;
}

*{box-sizing:border-box;}
html{scroll-behavior:smooth;overflow-x:clip;}
body{
  margin:0;
  overflow-x:clip;   /* full-bleed hero effect layers must never cause sideways scroll */
  background:var(--bg);
  color:var(--ink);
  font-family:Manrope,sans-serif;
  font-size:var(--t-body);
  line-height:1.5;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  transition:background .4s ease,color .4s ease;
}

/* Animations */
@keyframes heroIn{from{opacity:0;transform:translateY(18px);}to{opacity:1;transform:translateY(0);}}
@keyframes revealUp{from{opacity:0;transform:translateY(56px) scale(.98);}to{opacity:1;transform:translateY(0) scale(1);}}
@media(prefers-reduced-motion:reduce){*{animation:none !important;}}

/* Scroll-reveal fallback — browsers without CSS scroll-driven animations
   (Firefox, Safari) play the same reveal once per element instead: theme.js
   stamps .no-st on <html>, then .in on each element as it scrolls into view.
   Wrapped in no-preference so reduced-motion users never get hidden content. */
@media(prefers-reduced-motion:no-preference){
  html.no-st :is(.reveal-up,.reveal-left,.scroll-scale,.proj-card,.domain,.lang,.wcard,.wu-card){animation:none;opacity:0;}
  html.no-st :is(.reveal-up,.reveal-left,.scroll-scale,.proj-card,.domain,.lang,.wcard,.wu-card).in{
    animation:revealUp .7s cubic-bezier(.22,1,.36,1) both;
    animation-delay:calc(var(--i,0) * .07s);
  }
}

/* Focus ring — visible for keyboard users on every interactive element */
a:focus-visible,button:focus-visible{
  outline:2px solid var(--accent);
  outline-offset:3px;
  border-radius:6px;
}

/* ── Section primitives ─────────────────────────────────────────────── */
.section{padding:var(--section-y) var(--section-x);border-top:1px solid var(--hair);overflow:clip;scroll-margin-top:72px;}
.measure{max-width:var(--measure);margin:0 auto;}
.eyebrow{
  font:600 var(--t-xs)/1 Manrope,sans-serif;
  letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);
}

/* ── Nav ───────────────────────────────────────────────────────────── */
.nav{
  position:sticky;top:0;z-index:50;height:56px;
  display:flex;align-items:center;justify-content:space-between;
  padding:0 var(--section-x);
  background:color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter:saturate(180%) blur(16px);
  -webkit-backdrop-filter:saturate(180%) blur(16px);
  border-bottom:1px solid var(--hair);
  transition:transform .35s cubic-bezier(.4,0,.2,1);
}
.nav.hidden{transform:translateY(-100%);}
.nav-brand{text-decoration:none;display:flex;align-items:baseline;gap:var(--s-2);}
.nav-brand b{font:700 var(--t-sm)/1 Manrope,sans-serif;letter-spacing:-.01em;color:var(--ink);}
.nav-brand span{font:500 var(--t-2xs)/1 Manrope,sans-serif;color:var(--ink-3);}
.nav-links{display:flex;align-items:center;gap:var(--s-6);}
.nav-links a{text-decoration:none;font:500 var(--t-xs)/1 Manrope,sans-serif;color:var(--ink-2);transition:color .25s;}
.nav-links a:hover{color:var(--accent);}
.nav-links a.active,.nav-links .active{font:600 var(--t-xs)/1 Manrope,sans-serif;color:var(--accent);}
.theme-btn{
  width:36px;height:36px;border-radius:10px;
  border:1px solid var(--hair);background:transparent;color:var(--ink-2);
  display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;
  transition:border-color .25s,color .25s,box-shadow .25s,transform .25s;
}
.theme-btn:hover{border-color:var(--accent);color:var(--accent);box-shadow:0 6px 16px rgba(59,111,214,.22);transform:translateY(-2px);}

/* ── Contact footer ────────────────────────────────────────────────── */
.contact{padding:var(--section-y) var(--section-x) var(--s-9);text-align:center;border-top:1px solid var(--hair);scroll-margin-top:72px;}
.contact-label{font:600 var(--t-xs)/1 Manrope,sans-serif;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);margin-bottom:var(--s-6);}
.contact-icons{display:flex;justify-content:center;gap:var(--s-6);margin-bottom:var(--s-6);}
.contact-icons a{display:inline-flex;opacity:1;transition:opacity .2s,transform .2s;}
.contact-icons a:hover{opacity:.55;transform:translateY(-2px);}
.contact-copy{font:500 var(--t-2xs)/1 Manrope,sans-serif;color:var(--ink-3);}

/* ── Bottom blur veil ──────────────────────────────────────────────────
   Fixed gradient blur pinned to the bottom of the viewport: content softly
   dissolves at the bottom edge, a gentle "there's more below" cue. It hides
   itself when the page is scrolled to the very bottom (nothing left to hint
   at) via the .at-end class set in theme.js. */
.scroll-veil{
  position:fixed;left:0;right:0;bottom:0;height:90px;z-index:40;
  pointer-events:none;
  backdrop-filter:blur(4px);
  -webkit-backdrop-filter:blur(4px);
  -webkit-mask-image:linear-gradient(to top,#000 0%,#000 30%,transparent 100%);
  mask-image:linear-gradient(to top,#000 0%,#000 30%,transparent 100%);
  background:linear-gradient(to top,var(--bg) 0%,color-mix(in srgb,var(--bg) 55%,transparent) 45%,transparent 100%);
  opacity:1;transition:opacity .4s ease;
}
.scroll-veil.at-end{opacity:0;}
@media(prefers-reduced-motion:reduce){.scroll-veil{display:none;}}

@media(max-width:820px){
  :root{--section-y:72px;--section-x:24px;}
}
@media(max-width:640px){
  :root{--section-x:22px;}
}
