/* =========================================================================
   Kamal Mehta — Portfolio
   Design: dark data/tech theme, cyan + violet accents, glassy cards.
   ========================================================================= */

:root {
  --bg: #0a0e17;
  --bg-alt: #0d1220;
  --surface: #121828;
  --surface-2: #182036;
  --border: #232c42;
  /* A second, brighter border for interactive edges. --border is tuned to
     separate panels without drawing the eye, which is right for a card and
     wrong for a form field: at 1.35:1 against the input fill the contact
     inputs were very nearly invisible, and WCAG asks 3:1 of a control's
     boundary. This clears it on both the page and card backgrounds. */
  --border-strong: #4f6495;
  --text: #e8ecf5;
  --text-dim: #9aa5bd;
  /* Was #6b7690, which measured 3.9:1 on the page background — under the 4.5:1
     AA floor for body-size text, and this token carries certificate issuers,
     figure captions and form hints. This is the smallest same-hue lightening
     that clears 4.5:1 on every surface it actually lands on (4.55 on --bg
     through 5.43 on --surface-2). */
  --text-faint: #7e889f;
  --accent: #43d9d9;
  --accent-2: #7c8cff;
  --accent-warm: #ffb454;
  --accent-grad: linear-gradient(120deg, #43d9d9 0%, #7c8cff 100%);
  --success: #5ee6a0;
  --shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.6);
  --radius: 16px;
  --radius-sm: 10px;
  --maxw: 1180px;
  --font-head: "Sora", "Segoe UI", system-ui, sans-serif;
  --font-body: "Inter", "Segoe UI", system-ui, sans-serif;
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }

/* The HTML `hidden` attribute is only a browser default, and a browser default
   loses to any rule written here. So the moment an element gets `display:
   grid` or `display: inline-flex` in this file, setting hidden on it stops
   working — silently, which is the worst way for it to fail. This restores the
   attribute's meaning everywhere at once. The modal opts out cleanly on its
   own further down, using `.modal:not([hidden])`. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

h1, h2, h3, h4 {
  font-family: var(--font-head);
  line-height: 1.15;
  margin: 0;
  letter-spacing: -0.01em;
}

p { margin: 0 0 1em; }
a { color: inherit; text-decoration: none; }
ul { margin: 0; padding: 0; list-style: none; }
img { max-width: 100%; display: block; }
button { font-family: inherit; cursor: pointer; }

.container {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 24px;
}

/* ---------------------------- progress bar ---------------------------- */
#progress-bar {
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  width: 0%;
  background: var(--accent-grad);
  z-index: 1000;
  transition: width 0.1s ease-out;
}

/* ------------------------------- header -------------------------------- */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 900;
  padding: 18px 0;
  transition: all 0.3s ease;
  background: transparent;
}
.site-header.scrolled {
  padding: 12px 0;
  background: rgba(10, 14, 23, 0.85);
  backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid var(--border);
}
.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.logo {
  font-family: var(--font-head);
  font-weight: 800;
  font-size: 1.3rem;
  color: var(--text);
}
.logo .dot { color: var(--accent); }
.nav-links {
  display: flex;
  align-items: center;
  gap: 8px;
}
.nav-links a {
  padding: 8px 16px;
  font-size: 0.94rem;
  font-weight: 500;
  color: var(--text-dim);
  border-radius: 999px;
  transition: all 0.2s ease;
  /* "How I work" is the first multi-word nav label on this site. Without
     this it breaks after "How" the moment the bar gets tight, which turns
     one nav row into two and shoves the hero down the page. */
  white-space: nowrap;
}
.nav-links a:hover { color: var(--text); background: var(--surface-2); }
.nav-links .nav-cta {
  background: var(--accent-grad);
  color: #06121a;
  font-weight: 700;
}
.nav-links .nav-cta:hover { filter: brightness(1.08); color: #06121a; }
.nav-toggle {
  display: none;
  flex-direction: column;
  gap: 5px;
  background: none;
  border: none;
  padding: 8px;
  z-index: 20;
}
.nav-toggle span {
  width: 24px; height: 2px;
  background: var(--text);
  border-radius: 2px;
  transition: all 0.25s ease;
}

/* -------------------------------- hero ---------------------------------- */
.hero {
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: center;
  padding-top: 100px;
  overflow: hidden;
}
.hero-bg { position: absolute; inset: 0; z-index: 0; }
.grid-overlay {
  position: absolute; inset: 0;
  background-image:
    linear-gradient(rgba(67, 217, 217, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(67, 217, 217, 0.055) 1px, transparent 1px);
  background-size: 44px 44px;
  mask-image: radial-gradient(ellipse 80% 60% at 50% 30%, black 40%, transparent 100%);
}
.orbit {
  position: absolute;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.35;
}
.orbit-1 {
  width: 520px; height: 520px;
  background: #43d9d9;
  top: -180px; right: -120px;
  animation: float1 16s ease-in-out infinite;
}
.orbit-2 {
  width: 420px; height: 420px;
  background: #7c8cff;
  bottom: -160px; left: -100px;
  animation: float2 18s ease-in-out infinite;
}
@keyframes float1 { 0%,100% { transform: translate(0,0);} 50% { transform: translate(-30px, 40px);} }
@keyframes float2 { 0%,100% { transform: translate(0,0);} 50% { transform: translate(30px, -30px);} }

.hero-inner { position: relative; z-index: 2; text-align: left; }
.eyebrow {
  color: var(--accent);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: 0.85rem;
  margin-bottom: 14px;
}
.hero-name {
  font-size: clamp(2.6rem, 7vw, 5rem);
  font-weight: 800;
  background: linear-gradient(120deg, #ffffff 30%, #b9c2d8 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  margin-bottom: 10px;
}
.hero-role {
  font-size: clamp(1.1rem, 2.6vw, 1.6rem);
  font-weight: 600;
  color: var(--accent);
  min-height: 1.6em;
  margin-bottom: 18px;
}
.hero-role .cursor { color: var(--text-dim); animation: blink 1s step-start infinite; }
@keyframes blink { 50% { opacity: 0; } }
.hero-tagline {
  max-width: 640px;
  font-size: 1.08rem;
  color: var(--text-dim);
  /* Tightened from 34px when the opener and availability pill were added.
     Three new blocks went into the hero; without clawing spacing back, the
     stats row falls below the fold on a 900px laptop screen. */
  margin-bottom: 26px;
}
.hero-cta {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  /* Was 56px, back when the stats row came straight after. The availability
     pill now sits between them and carries its own 30px, so keeping 56 here
     would open a 120px hole in the middle of the hero. */
  margin-bottom: 26px;
}
.hero-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 18px;
  max-width: 760px;
}
.stat-item {
  padding: 16px 18px;
  background: rgba(255,255,255,0.03);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.stat-value {
  font-family: var(--font-head);
  font-size: 1.7rem;
  font-weight: 800;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.stat-label {
  font-size: 0.78rem;
  color: var(--text-faint);
  margin-top: 2px;
}

.scroll-cue {
  position: absolute;
  bottom: 28px; left: 50%;
  transform: translateX(-50%);
  width: 26px; height: 42px;
  border: 2px solid var(--border);
  border-radius: 20px;
  z-index: 2;
}
.scroll-cue span {
  position: absolute;
  top: 6px; left: 50%;
  width: 5px; height: 8px;
  margin-left: -2.5px;
  background: var(--accent);
  border-radius: 3px;
  animation: scrollcue 1.6s ease infinite;
}
@keyframes scrollcue { 0% { opacity:1; top:6px;} 70% {opacity:0; top:20px;} 100% {opacity:0; top:6px;} }

/* ------------------------------- buttons -------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 13px 26px;
  border-radius: 999px;
  font-weight: 700;
  font-size: 0.95rem;
  border: 1px solid transparent;
  transition: all 0.22s ease;
  white-space: nowrap;
}
.btn-primary {
  background: var(--accent-grad);
  color: #06121a;
}
.btn-primary:hover { transform: translateY(-2px); filter: brightness(1.08); box-shadow: 0 10px 30px -8px rgba(67,217,217,0.5); }
.btn-outline {
  border-color: var(--border);
  color: var(--text);
  background: rgba(255,255,255,0.02);
}
.btn-outline:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-2px); }
.btn-ghost { color: var(--text-dim); }
.btn-ghost:hover { color: var(--accent); }

/* ------------------------------- sections -------------------------------- */
.section { padding: 110px 0; position: relative; }
.section-alt { background: var(--bg-alt); }
.section-kicker {
  color: var(--accent);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.8rem;
  margin-bottom: 12px;
}
.section-title {
  font-size: clamp(1.8rem, 4vw, 2.6rem);
  font-weight: 800;
  max-width: 780px;
  margin-bottom: 18px;
}
.section-sub {
  color: var(--text-dim);
  max-width: 680px;
  font-size: 1.05rem;
  margin-bottom: 44px;
}

/* -------------------------------- about ---------------------------------- */
.about-grid {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  gap: 48px;
  align-items: start;
}
.about-text p { color: var(--text-dim); font-size: 1.05rem; }
.about-text strong { color: var(--text); }
.about-stack { margin-top: 30px; }
.about-stack-label {
  font-family: var(--font-head);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.about-stack .tag-row { margin-top: 12px; }
.about-stack-more {
  display: inline-block;
  margin-top: 16px;
  font-size: 0.85rem;
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.about-stack-more:hover { border-bottom-color: var(--accent); }

/* ------------------------------- education ---------------------------------
   Two cards side by side, and it stops there. This section exists to be
   confirmed at a glance, not read — anything more than period, degree, school
   and one detail line starts competing with Experience directly above it.  */
.edu-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: 20px;
  margin-top: 34px;
}
.edu-item {
  padding: 24px 26px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  border-left: 3px solid var(--accent);
}
.edu-item .edu-period {
  font-family: var(--font-head);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 8px;
}
.edu-item h3 { font-size: 1.08rem; line-height: 1.35; margin: 0 0 6px; }
.edu-item .edu-school { color: var(--text-dim); font-weight: 600; font-size: 0.95rem; }
.edu-item .edu-detail { color: var(--text-faint); font-size: 0.86rem; margin-top: 8px; line-height: 1.55; }

/* ------------------------------ publications -------------------------------
   One card, wide, with the numbers pulled out. The link at the bottom goes to
   the university repository — everything above it is checkable there, which is
   the whole reason this section is worth its scroll depth.                  */
.pub-list { margin-top: 34px; }
.pub-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 32px 34px;
  max-width: 940px;
}
.pub-badge {
  display: inline-block;
  font-family: var(--font-head);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bg);
  background: var(--accent);
  padding: 4px 10px;
  border-radius: 999px;
  margin-bottom: 14px;
}
.pub-title {
  font-size: clamp(1.1rem, 2.2vw, 1.4rem);
  line-height: 1.35;
  margin: 0 0 10px;
}
.pub-meta { color: var(--text-dim); font-size: 0.9rem; margin: 0 0 4px; }
.pub-authors { color: var(--text-faint); font-size: 0.85rem; margin: 0; }
.pub-summary {
  color: var(--text-dim);
  margin: 20px 0 0;
  line-height: 1.7;
}
.pub-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 14px;
  margin-top: 26px;
  padding: 22px 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.pub-stat-value {
  font-family: var(--font-head);
  font-size: clamp(1.35rem, 2.6vw, 1.7rem);
  font-weight: 800;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  line-height: 1.1;
}
.pub-stat-label {
  font-size: 0.78rem;
  color: var(--text-faint);
  margin-top: 4px;
  letter-spacing: 0.02em;
}
/* Repository reach. Sits directly under the stat tiles and is deliberately
   quieter than them — a download count is weaker evidence than a measured
   accuracy figure and must not compete with it for attention. The as-of date
   is dimmer again: present for honesty rather than for reading. */
.pub-reach {
  margin: 14px 0 0;
  font-size: 0.85rem;
  color: var(--text-dim);
  line-height: 1.6;
}
.pub-reach strong { color: var(--text); font-weight: 700; }
.pub-asof { color: var(--text-faint); }

.pub-notes {
  list-style: none;
  margin: 22px 0 0;
  padding: 0;
  display: grid;
  gap: 10px;
}
.pub-notes li {
  position: relative;
  padding-left: 20px;
  color: var(--text-dim);
  font-size: 0.92rem;
  line-height: 1.65;
}
.pub-notes li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent-2);
}
.pub-foot { margin-top: 26px; }
.pub-cite {
  margin: 16px 0 0;
  font-size: 0.78rem;
  color: var(--text-faint);
  line-height: 1.6;
  font-style: italic;
}
.btn-sm { padding: 9px 18px; font-size: 0.85rem; white-space: normal; text-align: center; }

/* ------------------------------- beyond work -------------------------------
   The one section on the page that is not an argument. Kept in the same visual
   language as everything else so it reads as part of the record rather than as
   a personality break.                                                       */
.interest-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: 18px;
  margin-top: 34px;
}
.interest-item {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  padding: 22px 24px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.interest-icon { font-size: 1.4rem; line-height: 1.2; flex: 0 0 auto; }
.interest-item h3 { font-size: 1rem; margin: 0 0 6px; }
.interest-item p { color: var(--text-dim); font-size: 0.9rem; line-height: 1.6; margin: 0; }
.highlight-card {
  background: linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 32px;
}
.highlight-card h3 { font-size: 1.15rem; margin-bottom: 18px; }
.highlight-card ul { display: grid; gap: 14px; }
.highlight-card li {
  padding-left: 26px;
  position: relative;
  color: var(--text-dim);
  font-size: 0.95rem;
}
.highlight-card li::before {
  content: "";
  position: absolute;
  left: 0; top: 7px;
  width: 9px; height: 9px;
  border-radius: 3px;
  background: var(--accent-grad);
}

/* ------------------------------ timeline --------------------------------- */
.timeline {
  position: relative;
  border-left: 2px solid var(--border);
  margin-left: 10px;
  padding-left: 40px;
  display: grid;
  gap: 42px;
}
.timeline-item { position: relative; }
.timeline-item::before {
  content: "";
  position: absolute;
  left: -49.5px; top: 4px;
  width: 16px; height: 16px;
  border-radius: 50%;
  background: var(--bg-alt);
  border: 3px solid var(--accent);
}
.timeline-item .t-period {
  font-size: 0.8rem;
  color: var(--accent);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
/* The tenure pill next to the dates. A hiring manager scanning three roles
   should not have to do date arithmetic to find out how long each one was. */
.timeline-item .t-duration {
  margin-left: 10px;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-dim);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: none;
}
.timeline-item h3 { font-size: 1.25rem; margin: 6px 0 2px; }
.timeline-item .t-company { color: var(--text-dim); font-weight: 600; margin-bottom: 10px; }
.timeline-item .t-location { color: var(--text-faint); font-size: 0.85rem; font-weight: 400; }
.timeline-item p.t-summary { color: var(--text-dim); max-width: 700px; margin: 0; }

/* ------------------------------- projects --------------------------------- */
.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
  gap: 28px;
}
.project-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
}
.project-card:hover {
  transform: translateY(-6px);
  border-color: rgba(67,217,217,0.4);
  box-shadow: var(--shadow);
}
.project-media {
  position: relative;
  aspect-ratio: 16/10;
  background: var(--surface-2);
  overflow: hidden;
  cursor: pointer;
}
.project-media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.4s ease;
}
/* Engineering figures, not photographs. `cover` crops to fill, which on a
   composed plate means cutting the sub-captions off the bottom edge — so these
   are contained whole on a white mount instead. White because the line work is
   drawn for white and stays unreadable on anything else, and inset slightly so
   the plate reads as deliberate rather than as a letterboxing accident. The
   hover zoom is dropped here too: scaling a contained figure pushes its own
   edges out of frame, which is the exact failure this rule exists to prevent. */
.project-media.is-figure {
  background: #fff;
  padding: 10px;
}
.project-media.is-figure img { object-fit: contain; }
.project-card:hover .project-media.is-figure img { transform: none; }
/* The full-bleed scrim exists to keep a white badge legible over a bright
   photo. Over a white figure it just dulls the plate to gray, and the badge —
   a dark disc with a cyan ring — is perfectly legible on white without it. */
.project-media.is-figure .play-badge { background: transparent; }
.project-card:hover .project-media img { transform: scale(1.06); }
.project-media .play-badge {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(6,10,18,0.28);
}
.play-badge .play-circle {
  width: 56px; height: 56px;
  border-radius: 50%;
  background: rgba(10,14,23,0.7);
  border: 2px solid var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.1rem;
  color: var(--accent);
  backdrop-filter: blur(4px);
}
.media-gallery-strip {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 2px;
  height: 100%;
}
.media-gallery-strip img { height: 100%; object-fit: cover; }
.project-body { padding: 24px 26px 26px; display: flex; flex-direction: column; flex: 1; }
.project-company {
  font-size: 0.78rem;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 700;
  margin-bottom: 8px;
}
.project-body h3 { font-size: 1.2rem; margin-bottom: 10px; }
.project-impact {
  font-size: 0.92rem;
  color: var(--accent);
  font-weight: 600;
  background: rgba(67,217,217,0.08);
  border: 1px solid rgba(67,217,217,0.2);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  margin-bottom: 14px;
}
.project-body p.project-desc { color: var(--text-dim); font-size: 0.94rem; flex: 1; }
.tag-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.tag {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-dim);
  background: var(--surface-2);
  border: 1px solid var(--border);
  padding: 5px 11px;
  border-radius: 999px;
}
/* The overflow chip on a project card. Deliberately quieter than a real tag --
   it is a count, not a skill, and it should not compete with the ones it is
   summarizing. The full list is in the case-study modal. */
.tag-more {
  color: var(--text-dim);
  background: transparent;
  border-style: dashed;
  opacity: 0.75;
}

/* --------------------------- focus-area filter ----------------------------- */
/* Chips sit between the section heading and the grid. They're generated from
   SITE_DATA.focusAreas, so this styling has to survive any number of them. */
.proj-filter {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin: 0 auto 18px;
  max-width: 900px;
}
/* On a phone eight chips wrap to seven rows and push the projects almost a
   full screen down — which defeats the point of a filter that's meant to get
   people INTO the work faster. Below 700px it becomes a single swipeable
   strip instead. */
@media (max-width: 700px) {
  .proj-filter {
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding: 2px 18px 8px;
    margin-inline: -18px; /* let the strip bleed to the screen edge so it reads as scrollable */
    max-width: none;
  }
  .proj-filter::-webkit-scrollbar { display: none; }
  .fchip { flex: 0 0 auto; scroll-snap-align: start; }
}
.fchip {
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text-dim);
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 8px 15px;
  border-radius: 999px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}
.fchip:hover { color: var(--text); border-color: var(--accent); transform: translateY(-1px); }
.fchip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.fchip-n {
  font-size: 0.7rem;
  color: var(--text-faint);
  background: var(--surface-2);
  border-radius: 999px;
  padding: 1px 7px;
  min-width: 20px;
  text-align: center;
}
.fchip.active {
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
}
.fchip.active .fchip-n { color: var(--accent); background: rgba(10, 14, 23, 0.75); }

.proj-count {
  text-align: center;
  color: var(--text-dim);
  font-size: 0.88rem;
  margin: 0 0 22px;
  min-height: 1.2em; /* reserve the row so filtering doesn't jog the grid */
}
/* On first load there's no filter and therefore no message — don't leave a
   band of empty space above the grid waiting for one. */
.proj-count:empty { min-height: 0; margin-bottom: 0; }
.proj-count strong { color: var(--text); font-weight: 600; }
.fclear {
  font: inherit;
  font-size: 0.8rem;
  color: var(--accent);
  background: none;
  border: 0;
  cursor: pointer;
  padding: 2px 4px;
  margin-left: 6px;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.fclear:hover { color: var(--text); }

/* Filtered-out cards leave the layout entirely rather than fading in place, so
   the grid closes up and there's no half-visible row of rejects to scroll past. */
.filtered-out { display: none !important; }

/* ------------------------- clickable card affordance ------------------------ */
.project-card.is-openable { cursor: pointer; }
.project-card.is-openable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.project-open {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 16px;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--accent);
}
.project-open span { transition: transform 0.2s ease; }
.project-card:hover .project-open span { transform: translateX(4px); }

/* ---------------------------- live skill chips ----------------------------- */
/* A chip only looks clickable when it actually leads to a project. */
.tag-live {
  font-family: inherit;
  line-height: 1.45; /* buttons don't inherit the span's line box */
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.tag-live:hover { color: var(--text); border-color: var(--accent); background: var(--surface); }
.tag-live:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tag-n {
  font-size: 0.65rem;
  font-weight: 700;
  color: var(--accent);
  background: rgba(67, 217, 217, 0.12);
  border-radius: 999px;
  padding: 1px 6px;
}

/* ----------------------------- certificates -------------------------------- */
.cert-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(270px, 100%), 1fr));
  gap: 18px;
  align-items: start;
}
.cert-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: border-color 0.2s ease, transform 0.2s ease;
}
.cert-card:hover { border-color: rgba(67,217,217,0.4); transform: translateY(-3px); }

/* Variant: no scanned certificate on hand. It keeps the column shape of the
   image cards so the grid row doesn't leave a hole beside it — the alternative
   was demoting the certificate to the end of a list that is ordered by
   relevance, which would have been a layout decision overruling a content one. */
.cert-card.no-image { flex-direction: column; }
.cert-badge {
  flex-shrink: 0;
  width: 48px; height: 48px;
  border-radius: 12px;
  background: var(--accent-grad);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.3rem;
  color: #06121a;
  font-weight: 800;
  font-family: var(--font-head);
}
.cert-card.no-image .cert-badge {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  border-radius: 0;
  font-size: 2.4rem;
  letter-spacing: 0.05em;
}
.cert-card.no-image .cert-info { padding: 16px 18px; }
.cert-info h3 { font-size: 0.95rem; margin-bottom: 4px; line-height: 1.35; }
.cert-info .cert-issuer { color: var(--text-faint); font-size: 0.82rem; }

/* variant: real certificate image — clickable thumbnail + info block.
   This is a <button> rather than a <div> so it can be tabbed to and opened
   with Enter, which means undoing the UA button chrome by hand. */
.cert-thumb {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: inherit;
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  cursor: pointer;
  background: #fff;
}
.cert-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  transition: transform 0.35s ease;
}
.cert-card:hover .cert-thumb img { transform: scale(1.05); }
.cert-thumb .cert-zoom-hint {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  padding: 10px;
  background: linear-gradient(180deg, transparent 55%, rgba(6, 10, 18, 0.45));
  opacity: 0;
  transition: opacity 0.2s ease;
}
.cert-card:hover .cert-zoom-hint { opacity: 1; }
.cert-zoom-hint span {
  width: 30px; height: 30px;
  border-radius: 50%;
  background: rgba(10, 14, 23, 0.75);
  border: 1px solid rgba(255, 255, 255, 0.3);
  display: flex; align-items: center; justify-content: center;
  color: #fff;
  font-size: 0.85rem;
}
.cert-thumb:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: -3px;
}
.cert-card .cert-info-block { padding: 16px 18px; }
.cert-card .cert-info-block h3 { font-size: 0.95rem; margin-bottom: 4px; line-height: 1.35; }
.cert-card .cert-info-block .cert-issuer { color: var(--text-faint); font-size: 0.82rem; }
.cert-note {
  margin-top: 24px;
  padding: 16px 20px;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-faint);
  font-size: 0.85rem;
}

/* -------------------------------- skills ---------------------------------- */
.skills-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 20px;
}
.skill-group {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 22px;
}
.skill-group h3 {
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent);
  margin-bottom: 14px;
}
.skill-group .tag-row { margin-top: 0; }

/* -------------------------------- contact ---------------------------------- */
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1.2fr;
  gap: 40px;
}
.contact-cards {
  display: grid;
  gap: 14px;
  align-content: start;
}
.contact-card {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 18px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition: all 0.2s ease;
}
.contact-card:hover { border-color: var(--accent); transform: translateX(4px); }
.contact-icon { font-size: 1.4rem; }
.contact-label { font-size: 0.78rem; color: var(--text-faint); display: block; }
.contact-value { font-weight: 600; display: block; word-break: break-word; }

.contact-form {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 32px;
  display: grid;
  gap: 18px;
}
.form-row { display: grid; gap: 8px; }
.form-row label { font-size: 0.85rem; color: var(--text-dim); font-weight: 600; }
.form-row input, .form-row textarea {
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  color: var(--text);
  font-family: inherit;
  font-size: 0.95rem;
  resize: vertical;
}
.form-row input:focus-visible, .form-row textarea:focus-visible {
  /* An outline rather than `outline: none` plus a border-color swap. The
     border swap moved a 1px edge between two colors of similar weight, which
     is a weak focus signal at the best of times and disappears entirely in
     forced-colors mode, where author colors are thrown away but outlines
     are kept. */
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--accent);
}
.form-note { font-size: 0.85rem; color: var(--text-faint); min-height: 1.2em; }

/* -------------------------------- footer ---------------------------------- */
.site-footer {
  border-top: 1px solid var(--border);
  padding: 28px 0;
  background: var(--bg-alt);
}
.footer-inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  color: var(--text-faint);
  font-size: 0.85rem;
}
.footer-links { display: flex; gap: 20px; }
.footer-links a:hover { color: var(--accent); }

/* -------------------------------- modal ---------------------------------- */
.modal {
  position: fixed; inset: 0;
  z-index: 2000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
}
.modal:not([hidden]) { display: flex; }
.modal-backdrop {
  position: absolute; inset: 0;
  background: rgba(6,10,18,0.85);
  backdrop-filter: blur(6px);
}
.modal-content {
  position: relative;
  max-width: 900px;
  width: 100%;
  max-height: 86vh;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 28px;
  z-index: 1;
}
.modal-close {
  position: fixed;
  top: 24px; right: max(20px, calc(50vw - 470px));
  width: 40px; height: 40px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 1rem;
  z-index: 5;
}
.modal-close:hover { border-color: var(--accent); color: var(--accent); }
.modal-content img { border-radius: var(--radius-sm); margin-bottom: 14px; width: 100%; }
/* Certificate scans only. Fit to height, not width, so a portrait A4 scan
   fits the modal whole instead of scrolling. width:auto undoes the 100% above
   it; max-width keeps a very wide landscape scan inside the panel. */
.modal-content img.cert-full {
  width: auto;
  max-width: 100%;
  /* The panel is 86vh and its own chrome -- heading, caption, padding, the
     image's own bottom margin -- measured 153px at every desktop width.
     Subtracting it is what makes "fits" true rather than nearly true; a
     flat 72vh left every scan overflowing by about 30px. */
  max-height: calc(86vh - 180px);
  margin-left: auto;
  margin-right: auto;
  display: block;
}
.modal-content .modal-caption { color: var(--text-dim); font-size: 0.9rem; margin-bottom: 20px; }
.modal-video-frame { position: relative; padding-top: 56.25%; border-radius: var(--radius-sm); overflow: hidden; margin-bottom: 14px; background:#000;}
.modal-video-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.modal-content h3 { margin-bottom: 18px; }
.video-pending {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; text-align: center; color: var(--text-dim); padding: 40px 20px;
}

/* ------------------------------ case study --------------------------------- */
/* The panel that opens from a project card. Sized wider than the plain image
   modal because it's read, not glanced at, and reading needs a line length
   that doesn't run to the edge of a desktop screen. */
.modal-content-cs { max-width: 820px; padding: 34px 38px 30px; }
/* The panel takes programmatic focus when it opens, so that Escape and the
   arrow keys have somewhere to land. A halo around an 820px box the visitor
   never tabbed to is noise, so it is suppressed — but only for the pointer
   case. Open the same panel with Enter and :focus-visible still matches, and a
   keyboard visitor gets told where focus went. */
.modal-content-cs:focus:not(:focus-visible) { outline: none; }
.modal-content-cs:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -4px;
}

.cs-kicker {
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 10px;
}
.cs-title { font-size: 1.55rem; line-height: 1.25; margin-bottom: 20px; }

.cs-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px;
  margin-bottom: 28px;
}
.cs-metric {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
}
.cs-metric-v {
  display: block;
  font-family: "Sora", system-ui, sans-serif;
  font-size: 1.35rem;
  font-weight: 700;
  color: var(--accent);
  line-height: 1.15;
  margin-bottom: 4px;
}
.cs-metric-l { display: block; font-size: 0.78rem; color: var(--text-dim); line-height: 1.4; }

.cs-block { margin-bottom: 28px; }
.cs-block h4 {
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 12px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--border);
}
.cs-block p { color: var(--text-dim); line-height: 1.72; }

/* Numbered because the order is the point — this is a sequence of decisions,
   not a bag of bullet points. */
.cs-steps { list-style: none; counter-reset: step; padding: 0; margin: 0; }
.cs-steps li {
  counter-increment: step;
  position: relative;
  padding-left: 40px;
  margin-bottom: 15px;
  color: var(--text-dim);
  line-height: 1.7;
}
.cs-steps li::before {
  content: counter(step);
  position: absolute;
  left: 0;
  top: 1px;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(67, 217, 217, 0.1);
  border: 1px solid rgba(67, 217, 217, 0.32);
  color: var(--accent);
  font-size: 0.76rem;
  font-weight: 700;
}
.cs-summary { color: var(--text-faint); font-size: 0.92rem; line-height: 1.7; margin: -12px 0 28px; }

.cs-note {
  border-left: 3px solid var(--accent-warm);
  background: rgba(255, 180, 84, 0.06);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 16px 20px;
  margin-bottom: 28px;
}
.cs-note-tag {
  display: block;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--accent-warm);
  margin-bottom: 7px;
}
.cs-note p { color: var(--text-dim); font-size: 0.93rem; line-height: 1.65; }

.cs-figure { margin: 0 0 18px; }
/* These are engineering diagrams drawn on white, and they have to stay on white
   to stay readable — inverting or dimming them would wreck the line work. So
   they get an explicit white mount instead: padded, rounded, and matching the
   panel's own corner radius, which reads as a deliberate plate rather than a
   screenshot someone forgot to clean up. */
.cs-figure img {
  display: block;
  width: 100%;
  height: auto;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 14px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
}
.cs-figure figcaption { margin-top: 10px; }
.cs-rule { border: 0; border-top: 1px solid var(--border); margin: 22px 0; }

.cs-links { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 26px; }
.cs-link { color: var(--accent); font-size: 0.88rem; font-weight: 600; }

.cs-foot {
  border-top: 1px solid var(--border);
  padding-top: 22px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
.cs-nav { display: flex; gap: 10px; flex-wrap: wrap; flex: 1 1 320px; }
.cs-navbtn {
  font: inherit;
  text-align: left;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  cursor: pointer;
  color: var(--text);
  max-width: 240px;
  transition: border-color 0.18s ease, transform 0.18s ease;
}
.cs-navbtn:hover { border-color: var(--accent); transform: translateY(-1px); }
.cs-navbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cs-navbtn-next { text-align: right; }
.cs-navdir { display: block; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em; color: var(--accent); margin-bottom: 3px; }
.cs-navtitle { display: block; font-size: 0.82rem; color: var(--text-dim); line-height: 1.35; }
.cs-cta { flex: 0 0 auto; }

@media (max-width: 640px) {
  .modal-content-cs { padding: 26px 20px 24px; }
  .cs-title { font-size: 1.25rem; }
  .cs-metrics { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
  .cs-navbtn { max-width: none; flex: 1 1 100%; }
  .cs-navbtn-next { text-align: left; }
  .cs-cta { width: 100%; text-align: center; }
}

/* ----------------------------- hero opener ---------------------------------
   The three-sentence statement that sits directly under the role heading.

   Each sentence is its own <span> and each span is display:block, so the
   stanza reads as three separate facts rather than one long claim. That is
   the entire point of the block — run them together into a paragraph and the
   register changes from "here is what happened" to "here is what I want you
   to think about me".

   It is the brightest text in the hero after the name, and the tagline
   underneath it is deliberately dimmer and smaller. Hierarchy: name, then
   the number, then the context for the number.                              */
.hero-opener {
  max-width: 660px;
  margin-bottom: 16px;
  padding-left: 18px;
  font-size: clamp(1.08rem, 1.6vw, 1.3rem);
  line-height: 1.55;
  color: var(--text);
  font-weight: 500;
  /* A hairline that fades out downward — marks this as the statement without
     turning it into a pull-quote with a big glowing bar. */
  border-left: 2px solid transparent;
  border-image: linear-gradient(180deg, var(--accent) 0%, rgba(67, 217, 217, 0) 100%) 1;
}
.hero-opener span { display: block; }
.hero-opener span + span { margin-top: 2px; }

/* -------------------------- availability pill ------------------------------
   Stated, never explained. A pill this small cannot carry a narrative even if
   someone tried to put one in it, which is rather the point.                */
.hero-availability {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 30px;
  padding: 6px 14px 6px 12px;
  border: 1px solid rgba(94, 230, 160, 0.28);
  background: rgba(94, 230, 160, 0.07);
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--success);
}
.hero-availability::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--success);
  box-shadow: 0 0 0 3px rgba(94, 230, 160, 0.16);
  flex: 0 0 auto;
}

/* ------------------------------ how I work ---------------------------------
   A band, not a section: no .section padding, no .section-alt background,
   hairline rules top and bottom. Four lines does not deserve the same visual
   weight as Experience or Projects, and giving it that weight would make it
   the loudest thing on the page — the wrong note for this content.          */
.howiwork {
  padding: 68px 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  position: relative;
}
.howiwork-title {
  font-family: var(--font-head);
  font-size: clamp(1.35rem, 2.6vw, 1.75rem);
  font-weight: 800;
  /* Wide enough that the heading holds one line on a desktop screen. Broken
     across two lines it reads as a statement of belief; on one line it reads
     as an observation, which is the register this band is going for. */
  max-width: 760px;
  margin-bottom: 30px;
}
.hiw-list {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 22px 44px;
  max-width: 980px;
}
.hiw-item {
  display: flex;
  align-items: flex-start;
  gap: 14px;
}
/* The number is a marker, not a shout — dimmed accent, sized to sit optically
   level with the first line of text rather than the top of the box. */
.hiw-num {
  flex: 0 0 auto;
  font-family: var(--font-head);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--accent);
  opacity: 0.65;
  padding-top: 3px;
  min-width: 22px;
}
.hiw-item p {
  color: var(--text-dim);
  font-size: 0.97rem;
  line-height: 1.68;
  margin: 0;
}

/* ------------------------------ reveal anim --------------------------------
   Content is visible by default (safe for no-JS, crawlers, slow devices).
   The hidden-then-fade-in effect only activates once js-anim is present on
   <html>, which main.js/inline script only adds when IntersectionObserver
   is confirmed available and about to be wired up. */
.reveal { opacity: 1; transform: none; }
html.js-anim .reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s ease, transform 0.7s ease;
}
html.js-anim .reveal.in-view { opacity: 1; transform: translateY(0); }

/* =========================================================================
   RESPONSIVE
   ========================================================================= */
/* The hamburger doesn't take over until 760px, so between 761 and 980 the
   full seven-link bar has to fit on one row. Adding "How I work" was what
   pushed it over on a tablet; tightening the pill padding buys back the
   ~60px needed without dropping a link or shrinking the type to unreadable. */
/* Nine nav items now (Education and Beyond Work joined). The bar fits at
   1300px+; below that it needs the tighter padding to stay on one row, and
   below 760px it becomes the drawer anyway. */
@media (max-width: 1300px) and (min-width: 761px) {
  .nav-links { gap: 2px; }
  .nav-links a { padding: 8px 11px; font-size: 0.88rem; }
}
@media (max-width: 1040px) and (min-width: 761px) {
  .nav-links { gap: 0; }
  .nav-links a { padding: 7px 8px; font-size: 0.82rem; }
}

@media (max-width: 980px) {
  .about-grid { grid-template-columns: 1fr; }
  .contact-grid { grid-template-columns: 1fr; }
  .hero-stats { grid-template-columns: repeat(2, 1fr); }
  /* Two columns of How I work needs ~440px per column before the lines start
     breaking mid-thought. Below that it reads better as one column. */
  .hiw-list { grid-template-columns: 1fr; gap: 20px; max-width: 640px; }
}

@media (max-width: 760px) {
  .section { padding: 76px 0; }
  .howiwork { padding: 52px 0; }
  .howiwork-title { margin-bottom: 24px; }
  .hero-opener { padding-left: 14px; margin-bottom: 18px; }
  .hero-availability { margin-bottom: 34px; }
  .nav-toggle { display: flex; }
  .nav-links {
    position: fixed;
    top: 0; right: -100%;
    height: 100vh;
    width: min(78vw, 320px);
    background: var(--bg-alt);
    border-left: 1px solid var(--border);
    flex-direction: column;
    justify-content: center;
    align-items: stretch;
    padding: 40px 30px;
    gap: 6px;
    transition: right 0.3s ease;
  }
  .nav-links.open { right: 0; }
  .nav-links a { display: block; text-align: center; padding: 14px; font-size: 1.05rem; }
  .hero { padding-top: 120px; }
  .hero-stats { grid-template-columns: repeat(2, 1fr); }
  .hero-cta { flex-direction: column; align-items: stretch; }
  .hero-cta .btn { justify-content: center; }
  .timeline { padding-left: 28px; margin-left: 4px; }
  .timeline-item::before { left: -37.5px; }
  .media-gallery-strip { grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; }
  .media-gallery-strip img:nth-child(n+4) { display: none; }
  .modal-close { right: 20px; top: 16px; }
  .modal-content { padding: 20px; max-height: 90vh; }
  /* Narrower padding raises the panel to 90vh, and the title wraps to more
     lines down here, so the chrome is bigger, not smaller: 202px measured. */
  .modal-content img.cert-full { max-height: calc(90vh - 230px); }
}

@media (max-width: 480px) {
  .pub-card { padding: 24px 20px; }
  .pub-stats { grid-template-columns: 1fr 1fr; }
  .interest-item { padding: 18px 18px; }
  .edu-item { padding: 20px 20px; }
  .hero-stats { grid-template-columns: 1fr 1fr; }
  .stat-value { font-size: 1.4rem; }
  .container { padding: 0 18px; }
}

/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal { transition: none; opacity: 1; transform: none; }
  .orbit, .scroll-cue span { animation: none; }
}

/* ==========================================================================
   PROJECT ANIMATIONS — hover-to-play cards and the expanded case study
   ========================================================================== */

/* ---------------------------------------------------------------- the card */
/* Poster and video are stacked in the same box. Only the opacity crossfades,
   so the tile never reflows and the grid never jumps under the cursor. */
.project-media.is-anim {
  background: #0a0e17;
  aspect-ratio: 16 / 9;                 /* matches the 1280x720 render exactly */
}
.project-media.is-anim .anim-poster,
.project-media.is-anim .anim-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.project-media.is-anim .anim-video {
  opacity: 0;
  transition: opacity 0.28s ease;
  pointer-events: none;                 /* clicks belong to the card, not the video */
}
.project-media.is-anim .anim-poster {
  opacity: 1;
  transition: opacity 0.28s ease, transform 0.5s ease;
}
/* The zoom-on-hover the other cards use is wrong here: the frame is a composed
   layout with a header rail and a chapter rail, and scaling it crops both. */
.project-card:hover .project-media.is-anim .anim-poster { transform: none; }

.project-media.is-anim.is-playing .anim-video { opacity: 1; }
.project-media.is-anim.is-playing .anim-poster { opacity: 0; }

/* Chips sit above both layers. Small, low-contrast, out of the way of the
   frame content — they are affordances, not decoration. */
.project-media.is-anim .anim-chip {
  position: absolute;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 9px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  line-height: 1;
  color: var(--text);
  background: rgba(10, 14, 23, 0.78);
  border: 1px solid rgba(67, 217, 217, 0.28);
  backdrop-filter: blur(6px);
  transition: opacity 0.25s ease;
}
.project-media.is-anim .anim-hint { left: 10px; bottom: 10px; }
.project-media.is-anim .anim-time {
  right: 10px;
  bottom: 10px;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  border-color: var(--border);
}
.anim-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 0 rgba(67, 217, 217, 0.6);
}
/* The pulse is the only thing on the card that moves before you touch it, and
   it stops the moment the clip starts — at that point the frame is the signal. */
.project-card:hover .project-media.is-anim:not(.is-playing) .anim-dot {
  animation: animDotPulse 1.9s ease-out infinite;
}
@keyframes animDotPulse {
  0%   { box-shadow: 0 0 0 0 rgba(67, 217, 217, 0.55); }
  70%  { box-shadow: 0 0 0 7px rgba(67, 217, 217, 0); }
  100% { box-shadow: 0 0 0 0 rgba(67, 217, 217, 0); }
}
.project-media.is-anim.is-playing .anim-hint { opacity: 0; }

/* The expand glyph only appears on hover — it answers "what happens if I
   click", which is a question nobody has until the cursor is already there. */
.project-media.is-anim .anim-expand {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  font-size: 0.9rem;
  color: var(--accent);
  background: rgba(10, 14, 23, 0.72);
  border: 1px solid rgba(67, 217, 217, 0.3);
  backdrop-filter: blur(6px);
  opacity: 0;
  transform: translateY(-3px);
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.project-card:hover .project-media.is-anim .anim-expand {
  opacity: 1;
  transform: translateY(0);
}

/* File missing or codec unsupported: keep the still, drop the promise. */
.project-media.is-anim.anim-dead .anim-video { display: none; }
.project-media.is-anim.anim-dead .anim-hint { display: none; }

/* ------------------------------------------------ the expanded case study */
.modal-content-anim { max-width: 1040px; padding: 0 0 30px; }
.modal-content-anim .cs-kicker,
.modal-content-anim .cs-title,
.modal-content-anim .cs-lede,
.modal-content-anim .cs-role,
.modal-content-anim .cs-metrics,
.modal-content-anim .cs-block,
.modal-content-anim .cs-note,
.modal-content-anim .cs-links,
.modal-content-anim .cs-foot {
  margin-left: 38px;
  margin-right: 38px;
}

/* Full-bleed at the top of the panel: the clip is the reason the modal opened,
   so it gets the whole width and the panel's own top corners. */
.cs-anim { margin-bottom: 26px; }
.cs-anim-wrap {
  position: relative;
  aspect-ratio: 16 / 9;
  background: #0a0e17;
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  overflow: hidden;
}
.cs-anim-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;   /* never crop the frame — the rails carry information */
  background: #0a0e17;
  border-radius: 0;      /* overrides .modal-content img/video rounding */
  margin: 0;
}

.cs-anim-bar {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 12px 38px 0;
}
.cs-anim-meta {
  font-family: var(--font-body);
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
  white-space: nowrap;
}
/* The sound button sits beside the "Narrated" label rather than over the
   picture: an overlay control would cover the frame it is describing, and this
   row is already where the visitor looks for everything else the clip can do.
   Sized to match a chapter pill so the bar reads as one row of controls. */
.cs-anim-sound {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-dim);
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.cs-anim-sound:hover { border-color: rgba(67, 217, 217, 0.5); color: var(--text); }
.cs-anim-sound.is-on {
  border-color: var(--accent);
  background: rgba(67, 217, 217, 0.09);
  color: var(--text);
}
.cs-sound-ico { font-size: 0.86rem; line-height: 1; }

/* The captions button is the sound button with one letter changed, and that is
   the point: two controls that do the same kind of thing — flip a per-visit
   preference that rides along to the next project — should not look like two
   different kinds of control. Every dimension here is copied rather than
   re-chosen, so the row stays one row of controls instead of a row of
   individually-designed ones. */
.cs-anim-cc {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-dim);
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.cs-anim-cc:hover { border-color: rgba(67, 217, 217, 0.5); color: var(--text); }
.cs-anim-cc.is-on {
  border-color: var(--accent);
  background: rgba(67, 217, 217, 0.09);
  color: var(--text);
}
/* Boxed, because "CC" as bare letters next to the words "Captions on" reads as
   a stutter. The box is the glyph every player already uses, so it is doing
   recognition work the two words alone would not. It is decorative — the
   button is aria-hidden on it and the state lives in the label. */
.cs-cc-ico {
  font-size: 0.56rem;
  line-height: 1;
  letter-spacing: 0.02em;
  padding: 2px 3px;
  border: 1px solid currentColor;
  border-radius: 3px;
  opacity: 0.85;
}

/* A horizontal rail rather than a wrapped block: it reads as a timeline, which
   is what it is, and it never pushes the written copy below the fold. */
.cs-chapters {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding-bottom: 4px;
  scrollbar-width: thin;
  /* Always its own full-width line. Sharing the row with the label and the
     sound button left five pills about 40px short at 1440, and the one that
     got clipped was the last one — a chapter sliced down the middle reads as
     a broken panel, not as a rail with more to the right. min-width: 0 keeps
     it scrolling rather than forcing the bar wider than the panel. */
  flex: 1 0 100%;
  min-width: 0;
}
.cs-chapters::-webkit-scrollbar { height: 5px; }
.cs-chapters::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.cs-chapter {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 11px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-dim);
  cursor: pointer;
  text-align: left;
  transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.cs-chapter:hover { border-color: rgba(67, 217, 217, 0.5); color: var(--text); }
.cs-chapter.is-current {
  border-color: var(--accent);
  background: rgba(67, 217, 217, 0.09);
  color: var(--text);
}
.cs-chapter-t {
  font-family: var(--font-body);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
.cs-chapter-l {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* --------------------------------------------- written copy under the clip */
.cs-lede {
  font-size: 1.02rem;
  line-height: 1.65;
  color: var(--text-dim);
  margin-bottom: 16px;
}
/* "What I owned" is the first question anyone asks about work done on a team,
   and it is the one a portfolio usually leaves them guessing at. */
.cs-role {
  font-size: 0.94rem;
  line-height: 1.6;
  color: var(--text);
  background: rgba(124, 140, 255, 0.07);
  border-left: 2px solid var(--accent-2);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 12px 16px;
  margin-bottom: 22px;
}
.cs-role-tag {
  display: block;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent-2);
  margin-bottom: 5px;
}

.cs-impact {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 12px;
}
.cs-impact li {
  position: relative;
  padding-left: 22px;
  font-size: 0.95rem;
  line-height: 1.62;
  color: var(--text-dim);
}
.cs-impact li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 0.62em;
  width: 7px;
  height: 7px;
  border-radius: 2px;
  background: var(--accent);
  transform: rotate(45deg);
}

/* ------------------------------------------------------------- responsive */
@media (max-width: 720px) {
  .modal-content-anim { max-width: 100%; padding: 0 0 24px; }
  .modal-content-anim .cs-kicker,
  .modal-content-anim .cs-title,
  .modal-content-anim .cs-lede,
  .modal-content-anim .cs-role,
  .modal-content-anim .cs-metrics,
  .modal-content-anim .cs-block,
  .modal-content-anim .cs-note,
  .modal-content-anim .cs-links,
  .modal-content-anim .cs-foot {
    margin-left: 20px;
    margin-right: 20px;
  }
  .cs-anim-bar { padding: 12px 20px 0; }
  /* Two buttons and the label do not fit 350px of usable width. The bar wraps
     rather than overflows, so nothing is ever clipped — but left to itself the
     wrap point lands between the two buttons, which splits a pair. Giving the
     label its own line moves the break to a seam that means something: what
     the clip is, then what you can do to it, then the chapters. */
  .cs-anim-meta { flex: 1 0 100%; }
  .cs-anim { margin-bottom: 20px; }
  /* Five pills never fit a phone, so here the rail really does scroll. A
     scrollbar that only exists while the finger is moving is no help before
     the finger moves, so the right edge fades: the half-visible pill under it
     is the affordance. Narrow only — on a wide screen everything already fits
     and the mask would just fade empty space. */
  .cs-chapters {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 30px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 30px), transparent);
  }
}

/* Reduced motion: the card never animates on its own. The clip in the modal
   still exists and still has controls — it just does not start by itself. */
@media (prefers-reduced-motion: reduce) {
  .project-media.is-anim .anim-video,
  .project-media.is-anim .anim-poster { transition: none; }
  .project-card:hover .project-media.is-anim .anim-dot { animation: none; }
}

/* The close button is fixed and centered on the panel width. The animation
   panel is wider than the standard one, so it needs its own offset or the
   button lands on top of the clip. */
.modal.modal-wide .modal-close { right: max(20px, calc(50vw - 552px)); }
@media (max-width: 1160px) {
  .modal.modal-wide .modal-close { right: 20px; top: 20px; }
}

/* ------------------------- simulation demos (appendix) ----------------------
   The quietest section on the page, and that is the design. It sits between
   Beyond Work (which is `section-alt`) and Contact (which is not), so the
   alternating background gives it a contrast edge on top for free; the
   hairline underneath is what stops it merging into Contact.

   Two columns on a wide screen, one below 900px. The grid floor uses
   minmax(min(Npx, 100%), 1fr) rather than minmax(Npx, 1fr): the plain form
   makes the track wider than its own container on a narrow phone and takes
   the whole page sideways with it. Learned the hard way on five other grids
   in this file.
   -------------------------------------------------------------------------- */
.demos-section {
  border-bottom: 1px solid var(--border);
}
.demo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
  gap: 24px;
  margin-top: 34px;
}
.demo-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
  min-width: 0;
}

/* The poster is a button, not a div with a click handler: keyboard users get
   it for free, and screen readers are told it plays something. */
.demo-media {
  position: relative;
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  border-bottom: 1px solid var(--border);
  background: var(--bg-alt);
  aspect-ratio: 16 / 9;
  cursor: pointer;
  overflow: hidden;
  font: inherit;
  color: inherit;
}
.demo-poster {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* These are screen recordings of a gray simulator, so they sit slightly
     bright against a dark page. Knocking them back means the play button and
     the heading stay the first things the eye lands on. */
  filter: brightness(0.82) saturate(0.95);
  transition: filter 0.25s ease, transform 0.4s ease;
}
.demo-poster-blank {
  display: block;
  width: 100%;
  height: 100%;
  background: linear-gradient(160deg, var(--surface) 0%, var(--surface-2) 100%);
}
.demo-media:hover .demo-poster,
.demo-media:focus-visible .demo-poster {
  filter: brightness(1) saturate(1);
  transform: scale(1.02);
}
.demo-media:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.demo-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 56px;
  height: 56px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 14, 23, 0.72);
  border: 1px solid rgba(232, 236, 245, 0.35);
  color: var(--text);
  backdrop-filter: blur(3px);
  transition: background 0.25s ease, border-color 0.25s ease, transform 0.25s ease;
  pointer-events: none;
}
.demo-media:hover .demo-play,
.demo-media:focus-visible .demo-play {
  background: rgba(67, 217, 217, 0.92);
  border-color: transparent;
  color: #06222a;
  transform: translate(-50%, -50%) scale(1.06);
}
.demo-dur {
  position: absolute;
  right: 10px;
  bottom: 10px;
  padding: 3px 8px;
  border-radius: 6px;
  background: rgba(10, 14, 23, 0.8);
  color: var(--text-dim);
  font-family: var(--font-head);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  pointer-events: none;
}

/* Once a poster has been clicked it is replaced outright by this, so the
   aspect-ratio box has to be reproduced here or the card jumps. */
.demo-media-live {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  border-bottom: 1px solid var(--border);
}
.demo-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}

.demo-body {
  padding: 22px 24px 24px;
}
.demo-title {
  font-size: 1.05rem;
  line-height: 1.35;
  margin: 0 0 10px;
}
.demo-lead {
  color: var(--text-dim);
  font-size: 0.92rem;
  line-height: 1.65;
  margin: 0;
}
.demo-notes {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  display: grid;
  gap: 9px;
}
.demo-notes li {
  position: relative;
  padding-left: 16px;
  color: var(--text-faint);
  font-size: 0.86rem;
  line-height: 1.6;
}
.demo-notes li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 6px;
  height: 1px;
  background: var(--accent);
}

@media (max-width: 760px) {
  .demo-body { padding: 20px 20px 22px; }
  .demo-play { width: 48px; height: 48px; }
}

/* ==========================================================================
   CAPTIONS AND TRANSCRIPTS
   ==========================================================================

   Two surfaces for the same words. The <track> puts them over the picture for
   anyone watching with the sound off — which, on a portfolio opened at a desk
   in an open-plan office, is most people. The transcript puts them in the
   page as text, for a screen reader, for someone who would rather read than
   sit through fifty seconds, and for the crawlers that never run JavaScript
   and would otherwise see a 195-character site.
   ========================================================================== */

/* ::cue is styled sparingly on purpose. A caption that fights the frame is
   worse than the browser default, and every rule here is one the UA would
   otherwise decide differently across Chrome, Safari and Firefox: the default
   background is pure black at full opacity in some and translucent in others,
   which changes how much of the animation the text covers. */
video::cue {
  background: rgba(6, 10, 20, 0.82);
  color: #f2f5fb;
  font-family: var(--font-body), system-ui, sans-serif;
  font-size: 0.92em;
  line-height: 1.42;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85);
}

/* ------------------------------------------- transcript inside the modal */
.cs-transcript { margin-top: 10px; }
.cs-transcript > summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding: 7px 13px;
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: color 0.18s ease, border-color 0.18s ease;
}
.cs-transcript > summary::-webkit-details-marker { display: none; }
.cs-transcript > summary::before {
  content: "▸";
  font-size: 0.7em;
  transition: transform 0.18s ease;
}
.cs-transcript[open] > summary::before { transform: rotate(90deg); }
.cs-transcript > summary:hover,
.cs-transcript > summary:focus-visible {
  color: var(--text);
  border-color: var(--border-strong);
}
.cs-transcript-body {
  margin-top: 14px;
  padding-left: 14px;
  border-left: 2px solid var(--border);
}
.cs-transcript-body p {
  font-size: 0.95rem;
  line-height: 1.66;
  color: var(--text-dim);
  margin: 0 0 12px;
}
.cs-transcript-body p:last-child { margin-bottom: 0; }

/* ---------------------------------------- the static section on the page */
/* Deliberately quiet. This is a reference shelf, not a pitch — it sits below
   the projects grid, closed, costing one row of the page until someone wants
   it. The words inside are in the DOM whether it is open or not, which is the
   entire reason it is markup rather than a fetch. */
.section-tx { padding-top: 0; }
.tx-all,
.tx-item { border-radius: var(--radius-sm); }
.tx-all {
  border: 1px solid var(--border);
  background: var(--surface);
  padding: 4px 6px;
}
.tx-all > summary,
.tx-item > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
  padding: 13px 16px;
  border-radius: var(--radius-sm);
  transition: background 0.18s ease;
}
.tx-all > summary::-webkit-details-marker,
.tx-item > summary::-webkit-details-marker { display: none; }
.tx-all > summary:hover,
.tx-item > summary:hover { background: rgba(255, 255, 255, 0.03); }
.tx-all-l {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
}
.tx-all-n,
.tx-org {
  font-size: 0.8rem;
  color: var(--text-faint);
}
.tx-intro {
  font-size: 0.92rem;
  line-height: 1.62;
  color: var(--text-dim);
  margin: 0 16px 14px;
  max-width: 62ch;
}
.tx-item {
  border-top: 1px solid var(--border);
  margin: 0 6px;
}
.tx-name {
  font-size: 0.94rem;
  font-weight: 600;
  color: var(--text-dim);
}
.tx-item[open] .tx-name { color: var(--text); }
.tx-item > p {
  font-size: 0.93rem;
  line-height: 1.68;
  color: var(--text-dim);
  margin: 0 0 11px;
  padding: 0 16px;
  max-width: 74ch;
}
.tx-item > p:first-of-type { margin-top: 2px; }
.tx-item > p:last-child { margin-bottom: 16px; }

@media (max-width: 720px) {
  .tx-all > summary,
  .tx-item > summary { padding: 12px 12px; }
  .tx-item > p { padding: 0 12px; }
  .tx-intro { margin: 0 12px 12px; }
}
