/* ============================================================
   Quang Nhat Duong — Portfolio
   Datasheet, not brochure. IBM Plex Sans + Mono.

   The page is built from measurements, so it is styled like a
   measurement: labelled values, units always shown, every number
   within reach of the source that produced it.
   ============================================================ */

:root{
  /* ---- colour ----------------------------------------------------
     Every pair below is checked against WCAG AA in tools/contrast.py.
     --faint previously failed at ~2.6:1 and is now a real text grey. */
  --bg:#FAF9F6;
  --surface:#FFFFFF;
  --ink:#17191C;
  --sub:#4E535B;
  --faint:#6A7079;
  --line:#E4E1DA;
  --line-2:#CFCAC0;
  --accent:#2445B8;
  --accent-hover:#163090;
  --good:#1F6B4C;
  --warn:#8A5410;

  --sans:'IBM Plex Sans',system-ui,-apple-system,'Segoe UI',sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;

  /* ---- type scale (1.25 modular, fluid only where it must be) ---- */
  --fs-mono-xs:11.5px;
  --fs-mono-sm:12.5px;
  --fs-mono:13.5px;
  --fs-sm:14.5px;
  --fs-base:16px;
  --fs-md:17.5px;
  --fs-lg:20px;
  --fs-xl:clamp(21px,2.6vw,26px);
  --fs-2xl:clamp(26px,4vw,34px);
  --fs-display:clamp(40px,6.4vw,64px);

  /* ---- spacing scale (8px base) ---- */
  --s1:4px;  --s2:8px;  --s3:12px; --s4:16px; --s5:24px;
  --s6:32px; --s7:48px; --s8:64px; --s9:96px;
  --sec-y:clamp(48px,6.5vw,80px);

  --maxw:1080px;
  --gutter:clamp(20px,5vw,56px);
  --radius:4px;
}

@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    --bg:#101215; --surface:#171A1E; --ink:#ECEEF1; --sub:#AEB4BC;
    --faint:#8B929B; --line:#262A30; --line-2:#3A414A;
    --accent:#8FADFF; --accent-hover:#B3C6FF;
    --good:#6FC79E; --warn:#E3AE72;
  }
}
:root[data-theme="dark"]{
  --bg:#101215; --surface:#171A1E; --ink:#ECEEF1; --sub:#AEB4BC;
  --faint:#8B929B; --line:#262A30; --line-2:#3A414A;
  --accent:#8FADFF; --accent-hover:#B3C6FF;
  --good:#6FC79E; --warn:#E3AE72;
}

*{box-sizing:border-box;}
html{scroll-behavior:smooth;}
@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto;}
  *,*::before,*::after{animation-duration:.001ms !important;transition-duration:.001ms !important;}
}

body{
  margin:0;background:var(--bg);color:var(--ink);
  font-family:var(--sans);font-size:var(--fs-base);line-height:1.6;
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
}
img{max-width:100%;display:block;}
[hidden]{display:none !important;}
a{color:var(--accent);text-decoration:none;}
a:hover{color:var(--accent-hover);}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:2px;}
/* The top bar is sticky and 60px tall, so an anchor jump must stop short of it
   or the heading it was aimed at lands underneath. */
[id]{scroll-margin-top:76px;}

/* Keyboard users should not have to tab the whole nav to reach content. */
.skip{position:absolute;left:-9999px;top:0;background:var(--surface);color:var(--ink);
      padding:var(--s3) var(--s4);border:1px solid var(--line-2);border-radius:var(--radius);z-index:100;}
.skip:focus{left:var(--s4);top:var(--s2);}

.wrap{max-width:var(--maxw);margin-inline:auto;padding-inline:var(--gutter);}
.mono{font-family:var(--mono);}
.eyebrow{font-family:var(--mono);font-size:var(--fs-mono-sm);letter-spacing:.2em;
         text-transform:uppercase;color:var(--faint);}

/* ---- top bar ---------------------------------------------------- */
.topbar{position:sticky;top:0;z-index:40;background:color-mix(in srgb,var(--bg) 90%,transparent);
        backdrop-filter:blur(8px);border-bottom:1px solid var(--line);}
.topbar-inner{display:flex;align-items:center;justify-content:space-between;gap:var(--s4);height:60px;}
.brand{font-family:var(--mono);font-size:var(--fs-mono);font-weight:500;color:var(--ink);}
.brand .d{color:var(--accent);}
.topnav{display:flex;gap:var(--s5);}
.topnav a{font-family:var(--mono);font-size:var(--fs-mono-sm);color:var(--sub);}
.topnav a:hover{color:var(--ink);}
.controls{display:flex;align-items:center;gap:var(--s3);}
.icon-btn{font-family:var(--mono);font-size:var(--fs-mono-sm);color:var(--sub);
          background:none;border:0;cursor:pointer;padding:var(--s1);}
.icon-btn:hover{color:var(--ink);}
@media (max-width:760px){ .topnav{display:none;} }

/* ---- hero ------------------------------------------------------- */
.hero{padding-block:clamp(48px,8vw,88px) var(--s7);}
.hero-grid{display:grid;grid-template-columns:1fr 260px;gap:clamp(32px,6vw,72px);align-items:center;}
.hero-name{font-size:var(--fs-display);line-height:1.02;letter-spacing:-.028em;
           font-weight:600;margin:var(--s5) 0;}
.hero-lead{font-size:var(--fs-lg);line-height:1.5;color:var(--sub);max-width:50ch;margin:0 0 var(--s5);}
.hero-meta{font-family:var(--mono);font-size:var(--fs-mono-sm);color:var(--faint);margin-bottom:var(--s5);}
.hero-meta b{color:var(--ink);font-weight:500;}
.hero-links{display:flex;flex-wrap:wrap;gap:var(--s5);font-family:var(--mono);font-size:var(--fs-mono);}
.hero-portrait{justify-self:center;}
.hero-portrait img{width:260px;height:312px;object-fit:cover;object-position:center;
                   border:1px solid var(--line-2);border-radius:var(--radius);}
@media (max-width:760px){
  .hero-grid{grid-template-columns:1fr;gap:var(--s6);}
  .hero-portrait{justify-self:start;order:-1;}
  .hero-portrait img{width:140px;height:168px;}
}

/* ---- evidence strip --------------------------------------------
   Four checkable facts directly under the name. A reader who leaves
   after eight seconds has still seen something verifiable. */
.evidence{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
          border-top:1px solid var(--line);border-bottom:1px solid var(--line);}
.ev{padding:var(--s5) var(--s5) var(--s5) 0;border-left:1px solid var(--line);
    padding-left:var(--s5);}
.evidence .ev:first-child{border-left:0;padding-left:0;}
.ev-v{font-family:var(--mono);font-size:var(--fs-lg);font-weight:500;color:var(--ink);
      letter-spacing:-.01em;line-height:1.2;}
.ev-k{font-size:var(--fs-sm);color:var(--sub);margin-top:var(--s2);}
.ev a{border-bottom:1px solid transparent;}
.ev a:hover{border-bottom-color:currentColor;}
@media (max-width:820px){
  .evidence{grid-template-columns:repeat(2,minmax(0,1fr));}
  .ev{border-left:0;padding-left:0;padding-block:var(--s4);}
  .evidence .ev:nth-child(n+3){border-top:1px solid var(--line);}
}
@media (max-width:460px){
  .evidence{grid-template-columns:1fr;}
  .ev{padding-block:var(--s3);}
  .ev-k{margin-top:var(--s1);}
  .evidence .ev:nth-child(n+2){border-top:1px solid var(--line);}
}

/* ---- sections --------------------------------------------------- */
.section{padding-block:var(--sec-y);border-top:1px solid var(--line);}
.section:first-of-type{border-top:0;}
.sec-label{font-family:var(--mono);font-size:var(--fs-mono-sm);letter-spacing:.16em;
           text-transform:uppercase;color:var(--faint);font-weight:500;margin:0 0 var(--s6);}

/* ---- figures: the work, not a description of the work ----------- */
.figure{margin:0 0 var(--s5);border:1px solid var(--line);border-radius:var(--radius);
        overflow:hidden;background:var(--surface);}
.figure video,.figure img{width:100%;height:auto;display:block;}
.figure .chart{padding:var(--s4) var(--s3) var(--s2);}
.figure-cap{font-size:var(--fs-sm);color:var(--sub);padding:var(--s3) var(--s4);
            border-top:1px solid var(--line);line-height:1.5;}
.figure-cap b{color:var(--ink);font-weight:600;}

/* ---- case-study cards ------------------------------------------- */
.case + .case{margin-top:var(--s7);padding-top:var(--s7);border-top:1px solid var(--line);}
.case-head{display:flex;justify-content:space-between;align-items:baseline;
           gap:var(--s5);margin-bottom:var(--s3);}
.case-title{font-size:var(--fs-xl);font-weight:600;letter-spacing:-.015em;line-height:1.2;}
.case-year{font-family:var(--mono);font-size:var(--fs-mono-sm);color:var(--faint);white-space:nowrap;}
.case-lede{color:var(--sub);font-size:var(--fs-md);max-width:70ch;margin:0 0 var(--s5);}

/* metric row — the datasheet element */
.metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
         gap:1px;background:var(--line);border:1px solid var(--line);
         border-radius:var(--radius);overflow:hidden;margin-bottom:var(--s5);}
.metric{background:var(--bg);padding:var(--s4) var(--s5);}
.m-k{font-family:var(--mono);font-size:var(--fs-mono-xs);letter-spacing:.1em;
     text-transform:uppercase;color:var(--faint);}
.m-v{font-family:var(--mono);font-size:var(--fs-lg);font-weight:500;
     color:var(--ink);margin-top:var(--s1);letter-spacing:-.01em;}
.m-v.good{color:var(--good);}
.m-v.warn{color:var(--warn);}
.m-n{font-size:var(--fs-sm);color:var(--sub);margin-top:var(--s2);line-height:1.45;}
@media (max-width:760px){ .metrics{grid-template-columns:1fr;} }

/* the caveat block — a demo number never appears without its scope */
.caveat{border-left:2px solid var(--line-2);padding:var(--s1) 0 var(--s1) var(--s4);
        margin:0 0 var(--s5);color:var(--sub);font-size:var(--fs-sm);max-width:70ch;}
.caveat b{color:var(--ink);font-weight:600;}

.decision{font-size:var(--fs-sm);color:var(--sub);max-width:70ch;margin:0 0 var(--s5);}
.decision b{color:var(--ink);font-weight:600;}

.chips{display:flex;flex-wrap:wrap;gap:var(--s2);margin-bottom:var(--s4);}
.chip{font-family:var(--mono);font-size:var(--fs-mono-xs);color:var(--sub);
      border:1px solid var(--line-2);border-radius:var(--radius);padding:var(--s1) var(--s2);}
.sources{display:flex;flex-wrap:wrap;gap:var(--s5);font-family:var(--mono);font-size:var(--fs-mono);}
@media (max-width:600px){ .case-head{flex-direction:column;gap:var(--s1);} }

/* ---- secondary projects ----------------------------------------- */
.mini-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s6) var(--s8);
           margin-top:var(--s7);padding-top:var(--s7);border-top:1px solid var(--line);}
.mini h3{font-size:var(--fs-md);font-weight:600;margin:0 0 var(--s2);}
.mini p{color:var(--sub);font-size:var(--fs-sm);margin:0 0 var(--s3);}
.mini .sources{font-size:var(--fs-mono-sm);}
@media (max-width:680px){ .mini-grid{grid-template-columns:1fr;gap:var(--s6);} }

/* ---- focus band (compressed) ------------------------------------ */
.band{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--s5) var(--s6);}
.band-item h3{font-size:var(--fs-sm);font-weight:600;margin:0 0 var(--s1);}
.band-item p{color:var(--sub);font-size:var(--fs-sm);margin:0;line-height:1.5;}
.band-no{font-family:var(--mono);font-size:var(--fs-mono-xs);color:var(--accent);
         display:block;margin-bottom:var(--s2);}
@media (max-width:820px){ .band{grid-template-columns:repeat(2,minmax(0,1fr));} }
@media (max-width:480px){ .band{grid-template-columns:1fr;} }

/* ---- experience ledger ------------------------------------------ */
.ledger{display:flex;flex-direction:column;}
.entry{display:grid;grid-template-columns:176px 1fr;gap:var(--s6);
       padding:var(--s5) 0;border-top:1px solid var(--line);}
.entry:first-child{border-top:0;padding-top:0;}
.entry .when{font-family:var(--mono);font-size:var(--fs-mono-sm);color:var(--faint);}
.entry .role{font-size:var(--fs-md);font-weight:600;}
.entry .org{font-family:var(--mono);font-size:var(--fs-mono-sm);color:var(--accent);margin:var(--s1) 0 var(--s2);}
.entry .body{color:var(--sub);font-size:var(--fs-sm);max-width:66ch;}
.entry .body b{color:var(--ink);font-weight:600;}
@media (max-width:600px){ .entry{grid-template-columns:1fr;gap:var(--s1);} }

/* ---- background: awards + education side by side ---------------- */
.bg-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--s8);}
.bg-col h3{font-family:var(--mono);font-size:var(--fs-mono-xs);letter-spacing:.14em;
           text-transform:uppercase;color:var(--faint);font-weight:500;margin:0 0 var(--s4);}
.award{display:flex;justify-content:space-between;gap:var(--s4);align-items:baseline;
       border-bottom:1px solid var(--line);padding:var(--s3) 0;}
.award:first-of-type{border-top:1px solid var(--line);}
.award .t{font-weight:600;font-size:var(--fs-sm);}
.award .y{font-family:var(--mono);font-size:var(--fs-mono-xs);color:var(--faint);white-space:nowrap;}
.edu{border-top:1px solid var(--line);padding-top:var(--s3);}
.edu .when{font-family:var(--mono);font-size:var(--fs-mono-sm);color:var(--faint);}
.edu .inst{font-size:var(--fs-md);font-weight:600;margin-top:var(--s1);}
.edu .deg{color:var(--sub);font-size:var(--fs-sm);margin-top:var(--s1);}
@media (max-width:760px){ .bg-grid{grid-template-columns:1fr;gap:var(--s7);} }

/* ---- contact ---------------------------------------------------- */
.contact-line{font-size:var(--fs-2xl);font-weight:600;letter-spacing:-.02em;
              max-width:24ch;margin:0 0 var(--s5);line-height:1.15;}
.contact-note{color:var(--sub);font-size:var(--fs-sm);max-width:60ch;margin:0 0 var(--s5);}
.links{display:flex;flex-wrap:wrap;gap:var(--s5);font-family:var(--mono);font-size:var(--fs-mono);}

/* ---- footer ----------------------------------------------------- */
.foot{border-top:1px solid var(--line);}
.foot-inner{display:flex;justify-content:space-between;gap:var(--s4);flex-wrap:wrap;
            padding-block:var(--s5) var(--s7);font-family:var(--mono);
            font-size:var(--fs-mono-xs);color:var(--faint);}

/* ============================================================
   Motion

   One rule governs everything below: the *resting* state is the
   state the element is in when nothing has run. Animation only
   delays arrival; it never creates it. So a page with JavaScript
   disabled, a browser that fails to fire IntersectionObserver, and
   a reader with prefers-reduced-motion all see a complete page -
   none of them see blank space where a paragraph should be.

   That is why the hidden state is applied by `html.js [data-rise]`
   rather than by `[data-rise]` alone: only a script that has
   already run can add the `js` class, so the only way to be hidden
   is for the machinery that un-hides you to be running.
   ============================================================ */

/* Entrance: used by the hero on load and by sections on scroll. */
html.js [data-rise]{opacity:0;transform:translateY(12px);}
html.js [data-rise].is-in{
  opacity:1;transform:none;
  transition:opacity .45s cubic-bezier(.22,.61,.36,1),
             transform .45s cubic-bezier(.22,.61,.36,1);
  transition-delay:var(--rise-delay,0ms);
}

/* Chart: bars grow from the axis, then the reference ticks, the gap spans
   and the value labels land on top of them.

   transform-box:fill-box is load-bearing. Without it an SVG element's
   transform-origin resolves against the nearest SVG viewport rather than the
   element's own box, so `left center` is the left edge of the *chart*, not of
   the bar - every bar grows from the wrong place and sweeps across the row
   labels on its way out. */
html.js .chart [class~="g-bar"]{
  transform:scaleX(0);transform-box:fill-box;transform-origin:left center;
}
html.js .chart .g-tick,
html.js .chart .g-span,
html.js .chart text[font-family]{opacity:0;}
html.js .chart.is-in [class~="g-bar"]{
  transform:none;transition:transform .7s cubic-bezier(.22,.61,.36,1);
  transition-delay:calc(var(--i,0) * 70ms);
}
html.js .chart.is-in .g-tick,
html.js .chart.is-in .g-span{
  opacity:1;transition:opacity .3s ease;
  transition-delay:calc(420ms + var(--i,0) * 70ms);
}
html.js .chart.is-in text[font-family]{opacity:1;transition:opacity .4s ease .5s;}

/* Theme change: cross-fade rather than snap. Scoped to the properties
   that actually change, so it never lands on transform or opacity. */
html.theme-shift,html.theme-shift *{
  transition:background-color .22s ease,color .22s ease,
             border-color .22s ease,fill .22s ease,stroke .22s ease !important;
}

/* Micro-interactions. */
.hero-links a,.sources a,.links a,.ev a{
  position:relative;transition:color .18s ease;
}
.hero-links a::after,.sources a::after,.links a::after{
  content:"";position:absolute;left:0;right:100%;bottom:-3px;height:1px;
  background:currentColor;transition:right .28s cubic-bezier(.22,.61,.36,1);
}
.hero-links a:hover::after,.sources a:hover::after,.links a:hover::after{right:0;}
.case{transition:transform .3s cubic-bezier(.22,.61,.36,1);}
.chip{transition:border-color .2s ease,color .2s ease;}
.chip:hover{border-color:var(--faint);color:var(--ink);}
.figure{transition:border-color .3s ease;}
.case:hover .figure{border-color:var(--line-2);}
.topbar{transition:box-shadow .25s ease,border-color .25s ease;}
.topbar.is-stuck{box-shadow:0 1px 16px rgba(0,0,0,.09);}
:root[data-theme="dark"] .topbar.is-stuck,
:root:not([data-theme="light"]) .topbar.is-stuck{box-shadow:0 1px 20px rgba(0,0,0,.5);}

/* Everything above is opt-in motion; this turns all of it off and
   leaves the resting state, which is the finished page. */
@media (prefers-reduced-motion: reduce){
  html.js [data-rise],html.js [data-rise].is-in{opacity:1;transform:none;transition:none;}
  html.js .chart [class~="g-bar"]{transform:none;}
  html.js .chart .g-tick,html.js .chart .g-span,
  html.js .chart text[font-family]{opacity:1;}
  .hero-links a::after,.sources a::after,.links a::after{display:none;}
}
