/* ==========================================================================
   andrewparra.com — shared design system
   ========================================================================== */

@import url('https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');

/* ---- Tokens -------------------------------------------------------------- */
:root {
  --bg: #f5f5f7;
  --bg-elevated: rgba(255, 255, 255, 0.7);
  --bg-elevated-2: rgba(255, 255, 255, 0.92);
  --bg-elevated-3: rgba(255, 255, 255, 0.86);
  --border: rgba(0, 0, 0, 0.08);
  --border-strong: rgba(0, 0, 0, 0.14);
  --text: #1d1d1f;
  --text-dim: #56565c;
  --text-dimmer: #6a6a6f;
  --accent: #7638e1;
  --accent-2: #481bb1;
  --accent-pressed: #481bb1;
  --accent-soft: rgba(118, 56, 225, 0.08);
  --accent-soft-strong: rgba(118, 56, 225, 0.16);
  --overlay-weak: rgba(0, 0, 0, 0.03);
  --shadow-color: 29, 29, 31;
  --wash-1: rgba(118, 56, 225, 0.06);
  --wash-2: rgba(0, 0, 0, 0.025);
  --bg-nav-panel: rgba(255, 255, 255, 0.95);

  --font-display: 'Satoshi', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-body: 'Satoshi', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', monospace;

  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-spring: cubic-bezier(0.16, 1, 0.3, 1);

  --container: 1120px;
  --gutter: 24px;

  color-scheme: light dark;
}

/* ---- Dark mode ------------------------------------------------------------
   Follows the OS/browser preference automatically - no manual toggle, no
   stored state. Every surface, border and text color is a token, so this
   block is the ONLY place light vs. dark is decided; nothing below this
   line branches on theme again. Elevated "glass" surfaces get lighter
   (not darker) than the page background, same convention native dark-mode
   UIs use, and the accent shifts to a slightly lighter, separately
   contrast-checked violet so text and buttons keep meeting WCAG AA against
   the dark background (see DESIGN.md "Dark Mode" for the checked ratios). */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0c0c0f;
    --bg-elevated: rgba(255, 255, 255, 0.055);
    --bg-elevated-2: rgba(255, 255, 255, 0.09);
    --bg-elevated-3: rgba(255, 255, 255, 0.07);
    --border: rgba(255, 255, 255, 0.09);
    --border-strong: rgba(255, 255, 255, 0.17);
    --text: #f2f2f5;
    --text-dim: #a3a3ac;
    --text-dimmer: #85858d;
    --accent: #8555e0;
    --accent-2: #9c68f3;
    --accent-pressed: #6b3fc4;
    --accent-soft: rgba(133, 85, 224, 0.16);
    --accent-soft-strong: rgba(133, 85, 224, 0.26);
    --overlay-weak: rgba(255, 255, 255, 0.06);
    --shadow-color: 0, 0, 0;
    --wash-1: rgba(133, 85, 224, 0.1);
    --wash-2: rgba(255, 255, 255, 0.035);
    --bg-nav-panel: rgba(20, 20, 25, 0.97);
  }
}

/* ---- Reset ----------------------------------------------------------------*/
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; overflow-x: hidden; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
ul { margin: 0; padding: 0; list-style: none; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }

/* Only form fields had their own focus treatment; everything else (nav
   links, buttons, contact rows, the resume/copy-email links) fell back to
   the browser's default ring, which reads as disconnected from the rest of
   the accent-driven system. One rule, keyed off :focus-visible so it only
   shows for keyboard navigation, not every mouse click. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
input, textarea { font: inherit; color: inherit; }
a, button, .btn { touch-action: manipulation; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.08;
  margin: 0;
  letter-spacing: -0.01em;
}

p { margin: 0; }

::selection { background: var(--accent); color: #ffffff; }

/* Faint light wash on the whole page for depth under the glass surfaces.
   Drifts very slowly on its own so the page has a pulse even before anyone
   scrolls or moves the cursor - motion that is ambient, not attention-grabbing. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background:
    radial-gradient(ellipse 900px 500px at 12% -10%, var(--wash-1), transparent 60%),
    radial-gradient(ellipse 700px 500px at 100% 10%, var(--wash-2), transparent 55%);
  animation: ambientDrift 26s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes ambientDrift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(-1.5%, 1%, 0) scale(1.03); }
}

/* ---- Layout helpers ------------------------------------------------------*/
.wrap {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--gutter);
  position: relative;
  z-index: 1;
}

.section {
  padding: 96px 0;
  position: relative;
  z-index: 1;
}
.section--tight { padding: 64px 0; }
@media (max-width: 720px) {
  .section { padding: 64px 0; }
  .section--tight { padding: 44px 0; }
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-2);
  margin-bottom: 18px;
}

.section-head {
  max-width: 640px;
  margin-bottom: 56px;
}
.section-head h2 {
  font-size: clamp(28px, 4vw, 40px);
  color: var(--text);
}
.section-head p {
  margin-top: 14px;
  color: var(--text-dim);
  font-size: 17px;
  max-width: 54ch;
}

/* ---- Buttons ---------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 13px 24px;
  border-radius: 999px;
  font-weight: 700;
  font-size: 14.5px;
  white-space: nowrap;
  transition: transform 120ms ease-out, background 160ms var(--ease-out), border-color 160ms var(--ease-out), color 160ms var(--ease-out), box-shadow 220ms var(--ease-out);
  border: 1px solid transparent;
}
.btn:active { transform: scale(0.96); }

.btn--primary {
  background: var(--accent);
  color: #ffffff;
  box-shadow: 0 1px 2px rgba(var(--shadow-color), 0.06), 0 10px 20px -12px rgba(var(--shadow-color), 0.35);
}
@media (hover: hover) and (pointer: fine) {
  .btn--primary:hover { background: var(--accent-pressed); box-shadow: 0 1px 2px rgba(var(--shadow-color), 0.08), 0 12px 22px -12px rgba(var(--shadow-color), 0.4); }
}

.btn--ghost {
  border-color: var(--border-strong);
  color: var(--text);
  background: var(--overlay-weak);
}
@media (hover: hover) and (pointer: fine) {
  .btn--ghost:hover { border-color: var(--accent); color: var(--accent-2); background: var(--accent-soft); }
}

.btn--sm { padding: 9px 18px; font-size: 13px; }

/* ---- Nav ---------------------------------------------------------------- */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 50;
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  background: var(--bg-elevated);
  border-bottom: 1px solid transparent;
  transition: background 240ms var(--ease-out), border-color 240ms var(--ease-out), box-shadow 240ms var(--ease-out);
}
/* Scroll edge effect: the nav "materializes" a hairline and shadow only
   once content actually sits under it, instead of a permanent hard divider. */
.site-nav.is-scrolled {
  background: var(--bg-elevated-3);
  border-bottom-color: var(--border);
  box-shadow: 0 10px 30px -20px rgba(var(--shadow-color), 0.35);
}
.site-nav .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 68px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--font-display);
  font-size: 19px;
  letter-spacing: 0.01em;
}
.brand-mark {
  display: block;
  width: 38px;
  height: 38px;
  flex-shrink: 0;
  transition: transform 220ms var(--ease-spring);
}
.brand-mark svg { display: block; width: 100%; height: 100%; }
.brand-mark .g0 { stop-color: var(--accent); }
.brand-mark .g1 { stop-color: var(--accent-2); }
.brand:hover .brand-mark { transform: rotate(-8deg) scale(1.05); }
.brand-name { display: inline-flex; gap: 5px; }
.brand-first { font-weight: 500; color: var(--text-dim); }
.brand-last { font-weight: 700; color: var(--text); }

.nav-links {
  display: flex;
  align-items: center;
  gap: 4px;
}
.nav-links a {
  position: relative;
  padding: 8px 14px;
  font-size: 14px;
  color: var(--text-dim);
  border-radius: 999px;
  transition: color 160ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .nav-links a:hover { color: var(--text); }
}
.nav-links a[aria-current="page"] { color: var(--text); }
.nav-links a[aria-current="page"]::after {
  content: '';
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 3px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}

.nav-cta { display: flex; align-items: center; gap: 10px; }

.nav-toggle {
  display: none;
  width: 38px;
  height: 38px;
  border-radius: 10px;
  border: 1px solid var(--border-strong);
  align-items: center;
  justify-content: center;
  transition: transform 140ms var(--ease-out);
}
.nav-toggle:active { transform: scale(0.93); }
.nav-toggle svg { width: 18px; height: 18px; }
.nav-toggle svg line {
  transform-origin: center;
  transition: transform 220ms var(--ease-spring), opacity 220ms var(--ease-out);
}
.nav-toggle[aria-expanded="true"] svg line:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] svg line:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] svg line:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 860px) {
  .nav-links { display: none; }
  .nav-cta .btn--ghost { display: none; }
  .nav-toggle { display: inline-flex; }
}

.mobile-menu {
  display: none;
  position: fixed;
  inset: 68px 0 0 0;
  z-index: 49;
  /* A full-screen nav panel has to fully obscure the page behind it to stay
     legible - unlike the small site-nav bar or a card, which can get away
     with the subtler --bg-elevated-2 glass tint. Over the hero's own accent
     wash and violet text, that lower opacity let content ghost through and
     let backdrop-filter's saturate() amplify the bleed-through into a stray
     violet glow behind the CTA. --bg-nav-panel is dedicated to this and
     kept near-opaque so blur/saturate stay purely a soft edge treatment. */
  background: var(--bg-nav-panel);
  backdrop-filter: blur(24px) saturate(140%);
  -webkit-backdrop-filter: blur(24px) saturate(140%);
  padding: 28px var(--gutter);
  opacity: 0;
  transform: translateY(-10px) scale(0.98);
  transition: opacity 300ms var(--ease-spring), transform 300ms var(--ease-spring);
  pointer-events: none;
}
.mobile-menu.is-open {
  display: block;
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}
/* :not(.btn) so this doesn't fight .btn--primary's own white text on the
   "Download Resume" link below - same selector specificity, and this rule
   came later in the cascade, so it was winning and turning that button's
   label var(--text) (near-black in light mode) instead of white. */
.mobile-menu a:not(.btn) {
  display: block;
  padding: 16px 4px;
  font-family: var(--font-display);
  font-size: 26px;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.mobile-menu .btn { margin-top: 24px; width: 100%; }

/* Staggered entrance for the menu's own links, on top of the panel's fade -
   reuses the site's existing [data-stagger]/--i mechanism (see the Scroll
   reveal block) rather than a bespoke system, just with a snappier,
   menu-appropriate cadence so it settles well inside the panel's own
   300ms open time. */
.mobile-menu a.reveal, .mobile-menu .btn.reveal {
  transform: translateY(8px);
  transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out);
}
.mobile-menu a.reveal.is-visible, .mobile-menu .btn.reveal.is-visible {
  transform: translateY(0);
}
.mobile-menu a[style*="--i"], .mobile-menu .btn[style*="--i"] {
  transition-delay: calc(var(--i, 0) * 40ms);
}

/* ---- Hero ----------------------------------------------------------------*/
.hero {
  padding: 88px 0 60px;
  position: relative;
}
.hero-kicker {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-dim);
  letter-spacing: 0.01em;
  margin-bottom: 20px;
  display: flex;
  align-items: center;
  gap: 10px;
}
.hero-kicker .dot {
  width: 7px; height: 7px; border-radius: 999px; background: #58e08a;
  box-shadow: 0 0 0 3px rgba(88, 224, 138, 0.18);
  animation: liveDotPulse 2.4s ease-in-out infinite;
}
@keyframes liveDotPulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(88, 224, 138, 0.18); }
  50% { box-shadow: 0 0 0 6px rgba(88, 224, 138, 0.06); }
}

/* Inner-page heroes (About/Experience/Projects/Contact) are just an eyebrow,
   h1 and one line of sub-copy - no CTA row or badge to fill the space the
   homepage hero has. Stacked with the following .section's own 96px/64px
   top padding, that reads as an oversized dead gap on mobile, where the
   short hero already occupies less height. .hero--compact tightens only
   that one transition, on mobile only - desktop and the homepage hero
   (which does fill its space) keep the standard rhythm. */
.hero--compact { padding-bottom: 20px; }
@media (max-width: 720px) {
  .hero--compact + .section { padding-top: 32px; }
}

.hero h1 {
  font-size: clamp(40px, 7vw, 76px);
  max-width: 16ch;
}
.hero h1 em {
  font-style: normal;
  font-weight: 900;
  color: var(--accent-2);
}

.hero-sub {
  margin-top: 24px;
  max-width: 58ch;
  font-size: clamp(17px, 2vw, 19px);
  color: var(--text-dim);
}

.hero-actions {
  margin-top: 36px;
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}

/* Quiet credibility badge - same glass-pill treatment as .tag/.copy-email,
   so it reads as part of the existing component language rather than a
   one-off. Gives a fast visual "who do they work for right now" answer
   for a skimming reader, alongside (not instead of) the fuller sentence
   in .hero-sub. */
.hero-current {
  margin-top: 32px;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 9px 16px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg-elevated);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
}
.hero-current-label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-dimmer);
}
.hero-current-value {
  font-size: 13.5px;
  color: var(--text-dim);
}
.hero-current-value strong {
  color: var(--text);
  font-weight: 700;
}

/* ---- Stat strip ----------------------------------------------------------*/
.stat-strip {
  margin-top: 76px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.stat {
  padding: 28px 20px;
  border-right: 1px solid var(--border);
}
.stat:last-child { border-right: none; }
.stat-num {
  font-family: var(--font-mono);
  font-size: clamp(26px, 3.4vw, 38px);
  color: var(--text);
  font-weight: 500;
  letter-spacing: -0.02em;
}
.stat-num .accent { color: var(--accent); }
.stat-label {
  margin-top: 8px;
  font-size: 13px;
  color: var(--text-dim);
  line-height: 1.4;
}
@media (max-width: 760px) {
  .stat-strip { grid-template-columns: repeat(2, 1fr); }
  .stat:nth-child(2) { border-right: none; }
}

/* ---- Pillars / feature cards ----------------------------------------------*/
.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
.grid-2 {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
}
.grid-2--about {
  grid-template-columns: 1.1fr 0.9fr;
  gap: 56px;
  align-items: start;
}
@media (max-width: 860px) {
  .grid-3 { grid-template-columns: 1fr; }
  .grid-2,
  .grid-2--about { grid-template-columns: 1fr; gap: 32px; }
}

/* Asymmetric bento for the three "what I do" pillars - deliberately not an
   equal 3-up row: one wide featured tile plus two stacked smaller tiles. */
.pillars-grid {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  grid-template-rows: repeat(2, 1fr);
  gap: 20px;
}
.pillars-grid .card--featured { grid-row: 1 / 3; }
@media (max-width: 860px) {
  .pillars-grid { grid-template-columns: 1fr; grid-template-rows: none; }
  .pillars-grid .card--featured { grid-row: auto; }
}

.card {
  background: var(--bg-elevated);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 30px;
  box-shadow: 0 1px 2px rgba(var(--shadow-color), 0.04);
  transition: transform 380ms var(--ease-spring), border-color 220ms var(--ease-out), background 220ms var(--ease-out), box-shadow 380ms var(--ease-spring);
}
.card--featured {
  background: linear-gradient(155deg, var(--accent-soft), var(--bg-elevated) 60%);
  border-color: var(--accent-soft-strong);
}
@media (hover: hover) and (pointer: fine) {
  .card:hover {
    border-color: var(--border-strong);
    background: var(--bg-elevated-2);
    transform: translateY(-6px);
    box-shadow: 0 26px 48px -28px rgba(var(--shadow-color), 0.22);
  }
}
/* Icon sits inline beside the heading, not stacked above it in its own
   tile - the stacked-tile-above-heading shape is the generic template
   every AI site generator defaults to, so the icon here is smaller and
   folded into the same row as the title instead of getting its own
   vertical beat. */
.card-head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 14px;
}
.card-icon {
  width: 32px;
  height: 32px;
  border-radius: 9px;
  background: var(--accent-soft);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.card-icon svg { width: 16px; height: 16px; }
.card h2, .card h3 {
  font-size: 20px;
  color: var(--text);
}
.card-head h2, .card-head h3 { margin-bottom: 0; }
.card p {
  color: var(--text-dim);
  font-size: 15px;
}
.card ul { margin-top: 14px; }
.card ul li {
  font-size: 14px;
  color: var(--text-dim);
  padding-left: 18px;
  position: relative;
  margin-top: 8px;
}
.card ul li::before {
  content: '';
  position: absolute;
  left: 0; top: 8px;
  width: 5px; height: 5px;
  border-radius: 999px;
  background: var(--accent);
}

/* ---- Featured project band -------------------------------------------- */
.feature-band {
  border: 1px solid var(--border);
  border-radius: 20px;
  background: linear-gradient(155deg, var(--bg-elevated), var(--bg));
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  padding: 44px;
  display: grid;
  grid-template-columns: 1.1fr 0.9fr;
  gap: 40px;
  align-items: center;
}
@media (max-width: 860px) {
  .feature-band { grid-template-columns: 1fr; padding: 30px; }
}
.tag-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; }
.tag {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-dim);
  border: 1px solid var(--border-strong);
  padding: 5px 11px;
  border-radius: 999px;
}
/* Honest build-status panel, not a decorative stand-in for a product
   screenshot: real phase content pulled from the actual project rollout
   (see projects.html), styled as a compact ledger list rather than
   implying a demo or mockup that doesn't exist. */
.feature-visual {
  border-radius: 14px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  padding: 8px;
  display: grid;
  gap: 2px;
}
.feature-phase {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 16px 14px;
  border-radius: 10px;
  transition: background 220ms var(--ease-out);
}
.feature-phase-num {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--accent);
  padding-top: 3px;
  flex-shrink: 0;
}
.feature-phase h3 { font-size: 14.5px; margin-bottom: 4px; }
.feature-phase p { font-size: 13px; color: var(--text-dim); line-height: 1.5; }
.feature-phase.is-active { background: var(--accent-soft); }
.feature-phase.is-active .feature-phase-num { color: var(--accent-2); }

/* ---- Timeline (experience / roadmap) ------------------------------------ */
.timeline { position: relative; }
.timeline::before {
  content: '';
  position: absolute;
  left: 11px;
  top: 6px;
  bottom: 6px;
  width: 1px;
  background: var(--border-strong);
}
.tl-item {
  position: relative;
  padding: 0 0 48px 44px;
}
.tl-item:last-child { padding-bottom: 0; }
.tl-dot {
  position: absolute;
  left: 3px;
  top: 4px;
  width: 17px;
  height: 17px;
  border-radius: 999px;
  background: var(--bg);
  border: 2px solid var(--accent);
}
.tl-dot::after {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 999px;
  background: var(--accent);
}
.tl-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 6px;
}
.tl-role { margin: 0; font-size: 22px; font-weight: 400; font-family: var(--font-display); color: var(--text); }
.tl-org { color: var(--accent-2); font-size: 15px; font-weight: 500; }
.tl-date {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--text-dimmer);
}
.tl-body { color: var(--text-dim); margin-top: 12px; }
.tl-body ul { margin-top: 12px; display: grid; gap: 10px; }
.tl-body li {
  padding-left: 20px;
  position: relative;
  font-size: 15px;
  line-height: 1.6;
}
.tl-body li::before {
  content: '→';
  position: absolute;
  left: 0;
  color: var(--accent);
  font-size: 13px;
  top: 2px;
}
.tl-stats { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
.tl-stats .pill {
  font-family: var(--font-mono);
  font-size: 12.5px;
  background: var(--accent-soft);
  color: var(--accent-2);
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--accent-soft-strong);
}

/* ---- Skills grid --------------------------------------------------------*/
.skills-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: 16px;
  overflow: hidden;
}
@media (max-width: 720px) { .skills-grid { grid-template-columns: 1fr; } }
.skill-cell {
  background: var(--bg-elevated);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  padding: 26px 28px;
}
.skill-cell h3 {
  font-family: var(--font-mono);
  font-size: 12.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent-2);
  margin-bottom: 12px;
}
.skill-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.skill-tags span {
  font-size: 13.5px;
  color: var(--text-dim);
  background: var(--overlay-weak);
  border: 1px solid var(--border);
  padding: 6px 12px;
  border-radius: 999px;
}

/* ---- Project cards (projects page) --------------------------------------*/
.project-card {
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 36px;
  background: var(--bg-elevated);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 32px;
}
@media (max-width: 860px) { .project-card { grid-template-columns: 1fr; padding: 26px; } }
.project-card + .project-card { margin-top: 22px; }
.project-media { grid-column: 1 / -1; border-radius: 12px; overflow: hidden; border: 1px solid var(--border); }
.project-media img { display: block; width: 100%; height: auto; }

/* ---- Before/after comparison slider (6ave.com project) -------------------
   The "after" screenshot sits in a second layer, clipped to --pos with
   clip-path so only that layer repaints while dragging - the base image,
   the card and everything else stay untouched. --pos lives on .compare
   itself (not a shared ancestor) so a drag never triggers a style recalc
   outside this one small subtree. Degrades gracefully with no JS: the
   default --pos below still renders a real (static) 50/50 split. */
.compare {
  position: relative;
  cursor: ew-resize;
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
  --pos: 50%;
}
.compare-after { position: absolute; inset: 0; clip-path: inset(0 calc(100% - var(--pos)) 0 0); }
.compare::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos);
  width: 3px;
  margin-left: -1.5px;
  background: #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18);
  pointer-events: none;
}
.compare-tag {
  position: absolute;
  top: 14px;
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: #fff;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  padding: 6px 13px;
  border-radius: 999px;
  pointer-events: none;
}
.compare-tag--before { left: 14px; }
.compare-tag--after { right: 14px; }
.compare-hint {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: 12.5px;
  letter-spacing: 0.03em;
  color: #fff;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  padding: 7px 15px;
  border-radius: 999px;
  pointer-events: none;
  opacity: 1;
  transition: opacity 300ms var(--ease-out);
}
.compare-hint.is-hidden { opacity: 0; }
.compare-handle {
  position: absolute;
  top: 50%;
  left: var(--pos);
  width: 52px;
  height: 52px;
  margin: -26px 0 0 -26px;
  border-radius: 50%;
  border: 3px solid #fff;
  background: var(--accent);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.4);
  cursor: ew-resize;
  transition: transform 160ms var(--ease-out);
}
.compare-handle svg { width: 22px; height: 22px; }
.compare-handle:active,
.compare.is-dragging .compare-handle { transform: scale(0.93); }
@media (hover: hover) and (pointer: fine) {
  .compare-handle:hover { transform: scale(1.06); }
  .compare-handle:hover:active,
  .compare.is-dragging .compare-handle:hover { transform: scale(0.93); }
}

/* ---- Legal pages (Privacy / Terms) ---------------------------------------*/
.legal { max-width: 720px; margin: 0 auto; color: var(--text-dim); font-size: 16px; line-height: 1.8; }
.legal h2 { font-size: 22px; color: var(--text); margin-top: 40px; margin-bottom: 12px; }
.legal h2:first-child { margin-top: 0; }
.legal p + h2 { margin-top: 40px; }
.legal p { margin-bottom: 14px; }
.legal ul { margin: 14px 0 14px 20px; display: grid; gap: 8px; }
.legal a { color: var(--accent-2); text-decoration: underline; text-underline-offset: 2px; }
.legal .legal-updated { font-family: var(--font-mono); font-size: 13px; color: var(--text-dimmer); margin-bottom: 32px; }

/* ---- Contact form honeypot -------------------------------------------
   Off-screen (not display:none) so simple bots that skip hidden fields
   still fill it in; a human never sees or tabs to it (tabindex="-1" +
   aria-hidden). Any value on submit means non-human traffic. */
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---- Cookie consent banner --------------------------------------------*/
.cookie-banner {
  position: fixed;
  left: 20px;
  right: 20px;
  bottom: 20px;
  max-width: 560px;
  margin: 0 auto;
  z-index: 80;
  padding: 20px 22px;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: var(--bg-elevated-2);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  box-shadow: 0 20px 60px rgba(var(--shadow-color), 0.25);
  display: flex;
  flex-direction: column;
  gap: 14px;
  transform: translateY(calc(100% + 40px));
  opacity: 0;
  pointer-events: none;
  transition: transform 420ms var(--ease-spring), opacity 300ms var(--ease-out);
}
.cookie-banner.is-visible { transform: translateY(0); opacity: 1; pointer-events: auto; }
.cookie-banner p { font-size: 14px; color: var(--text-dim); line-height: 1.6; }
.cookie-banner p a { color: var(--accent-2); text-decoration: underline; text-underline-offset: 2px; }
.cookie-banner-actions { display: flex; gap: 10px; justify-content: flex-end; }
@media (max-width: 480px) {
  .cookie-banner { left: 12px; right: 12px; bottom: 12px; padding: 18px; }
  .cookie-banner-actions { justify-content: stretch; }
  .cookie-banner-actions .btn { flex: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .cookie-banner { transition: opacity 200ms linear; transform: none; }
}
.project-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px;}
.project-title { font-size: 24px; font-family: var(--font-display); }
.project-date { font-family: var(--font-mono); font-size: 12px; color: var(--text-dimmer); white-space: nowrap; }
@media (max-width: 560px) {
  .project-head { flex-direction: column; align-items: flex-start; gap: 6px; }
  .project-title { font-size: 21px; }
}
.project-col h3 {
  font-family: var(--font-mono);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dimmer);
  margin-bottom: 12px;
}
.project-col ul { display: grid; gap: 9px; }
.project-col li {
  font-size: 14.5px;
  color: var(--text-dim);
  padding-left: 18px;
  position: relative;
  line-height: 1.55;
}
.project-col li::before {
  content: '';
  position: absolute; left: 0; top: 7px;
  width: 5px; height: 5px; border-radius: 999px; background: var(--accent);
}
.project-outcome {
  margin-top: 18px;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

/* ---- Contact page -------------------------------------------------------*/
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 40px;
}
@media (max-width: 860px) { .contact-grid { grid-template-columns: 1fr; } }
.contact-list { display: grid; gap: 14px; margin-top: 28px; }
.contact-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--bg-elevated);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  transition: border-color 220ms var(--ease-out), transform 320ms var(--ease-spring), background 220ms var(--ease-out);
}
/* .contact-row--static: the Location row isn't a link (nowhere to go), so it
   opts out of the press/hover/cursor-glow treatment below that implies
   "clickable" - a false affordance for the one non-interactive row. */
.contact-row--static { cursor: default; }
.contact-row:not(.contact-row--static):active { transform: scale(0.97); transition: transform 140ms var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .contact-row:not(.contact-row--static):hover { border-color: var(--accent); background: var(--bg-elevated-2); transform: translateX(4px); }
  .contact-row:not(.contact-row--static):hover:active { transform: translateX(4px) scale(0.97); }
}
.contact-row .ic {
  width: 38px; height: 38px;
  border-radius: 10px;
  background: var(--accent-soft);
  color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.contact-row .ic svg { width: 18px; height: 18px; }
.contact-row .label { font-size: 12px; color: var(--text-dimmer); font-family: var(--font-mono); }
.contact-row .value { font-size: 15px; color: var(--text); margin-top: 2px; }

.form-field { margin-bottom: 18px; }
.form-field label {
  display: block;
  font-family: var(--font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dimmer);
  margin-bottom: 8px;
}
.form-field input, .form-field textarea {
  width: 100%;
  background: var(--bg-elevated);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 13px 14px;
  font-size: 15px;
  transition: border-color 160ms var(--ease-out), background 160ms var(--ease-out), box-shadow 160ms var(--ease-out);
}
.form-field input:focus, .form-field textarea:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--bg-elevated-2);
  box-shadow: 0 0 0 3px var(--accent-soft-strong);
}

/* Persistent, always-visible fallback in case the mailto handoff doesn't
   fire (no default mail client configured) - not just a JS error state. */
.form-fallback {
  margin-top: 14px;
  font-size: 13px;
  color: var(--text-dimmer);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}
.form-fallback a { color: var(--accent-2); text-decoration: underline; text-underline-offset: 2px; }
.copy-email {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-dim);
  background: var(--overlay-weak);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  padding: 3px 10px;
  transition: border-color 160ms var(--ease-out), color 160ms var(--ease-out), background 160ms var(--ease-out), filter 160ms var(--ease-out), opacity 160ms var(--ease-out), transform 140ms var(--ease-out);
}
.copy-email:active { transform: scale(0.94); }
@media (hover: hover) and (pointer: fine) {
  .copy-email:hover { border-color: var(--accent); color: var(--accent-2); background: var(--accent-soft); }
}
.copy-email.is-copied { border-color: var(--accent); color: var(--accent-2); background: var(--accent-soft); }
/* Brief blur+fade dip while the label text swaps (Copy -> Copied -> Copy),
   so the change reads as one object morphing rather than two labels
   swapping instantly. */
.copy-email.is-swapping { opacity: 0.5; filter: blur(2px); }
@media (prefers-reduced-motion: reduce) {
  .copy-email.is-swapping { opacity: 1; filter: none; }
}
.form-field textarea { resize: vertical; min-height: 120px; }

/* Inline field validation - stays inside the one-accent system: an invalid
   field gets a firmer border and bold ink text, never a new hue, so red/
   green semantics don't creep into a palette that's deliberately one color. */
.field-error {
  display: none;
  align-items: center;
  gap: 5px;
  margin-top: 6px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
}
.field-error.is-visible { display: flex; }
.form-field.has-error input,
.form-field.has-error textarea {
  border-color: var(--text-dimmer);
}

/* Submit outcome - success reuses the accent (the site's existing
   confirmation color, see .copy-email.is-copied); error stays achromatic
   like the field errors above. */
.form-status {
  display: none;
  margin: 14px 0 0;
  padding: 11px 14px;
  border-radius: 10px;
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.5;
}
.form-status.is-visible { display: block; }
.form-status.is-success {
  color: var(--accent-2);
  background: var(--accent-soft);
  border: 1px solid var(--accent-soft-strong);
}
.form-status.is-error {
  color: var(--text);
  background: var(--overlay-weak);
  border: 1px solid var(--border-strong);
}

/* ---- Footer ---------------------------------------------------------------*/
.site-footer {
  border-top: 1px solid var(--border);
  padding: 48px 0;
  position: relative;
  z-index: 1;
}
.footer-inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 20px;
}
.footer-links { display: flex; gap: 20px; flex-wrap: wrap; }
.footer-links a { color: var(--text-dim); font-size: 14px; transition: color 160ms var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .footer-links a:hover { color: var(--accent-2); }
}
.footer-note { color: var(--text-dimmer); font-size: 13px; font-family: var(--font-mono); }
/* Same mono-caps treatment as .footer-note, but its own name: .footer-note
   is scoped to the site footer's copyright line, and the About page's
   "quick facts" labels (BASED IN, CURRENT ROLE, ...) were borrowing it just
   for the look, then overriding its font-size inline every time. */
.fact-label { color: var(--text-dimmer); font-size: 12px; font-family: var(--font-mono); }

/* Repeated verbatim across the homepage CTA, the resume CTA on Experience,
   and the 404 page's two buttons - pulled into one class so the shared
   flex/wrap/gap pattern lives in one place. Each usage keeps its own
   margin-top inline, since that offset is contextual to its section, not
   part of the reusable row pattern itself. */
.cta-row { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }

/* ---- 404 ------------------------------------------------------------------*/
.error-code {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: clamp(56px, 10vw, 88px);
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--accent);
}

/* ---- Scroll reveal --------------------------------------------------------*/
.reveal {
  opacity: 0;
  transform: translateY(18px) scale(0.985);
  transition: opacity 680ms var(--ease-spring), transform 680ms var(--ease-spring);
}
.reveal.is-visible { opacity: 1; transform: translateY(0) scale(1); }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  body::before,
  .hero-kicker .dot {
    animation: none;
  }
}

/* stagger helper: children get incremental delay via inline --i custom prop */
.reveal[style*="--i"] { transition-delay: calc(var(--i, 0) * 70ms); }

/* ---- Page transition (view-transitions fallback fade) --------------------*/
.page-fade {
  animation: pageIn 420ms var(--ease-out);
}
@keyframes pageIn {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .page-fade { animation: none; }
}

@view-transition {
  navigation: auto;
}

::view-transition-old(root) {
  animation: vtOut 260ms var(--ease-in-out) forwards;
}
::view-transition-new(root) {
  animation: vtIn 420ms var(--ease-out) forwards;
}
@keyframes vtOut { to { opacity: 0; transform: translateY(-8px); } }
@keyframes vtIn { from { opacity: 0; transform: translateY(10px); } }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}

/* ---- Interactive glow ------------------------------------------------------
   A cursor-tracked accent highlight under glass surfaces. Purely decorative
   (spatial consistency: the light source follows the same pointer that's
   already there), fine-pointer/hover-capable devices only, and it never
   competes with content because it sits in its own negative-z-index layer
   inside an isolated stacking context - the surface's own background and
   its text/icons always paint above it. JS sets --mx/--my per element on
   pointermove; this block only owns the reveal/positioning math. */
.card, .project-card, .contact-row, .skill-cell {
  position: relative;
  isolation: isolate;
}
.card::before, .project-card::before, .contact-row::before, .skill-cell::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  opacity: 0;
  background: radial-gradient(320px circle at var(--mx, 50%) var(--my, 40%), var(--accent-soft-strong), transparent 68%);
  transition: opacity 320ms var(--ease-out);
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) {
  .card:hover::before, .project-card:hover::before, .contact-row:not(.contact-row--static):hover::before, .skill-cell:hover::before {
    opacity: 1;
  }
}

/* ---- Small interactive details -------------------------------------------*/
/* Icon tiles get a spring "wake up" nudge on hover, echoing the brand-mark
   micro-interaction already used in the nav - same easing, same family. */
.card-icon { transition: transform 260ms var(--ease-spring); }
@media (hover: hover) and (pointer: fine) {
  .card:hover .card-icon { transform: rotate(-6deg) scale(1.08); }
}

/* Stat cells get a quiet tinted wash on hover so a strip of plain numbers
   reads as touchable, not just decorative type. */
.stat { transition: background 220ms var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .stat:hover { background: var(--accent-soft); }
}

/* Inline arrow glyphs nudge forward on hover - "the interface is listening"
   feedback for links that end in a directional cue. */
.btn-arrow {
  display: inline-block;
  transition: transform 200ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .btn:hover .btn-arrow { transform: translateX(3px); }
}

/* Nav links: a fast, subtle underline-grow on hover for the non-active
   links (the active page keeps its permanent ::after underline). Kept
   under 200ms since this is scanned tens of times a day, not a rare event. */
.nav-links a::before {
  content: '';
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 3px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transition: transform 180ms var(--ease-out);
}
.nav-links a[aria-current="page"]::before { display: none; }
@media (hover: hover) and (pointer: fine) {
  .nav-links a:hover::before { transform: scaleX(1); }
}

@media (prefers-reduced-motion: reduce) {
  .card::before, .project-card::before, .contact-row::before, .skill-cell::before {
    display: none;
  }
  .card:hover .card-icon { transform: none; }
}

/* ---- Misc utility ---------------------------------------------------------*/
.text-accent { color: var(--accent); }
.mt-0 { margin-top: 0 !important; }
