/* ============================================================================
 *  Zheming Yin — homepage
 *  Two static themes (light / dark) selected by [data-theme] on <html>.
 *  Nothing is interpolated at runtime, so scrolling stays cheap.
 * ==========================================================================*/

/* Local-only Han subset: Latin and digits continue to use the SF system font.
   No font downloads; unavailable Apple fonts fall back to the visitor's OS. */
@font-face{
  font-family:"Homepage Han";font-style:normal;font-weight:300;font-display:swap;
  src:local("PingFangSC-Light"),local("PingFang SC Light");
  unicode-range:U+3400-4DBF,U+4E00-9FFF,U+F900-FAFF,U+20000-323AF;
}
@font-face{
  font-family:"Homepage Han";font-style:normal;font-weight:400;font-display:swap;
  src:local("PingFangSC-Regular"),local("PingFang SC Regular");
  unicode-range:U+3400-4DBF,U+4E00-9FFF,U+F900-FAFF,U+20000-323AF;
}
@font-face{
  font-family:"Homepage Han";font-style:normal;font-weight:500;font-display:swap;
  src:local("PingFangSC-Medium"),local("PingFang SC Medium");
  unicode-range:U+3400-4DBF,U+4E00-9FFF,U+F900-FAFF,U+20000-323AF;
}
@font-face{
  font-family:"Homepage Han";font-style:normal;font-weight:600;font-display:swap;
  src:local("PingFangSC-Semibold"),local("PingFang SC Semibold");
  unicode-range:U+3400-4DBF,U+4E00-9FFF,U+F900-FAFF,U+20000-323AF;
}

:root{
  --bg:#ffffff;  --bg-soft:#fafbfc; --surf:#ffffff;
  --line:#e4e7ec; --line-soft:#eef0f3;
  --text:#111418; --text2:#4c5763; --text3:#8b939e;
  --accent:#1a56b8; --accent-ink:#ffffff;
  --chip:#f2f4f6; --chip-ink:#3c454f;
  --text-meta:#39424e;    /* period + org in Experiences */
  /* A page-coloured veil softens content without a bright or tinted sheen. */
  --glass:rgba(255,255,255,.72);
  --glass-fx:blur(8px);
  --glass-fade:56px;      /* fade the whole glass layer below the headers */
  --rail:#d7dbe1;
  --rail-on:#f3a3c6;      /* light pink progress bar in both themes */
  --rail-label-on:#f3a3c6; /* light pink active text in both themes */

  /* University brand colours, used on their names in the About paragraph.
     Print specs converted to sRGB, so they are close rather than exact:
       --c-hku      PANTONE 3308 C   (HKU)
       --c-neu      C100 M70 Y0 K0   (Northeastern University)
       --c-bosch    Bosch red
       --c-ustutt   PANTONE 432 U    (University of Stuttgart)
     The dark-theme values below are lightened: 3308 C and C100 M70 are far
     too dark to read on a near-black page. */
  --c-hku:#004538;
  --c-neu:#0038a8;
  --c-ustutt:#676c73;
  --c-bosch:#ED0007;
  --c-campus-red:#b31b1b;
  --shadow:0 1px 2px rgba(16,24,40,.05);

  --ff:"Homepage Han",-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display",system-ui,"Segoe UI","Helvetica Neue",Arial,"PingFang SC","Microsoft YaHei",sans-serif;
  --ff-m:var(--ff); /* Dates, indices and counts use SF Pro too, not SF Mono. */

  --maxw:1120px;
  --pad:clamp(20px,5vw,60px);
  --head-h:60px;          /* height of the sticky module header */
  --badge-h:22px;         /* shared by publication badges and experience tags */
  --r-s:6px; --r-m:10px; --r-l:14px;
}

[data-theme="dark"]{
  --bg:#0d1117;  --bg-soft:#10161e; --surf:#141b24;
  --line:#232c38; --line-soft:#1b232d;
  --text:#e6eaf0; --text2:#9aa4b2; --text3:#6d7684;
  --accent:#79aaff; --accent-ink:#0d1117;
  --chip:#1a2230; --chip-ink:#b9c3d1;
  --text-meta:#b6c0ce;
  --glass:rgba(13,17,23,.72);
  --rail:#2b3543;
  --rail-on:#f3a3c6;
  --rail-label-on:#f3a3c6;
  --c-hku:#5cb79c;
  --c-neu:#7ea6f2;
  --c-ustutt:#a5abb4;
  --c-bosch:#ff6b70;
  --shadow:0 1px 2px rgba(0,0,0,.35);
}

*,*::before,*::after{box-sizing:border-box}
[hidden]{display:none !important}

/* Keep overflow control on the viewport. Putting overflow-x:hidden on body
   also makes its vertical overflow auto, which can trap sticky descendants
   in a non-scrolling ancestor (especially after the loading screen unlocks).
   Headings and the shared glass must follow the document at every width. */
html{-webkit-text-size-adjust:100%;overflow-x:hidden}
@supports (overflow:clip){html{overflow-x:clip}}

body{
  margin:0;
  font-family:var(--ff);
  font-optical-sizing:auto;
  font-size:15px;
  line-height:1.6;
  color:var(--text);
  background:var(--bg);
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
}
body.no-scroll{overflow:hidden}

/* Keep layout measurable and eager image requests active behind one gate. */
html.is-loading{overflow:hidden}
/* Nothing inside the page may widen the phone layout viewport (e.g. the
   hidden footer capsule): otherwise the loader and page render off-centre.
   clip (not hidden) keeps sticky headers working and adds no scroll box. */
#main{overflow-x:clip}
html.is-loading body > :not(#pageLoader){visibility:hidden!important}
.page-loader{
  position:fixed;inset:0;z-index:10000;background:#fff;color:#47382e;color-scheme:light;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;
  padding:24px;text-align:center;
}
.page-loader p{margin:0}
.coffee-title{font-size:13px;font-weight:500;color:#9a623c}
#pageLoaderText{font-size:11px;color:var(--rail-on);min-height:18px}
.page-loader progress{appearance:none;-webkit-appearance:none;width:min(180px,70vw);height:3px;border:0;border-radius:3px;overflow:visible;background:#fceef4;color:var(--rail-on);accent-color:var(--rail-on)}
.page-loader progress::-webkit-progress-inner-element{overflow:visible}
.page-loader progress::-webkit-progress-bar{background:#fceef4;border-radius:3px;overflow:visible}
.page-loader progress::-webkit-progress-value{background:var(--rail-on);border-radius:3px;box-shadow:0 0 4px 1px rgba(243,163,198,.16),0 0 8px 2px rgba(243,163,198,.06)}
.page-loader progress::-moz-progress-bar{background:var(--rail-on);border-radius:3px;box-shadow:0 0 4px 1px rgba(243,163,198,.16),0 0 8px 2px rgba(243,163,198,.06)}
.coffee-stage{position:relative;width:min(1000px,calc(100vw - 48px));height:165px;overflow:hidden;contain:layout paint}
.coffee-stage--3d{height:clamp(200px,34vh,300px)}
.coffee-video-scene{position:absolute;width:512px;height:384px;left:calc(50% - 256px);bottom:0;transform-origin:256px 384px;transition:transform .4s ease}
.coffee-video-scene video{display:block;width:512px;height:384px;background:#fff}
.coffee-frame-window{position:absolute;inset:0;overflow:hidden;background:#fff}
.coffee-frame-window img{position:absolute;width:2048px;height:1536px;max-width:none}
.coffee-served-row{position:absolute;inset:0;pointer-events:none;will-change:transform}
.coffee-served-row img{position:absolute;top:0;width:512px;height:384px;max-width:none}

a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline;text-underline-offset:2px}

/* ── links written with SITE.link() in js/content/en.js ──────────────────
   Default: the colour of the surrounding text, no underline, no bold — the
   underline only appears on hover. Every part is overridable per link
   (see js/core/links.js); change the defaults here to change them all. */
.lnk{
  color:var(--lnk-ink,inherit);
  font-weight:inherit;
  transition:color .15s;

  /* The underline is a background stripe, not text-decoration. An <img> is an
     atomic inline box: text-decoration is never drawn across one, so a link
     that starts with a crest would underline the words and skip the picture,
     making them look like two things. A background spans the whole anchor,
     crest included, so icon and text read — and hover — as one link. */
  background-image:linear-gradient(currentColor,currentColor);
  background-repeat:no-repeat;
  /* The padding is what holds the rule clear of both the descenders and the
     bottom of the crest, which hangs lower than the text; the rule then sits
     on the bottom edge of the padding box. Inline padding does not change
     the line height, so nothing shifts. */
  padding-bottom:.28em;
  background-position:0 100%;
  background-size:0 var(--lnk-rule,1.6px);
  transition:color .15s,background-size .16s ease;
  -webkit-box-decoration-break:clone;
          box-decoration-break:clone;   /* keep it on every line when it wraps */
}
/* `a:hover` above turns on a text-decoration underline. It has to be shut
   off here for BOTH states, or the browser's own rule draws a second, thinner
   line that stops at the crest — which is what made the underline look like
   two different weights. One stripe, one weight, crest included. */
.lnk,.lnk:hover,.lnk:focus-visible{text-decoration:none}
.lnk:hover,.lnk:focus-visible{background-size:100% var(--lnk-rule,1.6px)}
.lnk--ink   {--lnk-ink:var(--text)}
.lnk--accent{--lnk-ink:var(--accent)}

/* University names in their own brand colour. On a link, pass the variable
   through SITE.link's `color` option instead:
       SITE.link('HKU', href, { color: 'var(--c-hku)' })                    */
.c-hku   {color:var(--c-hku)}
.c-neu   {color:var(--c-neu)}
.c-ustutt{color:var(--c-ustutt)}
.lnk--u{background-size:100% var(--lnk-rule,1.6px)}
.lnk--b{font-weight:600}

/* a small crest or logo sitting just before the link text */
.lnk-ico{
  display:inline-block;
  /* a little taller than the words it sits in front of */
  width:1.34em;height:1.34em;
  object-fit:contain;
  vertical-align:-.32em;
  margin-right:.34em;
  border-radius:2px;
}
/* Keep Stuttgart's dark crest legible without changing its layout size. */
[data-theme="dark"] #aboutBody .lnk-ico[alt="Uni_Stuttgart"]{
  background:radial-gradient(circle closest-side at center,#fff 99%,transparent 100%);
  box-shadow:none;
}
/* the optional 🔗 marker */
.lnk-mark{
  display:inline-block;width:.82em;height:.82em;
  margin-left:.28em;vertical-align:-.05em;opacity:.55;
}
.lnk-mark svg{width:100%;height:100%}
img,video{max-width:100%;display:block}
h1,h2,h3,h4{margin:0;font-weight:600;line-height:1.3;letter-spacing:-.01em}
p{margin:0 0 .9em}
ul{margin:0;padding:0;list-style:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px}

.ic{display:inline-flex;width:15px;height:15px;flex:0 0 15px}
.ic svg{width:100%;height:100%}
.ic--sm{width:12px;height:12px;flex-basis:12px;opacity:.6}

/* ══════════════════════════════════════════════════════════════════════
   FIXED RIGHT COLUMN — theme switch + rail
   ══════════════════════════════════════════════════════════════════════ */
.side{
  position:fixed; right:clamp(8px,2vw,26px); top:50%;
  transform:translateY(-50%);
  z-index:60;
  display:flex; flex-direction:column; align-items:flex-end; gap:10px;
  max-width:calc(100vw - 12px);
}
/* Nothing in this column is frosted. The rail carries no backing at all;
   instead its text and ticks flip between dark and light to suit whatever is
   behind them — see core/backdrop.js and the .on-dark / .on-light rules.
   It deliberately stays visible at every viewport width and browser zoom. */

/* ── sun / moon switch ─────────────────────────────────────────────── */
.tsw{padding:0;line-height:0;border-radius:999px}
.tsw-track{
  position:relative; display:flex; flex-direction:column; align-items:center;
  justify-content:space-between;
  width:25px; height:46px; padding:5px 0;
  border-radius:999px;
  background:var(--chip);
  border:1px solid var(--line);
}
.tsw-ico{display:flex;width:12px;height:12px;position:relative;z-index:2}
.tsw-ico svg{width:100%;height:100%}
.tsw-ico--sun{color:#e0a13a}
.tsw-ico--moon{color:#8b93a1}
[data-theme="dark"] .tsw-ico--sun{color:#6b7280}
[data-theme="dark"] .tsw-ico--moon{color:#f5df9b}
.tsw-knob{
  position:absolute; top:2px; left:2px;
  width:19px; height:19px; border-radius:50%;
  background:var(--surf);
  box-shadow:0 1px 3px rgba(0,0,0,.16),
    0 0 8px 2px rgba(245,215,132,.22),
    0 0 18px 4px rgba(245,215,132,.10);
  transition:transform .2s ease;
  z-index:1;
}
:root:not([data-theme="dark"]) .tsw-knob{
  box-shadow:0 1px 3px rgba(0,0,0,.16),
    0 0 8px 2px rgba(235,188,70,.42),
    0 0 18px 4px rgba(245,210,110,.24);
}
[data-theme="dark"] .tsw-knob{transform:translateY(21px)}
@media (prefers-reduced-motion:reduce){ .tsw-knob{transition:none} }

/* ── progress rail ─────────────────────────────────────────────────── */
.rail{
  position:relative;display:flex;flex-direction:column;align-items:flex-end;
  gap:clamp(3px,1.15vh,9px);
}

/* The right padding is half the difference between the switch (25px) and the
   track (3px), which puts the two on the same vertical centre line. */
.rail-item{
  position:relative;display:flex;align-items:center;gap:8px;text-decoration:none;
  padding-right:11px;
}
.rail-item:hover{text-decoration:none}

.ri-track{
  position:relative;display:block;width:3px;
  height:clamp(18px,3.75vh,30px);border-radius:3px;
  background:var(--rail);
  overflow:visible; /* allow the active fill's glow beyond the thin track */
  /* Section changes settle immediately rather than relaying out the rail
     throughout a height animation while backdrop probes read its geometry. */
  transition:background-color .2s ease;
}
.ri-fill{
  position:absolute;left:0;right:0;top:0;height:100%;border-radius:3px;
  background:var(--text3);
  transform-origin:top center;transform:scaleY(0);
  /* No colour transition here on purpose. The fill changes colour when the
     section becomes the current one, and easing that means a visible grey
     flash before the pink — the current section should already be pink on
     the very first frame. (.ri-track still eases; nothing depends on it.) */
}
.rail-item.is-active .ri-track{height:clamp(28px,5.75vh,46px)}
/* The shadow follows the fill's existing scaleY progress, including zero.
   Small static shadows need no additional pointer/scroll listeners. */
.rail-item.is-active .ri-fill{
  background:var(--rail-on);
  box-shadow:0 0 4px 1px rgba(243,163,198,.30),
    0 0 8px 2px rgba(243,163,198,.12);
}
.rail-item.is-done  .ri-fill{background:var(--text3);opacity:.55}

/* labels stay hidden until the rail is hovered — the sticky section
   header already tells the reader where they are */
.ri-label{
  display:flex;align-items:baseline;gap:6px;white-space:nowrap;
  max-width:calc(100vw - 48px);overflow:hidden;text-overflow:ellipsis;
  font-size:11px;color:var(--text2);
  padding:2px 0;
  transition:color .2s ease;
  opacity:0;visibility:hidden;transform:translateX(4px);
  transition:opacity .18s ease,transform .18s ease,visibility 0s linear .18s;
  pointer-events:none;
}
.ri-label b{font-family:var(--ff-m);font-size:9px;color:var(--text3);font-weight:500}
/* `.is-open` is toggled by pointerenter / pointerleave in core/rail.js, so the
   column collapses the moment the pointer leaves — a click on the switch no
   longer holds it open. Keyboard focus still opens it. */
.side.is-open .ri-label{opacity:1;visibility:visible;transform:none;transition-delay:0s,0s,0s}
/* Kept in its own rule: a browser without :has() drops the whole selector
   list, which would take the hover behaviour down with it. */
.side:has(:focus-visible) .ri-label{opacity:1;visibility:visible;transform:none;transition-delay:0s,0s,0s}
/* Keep the hidden label area out of the phone's scroll/tap targets. Only an
   explicit first tap opens it; touch hover and retained focus cannot do so. */
@media (max-width:860px){
  .rail-item{min-width:36px;justify-content:flex-end}
  .side:not(.is-open) .ri-label{display:none}
}

/* ── adaptive colour: the rail reads whatever is behind it ──────────
   There is no glass and no plate under this column. core/rail.js asks
   core/backdrop.js what is actually painted behind each item — a page
   background, a figure, a video frame — and colours it to suit: white over a
   dark picture, near black over a light one.

   The unit is finer than the item and finer than the letter. Each label is
   probed every few horizontal pixels and a native CSS gradient is clipped to
   the glyphs, so one letter can be bright on one side and dark on the other.
   Ticks use the same idea down their height. The gradients are set inline by
   rail.js; these rules only enable that mode.

   When every probe agrees, the flat classes below are used instead: cheaper,
   and they let the colour ease rather than snap. An item nothing can be read
   behind keeps the theme colours — no guess beats a wrong guess.

   These are literal colours rather than theme variables on purpose: they
   answer to the picture behind the text, not to the current theme. */
.rail-item.on-dark-lab  .ri-label         {color:rgba(255,255,255,.96)}
.rail-item.on-dark-lab  .ri-label b       {color:rgba(255,255,255,.56)}
.rail-item.on-light-lab .ri-label         {color:rgba(8,11,15,.95)}
.rail-item.on-light-lab .ri-label b       {color:rgba(16,20,26,.52)}

.rail-item.on-dark-trk  .ri-track          {background:rgba(255,255,255,.26)}
.rail-item.on-dark-trk  .ri-fill           {background:rgba(255,255,255,.62)}
.rail-item.on-light-trk .ri-track          {background:rgba(16,20,26,.2)}
.rail-item.on-light-trk .ri-fill           {background:rgba(16,20,26,.5)}
/* the current section keeps its pink whatever it is sitting on */
.rail-item.on-dark-trk.is-active  .ri-fill,
.rail-item.on-light-trk.is-active .ri-fill {background:var(--rail-on)}

/* split mode — the gradient itself comes from rail.js as an inline style */
.rail-item.is-split-lab .ri-label{
  color:transparent;
  -webkit-text-fill-color:transparent;
  -webkit-background-clip:text;
          background-clip:text;
  background-repeat:no-repeat;
  background-size:100% 100%;
  transition:none;                 /* a ramp cannot tween; snapping is honest */
}
.rail-item.is-split-lab .ri-label b{color:transparent;-webkit-text-fill-color:transparent}
.rail-item.is-split-trk .ri-track{background-color:transparent;transition:none}

/* Fixed pink wins immediately, even before stale split styles are cleared. */
.rail-item.is-active .ri-label,
.rail-item.is-active .ri-label b{
  color:var(--rail-label-on);
  -webkit-text-fill-color:var(--rail-label-on);
  background-image:none !important;
  text-shadow:none;
  box-shadow:none;
}

/* ══════════════════════════════════════════════════════════════════════
   LANGUAGE SWITCH
   ══════════════════════════════════════════════════════════════════════ */
.lang-switch{
  position:fixed;top:18px;right:18px;z-index:60;
  display:flex;gap:2px;padding:3px;
  background:var(--surf);border:1px solid var(--line);border-radius:999px;
}
.lang-btn{padding:3px 10px;border-radius:999px;font-size:12px;color:var(--text2)}
.lang-btn.is-on{background:var(--accent);color:var(--accent-ink)}

/* ══════════════════════════════════════════════════════════════════════
   MODULE SHELL  —  sticky headers mark the current section
   ══════════════════════════════════════════════════════════════════════ */
.module{position:relative;padding:0 0 clamp(48px,7vh,86px)}
#m-profile{padding-top:clamp(14px,3vh,30px)}   /* space above the first header, keeping every header the same height */
.module + .module{border-top:1px solid var(--line-soft)}
.mod-inner{width:min(var(--maxw),100% - var(--pad) * 2);margin-inline:auto}

/* A pinned header sits over the text, so it turns to frosted glass and the
   page stays faintly visible through it. While it is still in the flow there
   is nothing behind it, so it stays a plain solid heading — `.is-stuck` is
   added by core/sticky.js the moment it reaches its pin position. */
.mod-head{
  position:sticky;top:0;z-index:6;
  display:flex;align-items:baseline;gap:12px;
  padding:16px 0 12px;
  margin-bottom:16px;
  background:var(--bg);
  border-bottom:1px solid var(--line-soft);
}
.mod-index{font-family:var(--ff-m);font-size:10.5px;letter-spacing:.12em;color:var(--text3)}
.mod-title{font-size:clamp(21px,2.6vw,27px);font-weight:600;letter-spacing:-.02em}

.sub-title{
  position:sticky;top:var(--head-h);z-index:5;
  display:flex;align-items:center;gap:8px;
  padding:12px 0 8px;
  margin:24px 0 10px;
  background:var(--bg);
  font-size:11.5px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;
  color:var(--text3);
}
/* Static variant: scrolls away with the text rather than pinning, so it never
   joins the glass panel. Used for About and Education inside About me, which
   already sit under the section's own big title. */
.sub-title--plain{
  position:static;
  background:transparent;
}
.pmain .sub-title--plain:first-child{margin-top:0;padding-top:0}

.sub-title .count{
  font-family:var(--ff-m);font-size:10px;letter-spacing:0;
  padding:0 6px;border-radius:99px;background:var(--chip);color:var(--chip-ink);
}
.sub-title::after{content:"";flex:1;height:1px;background:var(--line-soft)}

/* ── the pinned state ──────────────────────────────────────────────────
   One shared panel carries the glass for every pinned header at once, so the
   section title and its sub-heading are a single surface with no seam and no
   gap between them. js/core/sticky.js sizes it. A wide blur is what keeps the
   headings readable at this transparency: it flattens whatever is behind into
   soft colour instead of legible detail. */
/* Edge to edge, so the band reads as one continuous surface across the page
   rather than a plate floating over the text column.

   Fade the tint and filtered backdrop together, keeping full strength under
   the headings and tapering to transparent below them. Only this one shallow
   panel is filtered; the mask is static and the blur radius stays modest to
   limit scrolling cost. No separate filtered pseudo-elements are needed. */
.glass-plate{
  position:fixed;top:0;left:0;right:0;z-index:4;
  --h:0px;
  height:calc(var(--h) + var(--glass-fade));
  pointer-events:none;
  background:var(--glass);
  -webkit-backdrop-filter:var(--glass-fx);
  backdrop-filter:var(--glass-fx);
  -webkit-mask-image:linear-gradient(to bottom,
    #000 0, #000 var(--h),
    rgba(0,0,0,.84) calc(var(--h) + var(--glass-fade) * .25),
    rgba(0,0,0,.5) calc(var(--h) + var(--glass-fade) * .5),
    rgba(0,0,0,.16) calc(var(--h) + var(--glass-fade) * .75),
    transparent 100%);
  mask-image:linear-gradient(to bottom,
    #000 0, #000 var(--h),
    rgba(0,0,0,.84) calc(var(--h) + var(--glass-fade) * .25),
    rgba(0,0,0,.5) calc(var(--h) + var(--glass-fade) * .5),
    rgba(0,0,0,.16) calc(var(--h) + var(--glass-fade) * .75),
    transparent 100%);
  opacity:0;
}
.glass-plate.is-on{opacity:1}

/* A pinned header rides on that panel instead of carrying its own glass, and
   drops its own rule so the soft edge is uninterrupted. */
.mod-head.is-stuck,
.sub-title.is-stuck,
.sticky-sub.is-stuck{
  background:transparent;
  border-bottom-color:transparent;
}

/* Fallback: without backdrop-filter each pinned header stays solid — readable,
   just not layered. */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .glass-plate{background:var(--bg)}
  .glass-plate{display:none}
  .mod-head.is-stuck,.sub-title.is-stuck,.sticky-sub.is-stuck{background:var(--bg)}
  .mod-head.is-stuck,.sticky-sub.is-stuck{border-bottom-color:var(--line-soft)}
}

.prose{font-size:15.5px;color:var(--text)}
.prose p:last-child{margin-bottom:0}

/* ══════════════════════════════════════════════════════════════════════
   1 · PROFILE
   ══════════════════════════════════════════════════════════════════════ */
.profile-grid{
  display:grid;
  grid-template-columns:minmax(210px,252px) minmax(0,1fr);
  gap:clamp(26px,4vw,52px);
  align-items:start;
}
.pcard{position:sticky;top:calc(var(--head-h) + 14px)}

.pc-portrait{
  width:100%;aspect-ratio:1/1;border-radius:var(--r-l);overflow:hidden;
  background:transparent;border:1px solid var(--line);
}
.pc-portrait img{width:100%;height:100%;object-fit:cover}

.pc-name{font-size:clamp(20px,2.2vw,24px);margin:16px 0 2px;letter-spacing:-.02em}
.pc-alt{font-size:.66em;color:var(--text3);font-weight:500;margin-left:5px;letter-spacing:0}
.pc-tagline{font-size:12.5px;color:var(--text2);line-height:1.5;margin:0 0 14px}

.pc-meta{display:flex;flex-direction:column;gap:6px;padding:12px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.pc-meta li{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--text2)}
.pc-meta .ic{color:var(--text3)}

.pc-links{display:flex;flex-direction:column;margin-top:10px}
.pc-link{
  display:flex;align-items:center;gap:8px;
  padding:6px 8px;margin-left:-8px;border-radius:var(--r-s);
  font-size:12.5px;color:var(--text2);
  cursor:pointer;text-align:left;width:calc(100% + 8px);
}
.pc-link:hover{background:var(--bg-soft);color:var(--text);text-decoration:none}
.pc-link .ic{color:var(--text3)}
.pc-link:hover .ic{color:var(--accent)}
.pc-link--cv{margin-top:2px}

/* education — plain frame, no accent colour */
.edu-list{display:flex;flex-direction:column;gap:8px}
.edu{
  border:1px solid var(--edu-accent,var(--line));border-radius:var(--r-m);
  padding:12px 15px;
}
.edu-top{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px}
.edu-top h4{font-size:15px}
.edu-period{font-family:var(--ff-m);font-size:11px;color:var(--text3)}
.edu-degree{margin:3px 0 1px;font-size:13.5px;color:var(--text)}
.edu-sub{margin:0;font-size:12.5px;color:var(--text2)}
.edu-sub .dot{opacity:.5;margin:0 2px}

/* ══════════════════════════════════════════════════════════════════════
   BADGES + TAGS  —  one shared height, flat static colours
   ══════════════════════════════════════════════════════════════════════ */
.badge,.tag{
  display:inline-flex;align-items:center;
  height:var(--badge-h);
  padding:0 8px;border-radius:5px;
  font-size:11px;font-weight:600;letter-spacing:.01em;
  border:1px solid;white-space:nowrap;
  box-sizing:border-box;
}
.tag{font-size:10px;letter-spacing:.05em;text-transform:uppercase}

.t-violet {background:#f3eefe;color:#6d28d9;border-color:#e0d4fb}
.t-blue   {background:#eaf1fe;color:#1d4ed8;border-color:#d3e0fc}
.t-teal   {background:#e6f7f5;color:#0f766e;border-color:#c8ece7}
.t-gold   {background:#fdf3e3;color:#a35b09;border-color:#f7e2c0}
.t-green  {background:#e9f8ee;color:#15803d;border-color:#cceedb}
.t-indigo {background:#eeeffd;color:#4338ca;border-color:#dcdefb}
.t-pink   {background:#fdecf4;color:#be185d;border-color:#f9d5e5}
.t-cyan   {background:#e4f6fb;color:#0e7490;border-color:#c6eaf4}
.t-orange {background:#fdf0e6;color:#c2410c;border-color:#f8dcc5}
.t-purple {background:#f7edfd;color:#7e22ce;border-color:#ecd8fa}
.t-emerald{background:#e6f7f0;color:#047857;border-color:#c8ebdd}
.t-red    {
  background:color-mix(in srgb,var(--c-campus-red) 8%, white);
  color:var(--c-campus-red);
  border-color:color-mix(in srgb,var(--c-campus-red) 28%, white);
}
.t-gray   {background:#f2f4f6;color:#5f6873;border-color:#dfe3e8}

[data-theme="dark"] .t-violet {background:#241d3a;color:#c4b5fd;border-color:#392e5c}
[data-theme="dark"] .t-blue   {background:#16243f;color:#a5c6ff;border-color:#25395f}
[data-theme="dark"] .t-teal   {background:#12302e;color:#7fded2;border-color:#1e4b47}
[data-theme="dark"] .t-gold   {background:#33260f;color:#f3c778;border-color:#4d3a18}
[data-theme="dark"] .t-green  {background:#132e1f;color:#8ee0aa;border-color:#1f4a33}
[data-theme="dark"] .t-indigo {background:#1d1f3f;color:#b0b6fb;border-color:#2e3260}
[data-theme="dark"] .t-pink   {background:#341827;color:#f5a8cc;border-color:#4f273c}
[data-theme="dark"] .t-cyan   {background:#102b36;color:#84d7ee;border-color:#1b4453}
[data-theme="dark"] .t-orange {background:#33200f;color:#f5b183;border-color:#4d3319}
[data-theme="dark"] .t-purple {background:#2a1a3b;color:#dcb0fa;border-color:#412a5b}
[data-theme="dark"] .t-emerald{background:#0f2e25;color:#7fddb9;border-color:#1a4a3b}
[data-theme="dark"] .t-red    {
  background:color-mix(in srgb,var(--c-campus-red) 28%, var(--bg));
  color:color-mix(in srgb,var(--c-campus-red) 38%, white);
  border-color:color-mix(in srgb,var(--c-campus-red) 58%, var(--line));
}
[data-theme="dark"] .t-gray   {background:#202731;color:#aeb7c3;border-color:#343e4b}

/* ══════════════════════════════════════════════════════════════════════
   2 · PUBLICATIONS
   ══════════════════════════════════════════════════════════════════════ */
.pub{
  display:grid;
  grid-template-columns:minmax(112px,138px) minmax(0,1fr);
  gap:clamp(12px,2vw,24px);
  padding:16px 0;
  border-top:1px solid var(--line-soft);
}
.pub-group > .pub:first-of-type{border-top:0;padding-top:4px}

.pub-badges{display:flex;flex-direction:column;align-items:flex-start;gap:5px;padding-top:1px}

.pub-title{font-size:15.5px;line-height:1.38;margin-bottom:6px}
.pub-line{margin:0;color:var(--text2)}
.pub-line strong{color:var(--text);font-weight:600}
.pub-doi{color:var(--text3);margin:3px 0 0}
.pub-links{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:3px 0 0}
/* authors, DOI and links form one typographic block */
.pub-line,.pub-doi,.pub-links,.pub-links a{
  font-family:var(--ff);font-size:12.5px;line-height:1.45;
}
.pub-links a{display:inline-flex;align-items:center;gap:3px;font-weight:500}
.pub-link-sep{color:var(--text3);opacity:.65}
.pub .media{margin-left:0;margin-top:12px}

/* ══════════════════════════════════════════════════════════════════════
   3 · APP DEVELOPMENT
   ══════════════════════════════════════════════════════════════════════ */
/* `--app-accent` is set per card by sections/apps.js from `accent` in the
   content file; without one the card uses the neutral border. */
.app-card{
  border:1.5px solid var(--app-accent,var(--line));
  border-radius:var(--r-l);
  padding:clamp(14px,2vw,20px);
}
/* the same hue, calmed down so it does not glow against the dark page */
[data-theme="dark"] .app-card{
  border-color:color-mix(in srgb,var(--app-accent,var(--line)) 55%, var(--bg));
}
.app-card + .app-card{margin-top:14px}

.app-head{
  display:flex;gap:16px;align-items:stretch;
  --app-icon-size:clamp(74px,7vw,88px);
}
.app-icon{
  flex:0 0 var(--app-icon-size);
  width:var(--app-icon-size);height:var(--app-icon-size);
  min-width:var(--app-icon-size);min-height:var(--app-icon-size);
  aspect-ratio:1/1;align-self:flex-start;
  border-radius:17px;overflow:hidden;border:1px solid var(--line);
  background:transparent;
}
.app-icon img{width:100%;height:100%;object-fit:cover}
/* the text column matches the icon's height, so the two read as one block */
.app-text{
  display:flex;flex-direction:column;justify-content:space-between;gap:3px;
  min-width:0;min-height:clamp(74px,7vw,88px);
}
.app-name{font-size:16.5px;font-weight:600;letter-spacing:-.01em}
.app-blurb{font-size:13px;color:var(--text2);line-height:1.45}
.app-links{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.app-links a{display:inline-flex;align-items:center;gap:3px;font-size:12.5px}
.app-link-sep{font-size:12.5px;color:var(--text3);opacity:.65}
.app-card .media{margin-left:0;margin-top:14px}

/* ══════════════════════════════════════════════════════════════════════
   4 · EXPERIENCES
   ══════════════════════════════════════════════════════════════════════ */
.filter-bar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:18px}
.sticky-sub{
  position:sticky;top:var(--head-h);z-index:5;
  background:var(--bg);
  padding:10px 0;
  margin-bottom:8px;
  border-bottom:1px solid var(--line-soft);
}
.chip{
  display:inline-flex;align-items:center;gap:6px;
  height:28px;padding:0 12px;border-radius:999px;
  border:1px solid var(--line);
  font-size:12.5px;color:var(--text2);
}
.chip:hover{border-color:var(--text3);color:var(--text)}
.chip-label{display:inline-flex;align-items:center;gap:6px}
.chip.on-dark-lab:not(.is-on){color:#fff}
.chip.on-light-lab:not(.is-on){color:#080b0f}
.chip.is-split-lab:not(.is-on) .chip-label{
  color:transparent;-webkit-text-fill-color:transparent;
  background-clip:text;-webkit-background-clip:text;
}
.chip-n{font-family:var(--ff-m);font-size:10px;opacity:.65}
.chip.is-on{background:transparent;border-color:var(--rail-label-on);color:var(--rail-label-on)}
.chip.is-on .chip-label{-webkit-text-fill-color:var(--rail-label-on);background-image:none!important}
.chip.is-on .chip-n{opacity:.85}

.exp-list{display:flex;flex-direction:column}
.exp{padding:16px 0;border-top:1px solid var(--line-soft)}
/* `is-first` is set on the first VISIBLE entry by sections/experiences.js —
   :first-child would keep the rule on a filtered-out entry and leave a second
   divider right under the sticky tag bar. */
.exp.is-first{border-top:0;padding-top:8px}
.exp.is-out{display:none}

/* Align the text baselines despite the different title/date font metrics. */
.exp-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 10px;margin-bottom:7px}
.exp-role{font-size:15.5px;font-weight:600;letter-spacing:-.01em}
.exp-meta{font-family:var(--ff-m);font-size:11.5px;color:var(--text-meta)}
.exp-org{font-size:12.5px;color:var(--text-meta)}
.exp-sep{color:var(--text-meta);opacity:.55}
.exp-tags{display:flex;flex-wrap:wrap;gap:5px;margin-left:auto}

.exp-points{display:flex;flex-direction:column;gap:2px}
.exp-points li{position:relative;padding-left:14px;font-size:13.5px;color:var(--text2);line-height:1.55}
.exp-points li::before{content:"";position:absolute;left:2px;top:.68em;width:4px;height:1px;background:var(--text3)}
.empty-note{font-size:13px;color:var(--text3);padding:18px 0}
.exp .media{margin-left:14px}

/* ══════════════════════════════════════════════════════════════════════
   MEDIA STRIP
   ══════════════════════════════════════════════════════════════════════ */
.media{margin-top:12px}
.strip{
  display:flex;gap:10px;
  overflow-x:auto;overflow-y:hidden;
  scrollbar-width:none;
  overscroll-behavior-x:contain;
  -webkit-overflow-scrolling:touch;
}
.strip::-webkit-scrollbar{display:none}
.strip.is-scrollable{cursor:grab}
.strip.is-scrollable.is-grabbing{cursor:grabbing}

.strip .cell{
  margin:0;flex:0 0 auto;
  height:var(--cell-h);
  border-radius:var(--r-m);overflow:hidden;
  border:1px solid var(--line);
  background:transparent;          /* never paint behind an image */
  contain:paint;
}
.strip img,.strip video{
  height:100%;width:100%;
  object-fit:cover;object-position:center;
  background:transparent;
}
/* One row height for every section, scaled to the device: phones and small
   windows never go below 200px, large screens stop at 300px. vh uses the
   large viewport, so mobile toolbars showing/hiding cause no reflow. */
.media{--cell-h:clamp(200px,min(22vw,34vh),300px)}
/* A single item uses the same row height as multiple items; when it is wider
   than the column, its strip scrolls and drags exactly like a multi-item row. */

.strip-hint{margin-top:8px}
.strip-hint.is-hidden{display:none}
.sh-track{
  position:relative;display:block;height:2.5px;border-radius:3px;
  width:min(180px,40%);background:var(--rail);overflow:hidden;
}
.sh-thumb{position:absolute;top:0;bottom:0;left:0;width:36%;border-radius:3px}
.strip-hint--segmented{display:flex;gap:5px;width:min(180px,40%)}
.sh-segment{
  position:relative;flex:1;min-width:0;height:2.5px;
  border-radius:3px;background:var(--rail);
}
.sh-fill{
  position:absolute;inset:0;border-radius:inherit;
  background:var(--text3);opacity:.55;
  transform:scaleX(0);transform-origin:left center;
}
.sh-thumb,.sh-segment.is-active .sh-fill{
  background:var(--rail-on);opacity:1;
  box-shadow:0 0 4px 1px rgba(243,163,198,.16);
}
.media-note{margin:8px 0 0;font-size:12px;color:var(--text3)}

/* ══════════════════════════════════════════════════════════════════════
   5 · INTERESTS + GALLERY
   ══════════════════════════════════════════════════════════════════════ */
.interest-list{display:flex;flex-direction:column;gap:5px;max-width:760px}
.interest-list li{
  position:relative;padding-left:16px;
  font-size:15.5px;color:var(--text2);line-height:1.7;
}
.interest-list li::before{
  content:"-";position:absolute;left:2px;top:0;
  color:var(--text3);
}
.int-label{color:var(--text);font-weight:600;font-size:16px}
.int-sep{opacity:.55;margin:0 5px}

.gallery-wrap{margin-top:24px}
.gallery-toggle{
  display:inline-flex;align-items:center;gap:9px;
  height:34px;padding:0 15px;border-radius:999px;
  border:1px solid var(--line);
  font-size:13px;color:var(--text);
}
.gallery-toggle:hover{border-color:var(--text3)}
.gt-count{font-family:var(--ff-m);font-size:11px;color:var(--text3)}
.gt-chevron{display:inline-flex;color:var(--text3);transition:transform .22s ease}
.gallery-toggle[aria-expanded="true"] .gt-chevron{transform:rotate(180deg)}

.gallery-grid{margin-top:16px;display:flex;flex-direction:column;gap:8px}
.gal-group{
  display:grid;grid-template-columns:repeat(5,1fr);gap:8px;
}
.gcell{
  padding:0;border-radius:8px;overflow:hidden;
  border:1px solid var(--line);background:transparent;
}
.gal-group--landscape .gcell{aspect-ratio:3/2}
.gal-group--portrait  .gcell{aspect-ratio:2/3}
.gcell img{width:100%;height:100%;object-fit:cover}
@media (max-width:900px){ .gal-group{grid-template-columns:repeat(3,1fr)} }
@media (max-width:560px){ .gal-group{grid-template-columns:repeat(2,1fr)} }

/* ══════════════════════════════════════════════════════════════════════
   PAGEVIEW + FOOTER
   ══════════════════════════════════════════════════════════════════════ */
.module--tail{padding-bottom:clamp(26px,4vh,44px)}
.pageview{display:flex;flex-direction:column;align-items:flex-start;gap:12px;padding-top:26px}
.pv-title{font-size:10.5px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--text3)}
.pv-map-slot{display:block;width:300px;max-width:100%}
.pv-map-slot img{display:block;width:300px;height:auto}
.pv-map-slot canvas,.pv-map-slot iframe,.pv-map-slot img{max-width:100%}
.pv-fallback{font-size:12px;color:var(--text3)}

.site-foot{
  margin-top:clamp(30px,5vh,52px);
  padding-top:16px;
  border-top:1px solid var(--line-soft);
  text-align:left;
}
.site-foot p{margin:0;font-size:12px;color:var(--text3)}
.foot-sep{opacity:.5;margin:0 6px}

/* The sentence carrying the easter egg — see js/sections/footer.js.
   Deliberately indistinguishable from the rest of the footer: no cursor
   change, no underline, no tint. Nothing gives it away. */
.foot-egg{position:relative}

/* the capsule it opens */
.egg-pop{
  position:absolute;left:50%;bottom:calc(100% + 9px);
  transform:translateX(-50%);
  display:flex;align-items:center;gap:8px;
  white-space:nowrap;
  padding:5px 13px;
  border-radius:999px;
  background:var(--surf);
  border:1px solid var(--line);
  box-shadow:0 3px 12px rgba(0,0,0,.09);
  font-size:11.5px;line-height:1.5;color:var(--text2);
  opacity:0;visibility:hidden;
  transition:opacity .2s ease,visibility 0s linear .2s;
  pointer-events:none;      /* never steals the pointer that opened it */
  z-index:5;
}
.egg-pop.is-on{opacity:1;visibility:visible;transition-delay:0s,0s}
.egg-pop b{font-weight:500}
.egg-sep{font-style:normal;opacity:.4}
@media (prefers-reduced-motion:reduce){ .egg-pop{transition:none} }

/* ══════════════════════════════════════════════════════════════════════
   LIGHTBOX
   ══════════════════════════════════════════════════════════════════════ */
.lightbox{
  position:fixed;inset:0;z-index:120;
  display:grid;place-items:center;
  padding:clamp(14px,4vw,52px);
  background:rgba(8,10,14,.92);
}
.lb-figure{margin:0;display:flex;flex-direction:column;align-items:center;gap:10px;max-height:100%}
.lb-holder{position:relative;max-height:calc(100vh - 120px);display:flex}
.lb-holder img{
  max-height:calc(100vh - 120px);max-width:100%;width:auto;
  border-radius:6px;
  -webkit-user-drag:none;
}
.lightbox.is-protected .lb-holder img{pointer-events:none;user-select:none}
.lb-shield{position:absolute;inset:0}
.lightbox:not(.is-protected) .lb-shield{display:none}
#lbCap{font-size:12px;color:rgba(255,255,255,.7)}

.lb-close,.lb-nav{
  position:absolute;z-index:2;
  display:grid;place-items:center;
  width:40px;height:40px;border-radius:50%;
  color:#fff;background:rgba(255,255,255,.1);
  border:1px solid rgba(255,255,255,.16);
}
.lb-close:hover,.lb-nav:hover{background:rgba(255,255,255,.2)}
.lb-close{top:clamp(12px,3vw,24px);right:clamp(12px,3vw,24px)}
.lb-prev{left:clamp(8px,2vw,24px);top:50%;transform:translateY(-50%)}
.lb-next{right:clamp(8px,2vw,24px);top:50%;transform:translateY(-50%)}

/* ══════════════════════════════════════════════════════════════════════
   RESPONSIVE
   ══════════════════════════════════════════════════════════════════════ */
@media (max-width:860px){
  .profile-grid{grid-template-columns:1fr;gap:24px}
  .pcard{position:static;display:grid;grid-template-columns:120px 1fr;gap:16px;align-items:start}
  .pc-portrait{grid-row:span 4;aspect-ratio:1/1.1}
  .pc-name{margin-top:0}
  .pub{grid-template-columns:minmax(0,1fr);gap:8px}
  .pub-badges{flex-direction:row;flex-wrap:wrap}
  .exp-tags{margin-left:0;width:100%}
  .exp .media{margin-left:10px}
}
@media (max-width:560px){
  .pcard{grid-template-columns:1fr;gap:10px}
  .pc-portrait{grid-row:auto;max-width:160px;aspect-ratio:1/1}
  .app-head{flex-direction:column;gap:12px}
  .app-head{--app-icon-size:64px}
  .app-text{min-height:0}
  .lang-switch{top:12px;right:12px}
}

@media print{
  .side,.lang-switch,.glass-plate{display:none}
  .mod-head,.sub-title{position:static}
}
