:root {
  --bg: #3b3b3e;       /* tarmac */
  --surface: #232326;
  --text: #f3f1f6;
  --muted: #bab6c0;
  --accent: #8b45e6;   /* hoodie purple */
  --accent-2: #b98cff;
  --line: #141416;
  --soft-line: rgba(255, 255, 255, 0.16);
  --px: 4px;           /* one "pixel" */
  --pixel-font: "Press Start 2P", monospace;
  --body-font: "JetBrains Mono", ui-monospace, monospace;
  --ease: cubic-bezier(0.65, 0, 0.35, 1);
  /* hero layout: content width W split into text | portrait | text columns */
  --pad: clamp(16px, 4vw, 48px);
  --W: calc(min(100vw, 1600px) - 2 * var(--pad));
  --hero-text: clamp(15px, calc(var(--W) * 0.0165), 21px);
  /* portrait height; never wider than the middle 34% of W (image ratio is 52:90) */
  --av-h: min(calc(100vh - 110px), calc(var(--W) * 0.34 * 90 / 52));
  --av-half: calc(var(--av-h) * 26 / 90);
  --hero-gap: clamp(24px, 2.4vw, 40px);  /* text ↔ portrait */
  --strip-w: clamp(64px, 6vw, 96px);     /* "More Projects" strip */
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html { scroll-behavior: smooth; overflow-x: clip; }

body {
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--body-font);
  font-size: 15px;
  line-height: 1.7;
  overflow-x: clip;
}

/* tarmac: dark grit + a few light aggregate flecks, pinned to the screen so it never scrolls.
   Sized to the *large* viewport (address bar hidden) rather than inset: 0, so when a phone's
   address bar slides away the newly exposed strip at the bottom is already covered. */
body::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;    /* fallback: on mobile Chrome/Safari vh is already the large viewport */
  height: 100lvh;
  z-index: -1;
  pointer-events: none;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' seed='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  4 0 0 0 -2.5'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)' opacity='0.35'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.6 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

a { color: inherit; }
img { image-rendering: pixelated; display: block; }
strong { color: var(--accent-2); font-weight: 600; }

/* ---------- pixel primitives ---------- */
.px-box {
  background: var(--surface);
  box-shadow:
    0 calc(var(--px) * -1) 0 0 var(--line),
    0 var(--px) 0 0 var(--line),
    calc(var(--px) * -1) 0 0 0 var(--line),
    var(--px) 0 0 0 var(--line),
    var(--drop, calc(var(--px) * 2)) var(--drop, calc(var(--px) * 2)) 0 0 rgba(0, 0, 0, 0.45);
}

.px-title {
  font-family: var(--pixel-font);
  font-size: clamp(16px, 2.4vw, 22px);
  letter-spacing: 1px;
}

.px-btn {
  font-family: var(--pixel-font);
  font-size: 11px;
  color: var(--text);
  background: var(--accent);
  border: 0;
  padding: 12px 14px;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
  box-shadow:
    inset calc(var(--px) * -1) calc(var(--px) * -1) 0 0 rgba(0, 0, 0, 0.35),
    inset var(--px) var(--px) 0 0 rgba(255, 255, 255, 0.2);
  transition: transform 0.1s;
}
.px-btn:hover { transform: translateY(-2px); }
.px-btn:active { transform: translateY(2px); }
.px-btn.big { font-size: clamp(9px, 2.2vw, 13px); padding: 16px 20px; word-break: break-all; }
.copy-email.copied { color: var(--accent-2); }
.copy-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }  /* screen readers only */

.tags { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; }
.tags li {
  font-size: 12px;
  padding: 2px 8px;
  border: 2px solid var(--soft-line);
  color: var(--muted);
}

/* ---------- top bar ---------- */
.topbar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 10;
  display: flex;
  justify-content: center;
  padding: 20px 16px;
  pointer-events: none;
}
.main-nav {
  pointer-events: auto;
  display: flex;
  gap: clamp(20px, calc(10vw - 56px), 72px);   /* tighter on small screens, clear of the logo */
  padding: 14px clamp(20px, 4vw, 40px);
}
.main-nav a {
  font-family: var(--pixel-font);
  font-size: clamp(12px, 1.1vw, 15px);
  text-decoration: none;
  color: var(--muted);
  transition: color 0.2s;
}
.main-nav a:hover { color: var(--text); }
/* logo, top left: a bare square as tall as the nav box */
.brand {
  pointer-events: auto;
  position: absolute;
  top: 20px;
  bottom: 20px;
  left: 20px;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  transition: translate 0.2s;
}
.brand img { width: 100%; height: 100%; display: block; }
@media (hover: hover) { .brand:hover { translate: 0 -2px; } }

/* phones: menu button (top right) and a sidebar that slides in from the right.
   Hidden on larger screens; the phone media query below switches them on. */
.menu-btn,
.menu-backdrop,
.side-menu { display: none; }
.menu-btn {
  pointer-events: auto;
  position: relative;
  z-index: 2;               /* stays above the open sidebar, as its close button */
  margin-left: auto;
  width: 44px;
  height: 44px;
  border: 0;
  color: var(--text);
  cursor: pointer;
}
/* three pixel bars that become a pixel X when the menu is open */
.menu-icon,
.menu-icon::before,
.menu-icon::after {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 20px;
  height: 4px;
  margin: -2px 0 0 -10px;
  background: currentColor;
  transition: transform 0.25s var(--ease), opacity 0.2s;
}
.menu-icon::before,
.menu-icon::after { content: ""; left: 0; top: 0; margin: 0; }
.menu-icon::before { transform: translateY(-7px); }
.menu-icon::after { transform: translateY(7px); }
.menu-open .menu-icon { background: transparent; }
.menu-open .menu-icon::before { transform: rotate(45deg); }
.menu-open .menu-icon::after { transform: rotate(-45deg); }

.menu-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}
.side-menu {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(78vw, 300px);
  flex-direction: column;
  gap: 30px;
  padding: 104px 32px 32px;
  transform: translateX(calc(100% + 16px));   /* clear of its own pixel shadow */
  visibility: hidden;                         /* out of the tab order while closed */
  transition: transform 0.35s var(--ease), visibility 0s 0.35s;
}
.side-menu a {
  font-family: var(--pixel-font);
  font-size: 14px;
  line-height: 1.5;
  text-decoration: none;
  color: var(--muted);
}
.side-menu a:hover,
.side-menu a:focus-visible { color: var(--text); }
.menu-open .menu-backdrop { opacity: 1; pointer-events: auto; }
.menu-open .side-menu {
  transform: none;
  visibility: visible;
  pointer-events: auto;
  transition: transform 0.35s var(--ease), visibility 0s;
}
html.menu-open { overflow: hidden; }   /* the page doesn't scroll behind the open menu */

/* ---------- frames 1 & 2: intro ---------- */
.intro { position: relative; height: 403.6vh; }

/* nav clicks: the page content (not the nav or the tarmac) fades out, the page jumps,
   then it fades back in. .is-jumping switches off the sections' own animations for
   the jump so the destination appears already settled (see script.js). */
.intro,
.experience,
.projects { transition: opacity 0.3s ease; }
.is-faded .intro,
.is-faded .experience,
.is-faded .projects { opacity: 0; }
.is-jumping .stage,
.is-jumping .stage::before,
.is-jumping .stage > *,
.is-jumping .card,
.is-jumping .projects-head,
.is-jumping .experience-head,
.is-jumping .jobs,
.is-jumping .contact-head h2 { transition: none !important; }
/* nav targets, inside the about and skills stretches (timeline in script.js) */
.anchor { position: absolute; top: 90vh; }
.anchor-skills { top: 210vh; }

.stage {
  position: sticky;
  top: 0;
  height: 100vh;
  max-width: 1600px;
  margin: 0 auto;
  padding: 0 var(--pad);
  transform: scale(calc(1 - var(--shrink, 0) * 0.18));
  opacity: calc(1 - var(--shrink, 0));  /* about vanishes completely */
  transform-origin: 50% 30%;
}
/* soft spotlight so the portrait is the focus */
.stage::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 0;
  width: min(90vw, 900px);
  height: 90vh;
  transform: translateX(-50%);
  background: radial-gradient(ellipse at 50% 60%, rgba(255, 255, 255, 0.13), transparent 62%);
  transition: left 0.9s var(--ease);
  pointer-events: none;
}
.stage.is-about::before { left: 21%; }

.stage > * {
  position: absolute;
  transition: transform 0.9s var(--ease), opacity 0.9s var(--ease), left 0.9s var(--ease);
}

.avatar {
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  transform-origin: 50% 100%;
  filter: drop-shadow(0 0 30px rgba(0, 0, 0, 0.45));
}
.avatar img { height: var(--av-h); width: auto; }
.avatar-about { display: none; }  /* phones only */

/* side text hugs the portrait's edges rather than the page edges */
.hero-left {
  right: calc(50% + var(--av-half) + var(--hero-gap));
  top: 52%;
  transform: translateY(-50%);
  width: max-content;
  max-width: calc(var(--W) * 0.29);
  font-size: var(--hero-text);
}
.kicker { color: var(--muted); }
.hero-left h1 span { display: block; }
.hero-left h1 {
  font-family: var(--pixel-font);
  /* "Nishit DB" is 9 glyphs at 1em each; keep it inside the left column */
  font-size: min(46px, calc(var(--W) * 0.29 / 9.4));
  white-space: nowrap;
  line-height: 1.4;
  margin: 12px 0 16px;
}
.tagline { color: var(--accent-2); }

.hero-right {
  left: calc(50% + var(--av-half) + var(--hero-gap));
  top: 52%;
  transform: translateY(-50%);
  width: calc(var(--W) * 0.27);
  font-size: var(--hero-text);
  line-height: 1.6;
  color: var(--muted);
}
.hint {
  margin-top: 40px;
  font-family: var(--pixel-font);
  font-size: clamp(11px, 1vw, 13px);
  color: var(--accent-2);
  animation: bob 1.4s steps(2) infinite;
}
@keyframes bob { 50% { transform: translateY(6px); } }
@keyframes mini-drop { from { opacity: 0; transform: translateY(-16px); } }

/* first load / reload: the portrait slides up from the bottom, the left text in from
   the left, the right text in from the right. Uses the separate `translate` property so
   it stacks on top of each piece's own `transform` positioning (desktop and phone).
   Fill mode is backwards only: once played, the animation lets go entirely, so the
   scroll-driven states (about, skills, the phone fade) behave exactly as before. */
@keyframes hero-in-left   { from { translate: -60vw 0; opacity: 0; } }
@keyframes hero-in-right  { from { translate: 60vw 0;  opacity: 0; } }
@keyframes hero-in-bottom { from { translate: 0 100%;  opacity: 0; } }
.avatar:not(.avatar-about) { animation: hero-in-bottom 1s var(--ease) backwards; }
.hero-left  { animation: hero-in-left 1s var(--ease) 0.15s backwards; }
.hero-right { animation: hero-in-right 1s var(--ease) 0.25s backwards; }

/* side panels (about, skills) share one style */
.panel {
  right: clamp(16px, 4vw, 48px);
  top: 52%;
  width: min(56%, 760px);
  padding: clamp(24px, 2.6vw, 44px);
  font-size: var(--hero-text);  /* matches the hero side text */
  transform: translate(calc(100% + 80px), -50%);
  opacity: 0;
  pointer-events: none;
}
.panel h2 {
  font-family: var(--pixel-font);
  font-size: min(30px, calc(var(--W) * 0.29 / 14));  /* ~2/3 of the hero name */
  margin-bottom: 0.8em;
}
.panel p { margin-bottom: 0.8em; color: var(--muted); }
.panel p:first-of-type { color: var(--text); }

/* speech-bubble tail on the about panel, drawn as a stepped pixel triangle (4px
   steps) filled like the panel and outlined by the drop-shadows.
   Laptop: a right-angle triangle off the top-left corner — its top edge continues
   the panel's top border, the slope steps back down to the left edge.
   Phone: an isosceles tail at the bottom centre, pointing down (see media query). */
.bubble-tail {
  position: absolute;
  left: -24px;              /* the 4px base column covers the panel's left border */
  top: 0;
  width: 24px;
  height: 24px;
  filter:
    drop-shadow(-4px 0 0 var(--line))
    drop-shadow(0 4px 0 var(--line))
    drop-shadow(0 -4px 0 var(--line));
  pointer-events: none;
}
.bubble-tail::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--surface);
  clip-path: polygon(
    0 0, 24px 0, 24px 24px, 20px 24px, 20px 20px, 16px 20px, 16px 16px,
    12px 16px, 12px 12px, 8px 12px, 8px 8px, 4px 8px, 4px 4px, 0 4px
  );
}

.skills dt {
  font-family: var(--pixel-font);
  font-size: 0.55em;
  color: var(--accent-2);
  margin-top: 1.1em;
}
.skills dt:first-child { margin-top: 0; }
.skills dd { margin-top: 0.35em; color: var(--muted); }

/* frame 2 state: photo shifts left, text moves down, detail slides in from right */
.stage.is-about .avatar { left: 21%; transform: translateX(-50%) scale(0.88); }  /* room for the bubble tail */
.stage.is-about .hero-left {
  transform: translate(calc(-50vw - 100%), -50%);
  opacity: 0;
  pointer-events: none;
}
.stage.is-about .hero-right {
  transform: translateY(60vh);
  opacity: 0;
  pointer-events: none;
}
.stage.is-about .about {
  transform: translate(0, -50%);
  opacity: 1;
  pointer-events: auto;
}
/* frame 3 state: about scrolls right off screen, the portrait moves to the right
   side (staying on screen), and skills slides in from the left */
.skills {
  left: var(--pad);
  right: auto;
  transform: translate(calc(-100% - 50vw), -50%);
}
.stage.is-skills::before { left: 78%; }
.stage.is-skills .avatar { left: 78%; }
.stage.is-skills .about {
  transform: translate(calc(100% + 50vw), -50%);
  pointer-events: none;
}
.stage.is-skills .skills {
  transform: translate(0, -50%);
  opacity: 1;
  pointer-events: auto;
}

/* ---------- frame 3: experience ---------- */
/* pulled up so it rises in while the skills frame shrinks and fades; it then pins for
   a moment and shrinks and fades the same way (--exp-out, script.js) as the projects
   rise in over it. Height and timings are matched in script.js. */
.experience { position: relative; margin-top: -60vh; height: calc(163.6vh + var(--exp-extra, 0px)); }
.experience-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 104px var(--pad) 40px;
  pointer-events: none;   /* overlaps the skills fade-out; only the content takes clicks */
  transform: scale(calc(1 - var(--exp-out, 0) * 0.18));
  opacity: calc(1 - var(--exp-out, 0));
  transform-origin: 50% 30%;
}
.experience-inner {
  width: min(100%, 1320px);
  max-height: 100%;
  display: flex;
  flex-direction: column;
}
.experience-head {
  font-family: var(--pixel-font);
  font-size: clamp(18px, 2.2vw, 30px);
  line-height: 1.4;
  margin-bottom: clamp(12px, 2.2vh, 24px);
}
.experience-head,
.jobs {
  opacity: 0;
  transform: translateY(120px);
  transition: transform 0.8s var(--ease), opacity 0.8s var(--ease);
}
.jobs { transition-delay: 0.08s; pointer-events: auto; }
.experience.in .experience-head,
.experience.in .jobs { opacity: 1; transform: none; }

.jobs {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 3.2vh, 36px);
  padding: clamp(24px, 3.2vw, 56px);
}
.job + .job { border-top: 2px solid var(--soft-line); padding-top: clamp(16px, 3.2vh, 36px); }
.job-top { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 16px; }
.job h3 { font-family: var(--pixel-font); font-size: clamp(13px, 1.3vw, 19px); line-height: 1.4; }
.job-when { color: var(--muted); font-size: clamp(13px, 1vw, 16px); white-space: nowrap; }
.job-org { color: var(--accent-2); font-size: clamp(14px, 1.1vw, 17px); margin: 8px 0 12px; }
.job .features { font-size: clamp(14px, 1.15vw, 17.5px); gap: 8px; color: var(--muted); }
.job-project { color: var(--text); font-weight: 700; font-size: clamp(15px, 1.25vw, 19px); margin: 14px 0 8px; }
.job-org + .job-project { margin-top: 0; }
/* short screens where even .tight doesn't fit (script.js): the contents scroll inside the box */
.jobs.scrolls { overflow: hidden; }
.jobs.scrolls > .job { flex-shrink: 0; translate: 0 calc(var(--exp-shift, 0) * -1px); }
/* wide screens: the internship on the left, the other two roles stacked on the right */
@media (min-width: 1000px) {
  .jobs {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    column-gap: clamp(28px, 3.2vw, 56px);
  }
  /* the divider sits halfway across the column gap: the box reaches half the gap into
     it, and the same amount of padding keeps the text inside its own column */
  .job-main {
    grid-row: 1 / span 2;
    padding-right: calc(clamp(28px, 3.2vw, 56px) / 2);
    margin-right: calc(clamp(28px, 3.2vw, 56px) / -2);
    border-right: 2px solid var(--soft-line);
  }
  .job-main + .job { border-top: 0; padding-top: 0; }
}

/* shorter laptop screens (set by script.js when the list doesn't fit): first the
   smaller .snug sizes, then the tighter .tight type */
.jobs.snug { row-gap: clamp(14px, 2.6vh, 28px); padding: clamp(20px, 2.6vw, 40px); }
.jobs.snug .job + .job { padding-top: clamp(14px, 2.6vh, 28px); }
.jobs.snug .job h3 { font-size: clamp(12px, 1.1vw, 15px); }
.jobs.snug .job-when { font-size: 13px; }
.jobs.snug .job-org { font-size: 14px; margin: 6px 0 10px; }
.jobs.snug .features { font-size: clamp(13px, 1vw, 14.5px); gap: 6px; }
.jobs.snug .job-project { font-size: 15px; margin-top: 10px; }
.jobs.tight { row-gap: 12px; padding: 18px 22px; }
.jobs.tight .job + .job { padding-top: 12px; }
.jobs.tight .job-org { margin: 2px 0 6px; font-size: 13px; }
.jobs.tight .features { font-size: 12.5px; line-height: 1.45; gap: 4px; }
.jobs.tight .job-project { font-size: 14px; margin: 8px 0 4px; }
.jobs.tight .job-org + .job-project { margin-top: 0; }
@media (min-width: 1000px) {
  .jobs.snug .job-main + .job,
  .jobs.tight .job-main + .job { padding-top: 0; }   /* top of the right column */
}

/* ---------- frame 4: projects (horizontal scroll) + contact card ---------- */
/* .projects height and the --card-w / --contact-w / --track-x vars are set by script.js */
/* pulled up so the cards rise in while the about panel fades out (no blank scroll) */
.projects { position: relative; margin-top: -60vh; }
.projects-pin {
  touch-action: pan-y pinch-zoom;   /* sideways drags are handled in script.js */
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 96px 0 40px;
  pointer-events: none;  /* overlaps the about fade-out; only the cards take clicks */
}

/* section header: lines up with the cards' left edge, stays put while they slide,
   rises in with them, and fades out as the contact card arrives (--contact-in) */
.projects-head {
  width: var(--window-w, 100%);
  margin-bottom: clamp(4px, 1.2vh, 12px);
  opacity: 0;
  transform: translateY(120px);
  transition: transform 0.8s var(--ease), opacity 0.8s var(--ease);
}
.projects.in .projects-head { opacity: 1; transform: none; }
.projects-head h2 {
  font-family: var(--pixel-font);
  font-size: clamp(18px, 2.2vw, 30px);
  line-height: 1.4;
  opacity: calc(1 - var(--contact-in, 0));
}
.card { pointer-events: auto; }
/* clips the track to the content area; padding leaves room for the pixel shadows */
.track-window {
  --shadow-room: 16px;
  width: calc(var(--window-w, 100%) + 2 * var(--shadow-room));
  flex: 0 1 790px;          /* takes the height left under the header, up to ~760px cards */
  min-height: 0;
  margin: 0 auto;
  padding: var(--shadow-room);
  overflow: hidden;
  /* clip only sideways, so the "Contact Me" heading can sit above the contact card */
  overflow-x: clip;
  overflow-y: visible;
}
.track {
  display: flex;
  gap: var(--card-gap, 28px);
  height: 100%;
  transform: translateX(calc(var(--track-x, 0) * -1px));
}

.card {
  flex: 0 0 var(--card-w, 340px);
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* rise from the bottom when the section arrives */
  opacity: 0;
  transform: translateY(120px);
  transition: transform 0.8s var(--ease), opacity 0.8s var(--ease),
              translate 0.2s ease, box-shadow 0.2s ease;
}
.projects.in .card { opacity: 1; transform: none; }
/* the rise-in is staggered; the hover lift (3rd/4th) never waits */
.projects.in .card:nth-child(2) { transition-delay: 0.08s, 0.08s, 0s, 0s; }
.projects.in .card:nth-child(3) { transition-delay: 0.16s, 0.16s, 0s, 0s; }
.projects.in .card:nth-child(n + 4) { transition-delay: 0.24s, 0.24s, 0s, 0s; }

/* hover: project cards, the More Projects strip and the drawer's cards lift a little and
   cast a deeper pixel shadow. Uses `translate`, so it never fights the rise-in or drawer
   slides (which use `transform`). Only on devices that really hover, so a tap on a
   phone doesn't leave a card stuck up; keyboard focus inside a card lifts it too. */
@media (hover: hover) {
  .card.project:hover,
  .card.project:has(:focus-visible),   /* keyboard focus only, so a mouse click doesn't leave it up */
  .more-strip:hover,
  .more-strip:has(:focus-visible),
  .mini-card:hover,
  .mini-card:focus-visible {
    translate: 0 -6px;
    --drop: calc(var(--px) * 3);
  }
}

/* project card: small image, title, brief, stack, highlights, view button */
.thumb {
  flex: none;
  height: clamp(64px, 13vh, 140px);   /* deliberately small */
  display: grid;
  place-items: center;
  overflow: hidden;
  font-family: var(--pixel-font);
  font-size: clamp(20px, 2.2vw, 32px);
  color: var(--accent-2);
  background:
    repeating-linear-gradient(45deg, transparent 0 8px, rgba(139, 69, 230, 0.08) 8px 16px),
    #2c2c30;
}
.thumb { position: relative; }
/* screenshot fills the banner exactly and is cover-cropped (centred) to its shape */
.thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; image-rendering: auto; }
/* diagrams: show the whole image (letterboxed on its own white) instead of cropping it */
.thumb.thumb-fit { background: #fff; }
.thumb.thumb-fit img { object-fit: contain; padding: 6px; }
.card-body {
  flex: 1;
  min-height: 0;
  padding: clamp(16px, 1.6vw, 24px);
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.card h3 { font-family: var(--pixel-font); font-size: clamp(12px, 1.1vw, 15px); line-height: 1.4; }
/* short context line under a project title (e.g. internship / organisation); never trimmed */
.card .card-meta { margin-top: -6px; color: var(--accent-2); font-size: 12px; line-height: 1.4; }
.card .brief {
  color: var(--muted);
  font-size: 14px;
  line-height: 1.6;
}
.card h4 {
  font-family: var(--pixel-font);
  font-size: 9px;
  color: var(--accent-2);
  margin-bottom: 8px;
}
.card .tags { gap: 6px; }
.card .tags li { font-size: 12px; }
.features { list-style: none; display: flex; flex-direction: column; gap: 6px; font-size: 13.5px; line-height: 1.5; }
.features li { display: flex; gap: 10px; }
.features li::before {          /* pixel-square bullet */
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  margin-top: 0.6em;
  background: var(--accent);
}
.card .view {
  margin-top: auto;               /* button sits at the bottom of the card */
  align-self: flex-start;
  font-size: 10px;
  padding: 10px 14px;
}
/* projects without a public link: same spot and size as the button, but a plain label */
.card .view.static {
  background: transparent;
  color: var(--muted);
  box-shadow: inset 0 0 0 2px var(--soft-line);
  cursor: default;
}
.card .view.static:hover,
.card .view.static:active,
.mini-view.static:hover,
.mini-view.static:active { transform: none; }
.mini-view.static {          /* same plain label as the project cards', small size */
  background: transparent;
  color: var(--muted);
  box-shadow: inset 0 0 0 2px var(--soft-line);
  cursor: default;
}

/* auto-fit (script.js fitCards) on laptops: a card that overflows drops highlights from
   the end (down to two); if any card still overflows, all cards switch to .tight;
   capping the description (.clamp) is the very last resort */
.features li.is-cut { display: none; }
.card.tight h4 { display: none; }
.card.tight .card-body { gap: 9px; }
.card.tight .brief { font-size: 13px; }
.card.tight .features { gap: 3px; font-size: 12.5px; }
.card.tight .tags li { font-size: 11px; }
.card.clamp .brief {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.card.clamp1 .brief { -webkit-line-clamp: 1; }

/* short laptop screens: smaller image strip */
@media (min-width: 761px) and (max-height: 650px) {
  .thumb { height: 56px; }
}

/* "Contact Me" pops up from behind the contact card's top-left edge once the card
   has fully arrived (.contact-done, set in script.js): the wrapper clips at the
   card's top border and the heading slides straight up out of it.
   On laptops it lands where the Projects heading was. */
.contact-head {
  position: absolute;
  left: 0;
  bottom: calc(100% + 4px);                 /* just above the pixel top border */
  padding-bottom: var(--contact-head-gap, calc(12px + clamp(4px, 1.2vh, 12px)));
  overflow: hidden;
  pointer-events: none;
}
.contact-card .contact-head h2 {    /* beats the contact card's own h2 style */
  font-family: var(--pixel-font);
  font-size: clamp(18px, 2.2vw, 30px);
  line-height: 1.4;
  white-space: nowrap;
  transform: translateY(calc(100% + 40px));
  transition: transform 0.6s var(--ease);   /* plain slide, no overshoot */
}
.contact-done .contact-card .contact-head h2 { transform: none; }

/* "More Projects" strip after the last project: slim and full-height, with the label
   running bottom-to-top and a "+" beside it. Clicking it opens the drawer that follows it
   in the track (script.js). On phones it becomes a bar. */
.more-strip {
  flex: 0 0 var(--strip-w);
  position: relative;
  color: var(--text);
}
.more-toggle {
  position: absolute;
  inset: 0;
  width: 100%;
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  cursor: pointer;
}
.more-inner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) rotate(-90deg);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  white-space: nowrap;
  font-family: var(--pixel-font);
  font-size: clamp(16px, 1.6vw, 24px);
  line-height: 1;            /* the page's 1.7 would make the rotated label too thick */
  letter-spacing: 0.2em;
}
.more-plus {
  font-size: 1.5em;
  letter-spacing: 0;
  color: var(--accent-2);
  transition: transform 0.4s var(--ease);
}
.more-strip.is-open .more-plus { transform: rotate(45deg); }   /* + becomes × */
.more-toggle:hover .more-label,
.more-toggle:focus-visible .more-label,
.more-strip.is-open .more-label { color: var(--accent-2); }

/* the drawer: its own item in the track, right after the strip. Closed it takes no room
   (0 width, and a negative margin cancels the track gap before it); open it is exactly
   the rest of the card window beside the strip, so the section physically gets longer
   (script.js adds it to the scroll length). The 3 × 2 grid inside keeps that full width
   and the drawer clips it on the left (leaving room for the cards' pixel border), so as
   the drawer widens the cards slide out from behind the strip, in step with it (same
   duration and easing). */
.more-drawer {
  --drawer-w: calc(var(--window-w, 100vw) - var(--strip-w) - var(--card-gap, 28px));
  position: relative;
  flex: 0 0 0px;
  margin-left: calc(-1 * var(--card-gap, 28px));
  clip-path: inset(-16px -16px -16px calc(-1 * var(--px)));   /* room for the cards' 4px left border */
  visibility: hidden;
  transition: flex-basis 0.6s var(--ease), margin-left 0.6s var(--ease), visibility 0s linear 0.6s;
}
.more-strip.is-open + .more-drawer {
  flex-basis: var(--drawer-w);
  margin-left: 0;
  visibility: visible;
  pointer-events: auto;      /* the projects frame lets clicks through; the open drawer takes them */
  transition-delay: 0s;
}
.more-grid {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: var(--drawer-w);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));   /* minmax: let the one-line text shrink */
  grid-template-rows: repeat(2, minmax(0, 1fr));
  gap: var(--card-gap, 28px);
}
.mini-card {
  --col: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* start tucked behind the strip: shifted left by its column's distance */
  transform: translateX(calc(var(--col) * (-100% - var(--card-gap, 28px))));
  transition: transform 0.6s var(--ease), translate 0.2s ease, box-shadow 0.2s ease;
}
.mini-card:nth-child(3n + 2) { --col: 2; }
.mini-card:nth-child(3n) { --col: 3; }
.more-strip.is-open + .more-drawer .mini-card { transform: none; }
.mini-body { padding: clamp(14px, 1.4vw, 22px); }   /* small cards have no image, just text */
.mini-body h3 { font-family: var(--pixel-font); font-size: clamp(10px, 0.9vw, 13px); line-height: 1.4; }
/* small card: title, a short description (up to two lines), the tech stack and a
   "View project" button */
.mini-body { display: flex; flex-direction: column; gap: 8px; flex: 1; min-height: 0; }
.mini-view {                    /* small "View project" button, pinned to the card's bottom */
  margin-top: auto;
  align-self: flex-start;
  font-size: 9px;
  padding: 8px 10px;
}
.mini-body p {
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
}
/* laptops: script.js (fitMinis) shortens a small card that doesn't fit — the description
   first (3 → 2 → 1 lines), then tags from the end (down to two). Phones show everything. */
.mini-card.d3 p, .mini-card.d2 p, .mini-card.d1 p {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.mini-card.d3 p { -webkit-line-clamp: 3; }
.mini-card.d2 p { -webkit-line-clamp: 2; }
.mini-card.d1 p { -webkit-line-clamp: 1; }
.mini-body .tags li.is-cut { display: none; }
.mini-body .tags { gap: 6px; }
.mini-body .tags li { font-size: 11px; }
/* last card: the GitHub call-to-action, in the accent colour */
.more-cta {
  justify-content: center;
  gap: 12px;
  padding: clamp(14px, 1.6vw, 24px);
  background: var(--accent);
  color: var(--text);
  text-decoration: none;
  box-shadow:
    inset calc(var(--px) * -1) calc(var(--px) * -1) 0 0 rgba(0, 0, 0, 0.35),
    inset var(--px) var(--px) 0 0 rgba(255, 255, 255, 0.2),
    var(--drop, calc(var(--px) * 2)) var(--drop, calc(var(--px) * 2)) 0 0 rgba(0, 0, 0, 0.45);
}
.cta-q { font-family: var(--pixel-font); font-size: clamp(12px, 1.2vw, 18px); line-height: 1.4; }
.cta-go { font-size: clamp(13px, 1vw, 15px); }
.more-cta:hover .cta-go,
.more-cta:focus-visible .cta-go { text-decoration: underline; }

.contact-card {
  position: relative;
  flex-basis: var(--contact-w, 100%);
  align-items: center;
  justify-content: center;
  gap: 28px;
  padding: clamp(24px, 5vw, 64px);
  text-align: center;
}
.contact-card .contact-lead {
  font-family: var(--pixel-font);
  font-size: clamp(18px, 3vw, 40px);
  line-height: 1.4;
  color: var(--text);
}
.contact-card > p { color: var(--muted); font-size: var(--hero-text); }
.socials { list-style: none; display: flex; gap: 24px; flex-wrap: wrap; justify-content: center; }
.socials a { font-family: var(--pixel-font); font-size: 11px; color: var(--muted); text-decoration: none; }
.socials a:hover { color: var(--text); }
.contact-card footer { color: var(--muted); font-size: 12px; }
.contact-card footer time { white-space: nowrap; }

/* ---------- mobile ---------- */
@media (max-width: 760px) {
  /* logo and nav share one row: the nav takes the rest and spreads its links */
  /* logo on the left, menu button on the right; the nav bar becomes the sidebar */
  .topbar { justify-content: flex-start; padding: 20px 16px; }
  .brand { position: static; flex: none; width: 44px; height: 44px; }
  .main-nav { display: none; }
  .menu-btn { display: block; }
  .menu-backdrop { display: block; }
  .side-menu { display: flex; }

  /* intro: a plain vertical flow on phones (no pinned, scroll-driven stage).
     The stage box is dissolved so its children stack inside .intro:
     hero text → portrait → About → portrait → Skills. Each one rises in as it
     scrolls into view (.in is set in script.js). */
  .intro { height: auto; display: flex; flex-direction: column; }
  .stage { display: contents; }
  .intro .stage > * {
    position: relative;
    inset: auto;
    margin-inline: 16px;
    opacity: 0;
    transform: translateY(60px);
    pointer-events: none;
  }
  .intro .stage > .in { opacity: 1; transform: none; pointer-events: auto; }

  /* hero → About: the hero (text + portrait) stays pinned where it sits and fades out
     as About and its portrait scroll up over it. --stick-top and the hero opacity are
     set in script.js; About and its portrait skip the rise-in (the scroll is the slide). */
  .intro .stage > .hero-left,
  .intro .stage > .hero-right,
  .intro .stage > .avatar:not(.avatar-about) {
    position: sticky;
    top: var(--stick-top, 0);
    /* the hero comes in with the load slides (hero-in-*), so it skips the rise-in */
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
  /* the fade uses filter, not opacity, so it follows the finger instead of easing
     behind it (opacity keeps its transition for the rise-in on load) */
  .intro .stage > .hero-left,
  .intro .stage > .hero-right { filter: opacity(var(--hero-fade, 1)); }
  .intro .stage > .avatar:not(.avatar-about) {
    filter: drop-shadow(0 0 30px rgba(0, 0, 0, 0.45)) opacity(var(--hero-fade, 1));
  }
  .stage.hero-gone > .hero-left,
  .stage.hero-gone > .hero-right,
  .stage.hero-gone > .avatar:not(.avatar-about) { pointer-events: none; }
  .intro .stage > .about,
  .intro .stage > .avatar-about { opacity: 1; transform: none; pointer-events: auto; z-index: 1; }

  .hero-left { order: 1; width: auto; max-width: none; margin-top: 96px; font-size: 16px; }
  .hero-left h1 { font-size: 26px; margin: 4px 0 6px; }
  .hero-right { order: 2; width: auto; margin-top: 16px; font-size: 15px; }
  .hint { margin-top: 12px; }

  .intro .stage > .avatar { align-self: center; margin-top: 24px; }
  .avatar:not(.avatar-about) { order: 3; }
  /* hero portrait roughly fills what's left of the first screen */
  .avatar img { height: max(160px, min(48vh, calc(100vh - 400px))); }

  .anchor { position: static; scroll-margin-top: 88px; }  /* nav targets, clear of the top bar */
  #about { order: 4; }
  .about { order: 5; margin-top: var(--about-gap, 72px); }
  .intro .stage > .avatar-about { display: block; order: 6; margin-top: 40px; }  /* room for the bubble tail */
  .avatar-about img { height: min(40vh, 320px); }
  #skills { order: 7; }
  .skills { order: 8; margin-top: 72px; }

  .panel { width: auto; padding: 22px; font-size: 15px; }
  .panel h2 { font-size: 18px; }
  /* tail turns to point down at the portrait (rotation keeps the outline right) */
  .bubble-tail {
    left: calc(50% - 12px);
    top: calc(100% - 8px);
    height: 40px;
    transform: rotate(-90deg);
  }
  .bubble-tail::before {
    clip-path: polygon(
      24px 0, 24px 40px, 16px 40px, 16px 36px, 12px 36px, 12px 32px, 8px 32px,
      8px 28px, 4px 28px, 4px 24px, 0 24px, 0 16px, 4px 16px, 4px 12px, 8px 12px,
      8px 8px, 12px 8px, 12px 4px, 16px 4px, 16px 0
    );
  }

  /* experience: a plain block on phones; the heading and list rise in as they arrive */
  .experience { margin-top: 0; height: auto; }
  .experience-pin { position: static; height: auto; display: block; padding: 96px 16px 0; transform: none; opacity: 1; }
  .experience-head { font-size: 18px; margin-bottom: 20px; }
  .experience-head.in,
  .jobs.in { opacity: 1; transform: none; }
  .jobs { padding: 22px; gap: 20px; }
  .job + .job { padding-top: 20px; }

  /* projects: a plain vertical list on phones (no pinning, no sideways track) */
  .projects { margin-top: 0; }
  .projects-pin {
    touch-action: auto;
    position: static;
    height: auto;
    overflow: visible;
    display: block;
    padding: 96px 16px 48px;
  }
  .projects-head { width: auto; margin-bottom: 20px; }
  .projects-head.in { opacity: 1; transform: none; }
  .track-window { width: auto; height: auto; padding: 0; overflow: visible; }
  .track { flex-direction: column; height: auto; gap: 24px; transform: none; }
  .card { flex: none; }
  .thumb { height: 140px; }
  /* the last card fills the screen below the nav and its "Contact Me" heading, like it
     fills the track on desktop. Extra space above it (156px in all with the gap)
     keeps the previous card, including its pixel border and shadow, off screen
     once it lands 140px down. */
  .contact-card { flex-basis: auto; min-height: calc(100vh - 140px - 48px); margin-top: 132px; }
  .contact-head { --contact-head-gap: 14px; }
  .contact-card .contact-head h2 { font-size: 18px; }
  .more-strip { flex: none; min-height: 64px; }
  .more-toggle { position: relative; height: 64px; }
  .more-inner { transform: translate(-50%, -50%); flex-direction: row; gap: 14px; font-size: clamp(14px, 4.2vw, 16px); }
  /* phones: the drawer opens below the bar as a single column of full-width cards,
     spaced like the project list above it, each dropping in a beat after the last */
  .more-drawer { display: none; flex: none; margin: 0; clip-path: none; visibility: visible; transition: none; }
  .more-strip.is-open + .more-drawer { display: block; }
  .more-grid {
    position: static;
    width: auto;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    grid-auto-rows: 1fr;      /* every card as tall as the tallest, GitHub card included */
    gap: 24px;
  }
  .mini-card { transform: none; transition: none; }
  .more-strip.is-open + .more-drawer .mini-card { animation: mini-drop 0.45s var(--ease) backwards; }
  .more-strip.is-open + .more-drawer .mini-card:nth-child(2) { animation-delay: 0.05s; }
  .more-strip.is-open + .more-drawer .mini-card:nth-child(3) { animation-delay: 0.1s; }
  .more-strip.is-open + .more-drawer .mini-card:nth-child(4) { animation-delay: 0.15s; }
  .more-strip.is-open + .more-drawer .mini-card:nth-child(5) { animation-delay: 0.2s; }
  .more-strip.is-open + .more-drawer .mini-card:nth-child(6) { animation-delay: 0.25s; }
  .mini-body { padding: 16px 20px 20px; }
  .mini-body h3 { font-size: 14px; }

  .card.in { opacity: 1; transform: none; }
}

/* very short phones: tighten the about panel and contact card so they fit */
@media (max-width: 760px) and (max-height: 600px) {
  .contact-card { gap: 16px; padding: 20px; }
  .panel { padding: 16px; font-size: 14px; line-height: 1.55; }
  .panel h2 { font-size: 15px; }
}

}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition-duration: 0.01ms !important; animation: none !important; }
}
