/* ============================================
   VINCENT TRAN PORTFOLIO — DESIGN SYSTEM
   ============================================ */

/* ============================================
   NUNITO SANS — Google Fonts, SIL Open Font Licence
   Self-hosted variable font: weight 200-1000, optical size 6-12.
   wdth and YTLC axes are omitted deliberately — the site never varies
   them, and shipping those axes to sit at their defaults cost 130 KB.
   Latin + Latin-Ext subsets only; unicode-range means most visitors
   download just the ~48 KB latin roman.
   ============================================ */
@font-face {
  font-family: 'Nunito Sans';
  font-style: italic;
  font-weight: 200 1000;
  font-display: swap;
  src: url('../fonts/nunito-sans-italic-latin-ext.woff2') format('woff2-variations');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Nunito Sans';
  font-style: italic;
  font-weight: 200 1000;
  font-display: swap;
  src: url('../fonts/nunito-sans-italic-latin.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Nunito Sans';
  font-style: normal;
  font-weight: 200 1000;
  font-display: swap;
  src: url('../fonts/nunito-sans-normal-latin-ext.woff2') format('woff2-variations');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Nunito Sans';
  font-style: normal;
  font-weight: 200 1000;
  font-display: swap;
  src: url('../fonts/nunito-sans-normal-latin.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --- Variables --- */
:root {
  color-scheme: light;
  /* Light is the default palette. Every colour on the site resolves through these
     tokens, so the dark block below is the only place the theme diverges. The
     ratios are WCAG AA against --bg; both palettes hit the same targets. */
  --bg:             #FAF7F2;
  --bg-rgb:         250, 247, 242;  /* for translucent bars: rgba(var(--bg-rgb), a) */
  --surface:        #F2EEE7;
  --surface-2:      #EAE5DD;
  --text:           #1C1B1A;        /* 16.1:1 — dark grey, never pure black */
  --text-secondary: #5A5854;        /* 6.6:1 */
  --text-muted:     #726F69;        /* 4.7:1 — #75726C fell to 4.49 on the warmer bg */
  --border:         #E3DED5;
  --border-hover:   #CDC7BC;
  --accent:         #1C1B1A;
  --danger:         #A32D25;        /* 6.6:1 */
  --success:        #256E32;        /* 5.9:1 */
  --pattern-rgb:    28, 27, 26;     /* ring canvas stroke, read by js/background.js */
  --shadow:         rgba(28, 27, 26, 0.18);
  --nav-height: 72px;
  --max-width: 1280px;
  --gutter: 80px;
  --gutter-mobile: 24px;
  --radius: 4px;
  /* Typeface. Swap this one value to change the whole site — the @font-face
     block above can stay or go, the stack falls through to system fonts. */
  --font-sans: 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  --transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);

  /* Easing tokens. The built-in curves and the Material curve above are too weak
     for entrances; these are the standard strong pair. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}

/* Dark theme — opt-in via js/theme.js, which sets the attribute before paint. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg:             #141415;
  --bg-rgb:         20, 20, 21;
  --surface:        #1e1e20;
  --surface-2:      #26262a;
  --text:           #f0f0f0;
  --text-secondary: #9b9ca1;        /* 6.9:1 */
  --text-muted:     #7e7f85;        /* 4.6:1 */
  --border:         #292a2d;
  --border-hover:   #45464b;
  --accent:         #f0f0f0;
  --danger:         #e05c5c;        /* 5.1:1 */
  --success:        #5ec46a;        /* 8.4:1 */
  --pattern-rgb:    255, 255, 255;
  --shadow:         rgba(0, 0, 0, 0.5);
}

/* --- Reset --- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* No scroll-behavior: smooth here. ScrollTrigger sets the scroller to 0 to measure
   a pin; with smooth behaviour that assignment animates instead of applying, so any
   refresh while scrolled inside the pinned About carousel recorded start = offset −
   scrollY. The pin then released ~1500px early, leaving an empty spacer and the
   carousel re-appearing below it. Nothing on the site uses anchor scrolling. */
html { font-size: 16px; }
body {
  background: transparent;
  color: var(--text);
  font-family: var(--font-sans);
  font-optical-sizing: auto;
  font-weight: 400;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
img { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
button { cursor: pointer; border: none; background: none; font: inherit; color: inherit; }
ul { list-style: none; }

/* --- Typography --- */
.display {
  font-size: clamp(48px, 8vw, 120px);
  font-weight: 300;
  letter-spacing: -0.03em;
  line-height: 0.95;
}
.display-sm {
  font-size: clamp(36px, 5vw, 72px);
  font-weight: 300;
  letter-spacing: -0.02em;
  line-height: 1;
}
h1 { font-size: clamp(32px, 4vw, 56px); font-weight: 300; letter-spacing: -0.02em; line-height: 1.1; }
h2 { font-size: clamp(24px, 3vw, 40px); font-weight: 300; letter-spacing: -0.01em; }
h3 { font-size: clamp(18px, 2vw, 24px); font-weight: 400; }
h4 { font-size: 16px; font-weight: 500; }
p { font-size: 16px; line-height: 1.75; color: var(--text-secondary); }
.label {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.body-lg { font-size: 18px; line-height: 1.7; }

/* --- Layout --- */
.container {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 var(--gutter);
}
.section { padding: 120px 0; }
.section-sm { padding: 80px 0; }

/* --- Navigation --- */
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--nav-height);
  z-index: 100;
  display: flex;
  align-items: center;
  border-bottom: 1px solid transparent;
  transition: border-color var(--transition), background var(--transition);
}
.nav.scrolled {
  border-color: var(--border);
  /* Resting nav is 0.80; scrolled adds 25% for a solid bar over the pattern. */
  background: rgb(var(--bg-rgb));
  /* Fully opaque, so there is nothing behind to blur — dropping the filter
     removes a per-frame compositing cost while scrolling. */
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.nav-inner {
  max-width: var(--max-width);
  width: 100%;
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.nav-logo {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.05em;
  display: flex;
  align-items: center;
  gap: 10px;
}
.nav-logo img { width: 28px; height: 28px; }
.nav-links {
  display: flex;
  align-items: center;
  gap: 40px;
}
.nav-links a {
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
  transition: color var(--transition);
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.nav-links a:hover, .nav-links a.active { color: var(--text); }
.nav-num {
  font-size: 10px;
  color: var(--text-muted);
}
/* Same weight as .nav-num, but for the one nav item that leaves the site
   rather than linking to another of its own pages — App Workbench — so it
   reads as part of the same row without borrowing the "0X" numbering that
   implies an internal page in sequence. */
.nav-ext-icon {
  font-size: 10px;
  color: var(--text-muted);
}

/* --- Hero (Home) --- */
.hero {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-bottom: 80px;
  padding-top: var(--nav-height);
}
/* With the fox strip in it the hero ends on the grass, so the bottom padding
   goes — the strip is the spacing. Inert while the strip lives above the
   footer instead; harmless either way. */
.hero:has(.hero-game) { padding-bottom: 0; }
.hero .hero-game { margin-top: 64px; }
/* Placement B: a full-bleed band closing the page, above the footer. */
.hero-game--band { margin-top: 0; }
.hero-content { max-width: var(--max-width); margin: 0 auto; padding: 0 var(--gutter); width: 100%; }
.hero-eyebrow {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 32px;
}
.hero-title {
  font-size: clamp(56px, 9vw, 130px);
  font-weight: 700;          /* Nunito Sans Bold */
  letter-spacing: -0.01em;   /* tracking -10 (Adobe units: -10/1000 em) */
  line-height: 0.92;
  margin-bottom: 48px;
}
.hero-title em { font-style: normal; color: var(--text-secondary); }   /* emphasis by colour only */
.hero-footer {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-top: 0;
}
.hero-copy { max-width: 420px; }
.hero-desc {
  max-width: 420px;
  font-size: 15px;
  line-height: 1.7;
  color: var(--text-secondary);
}
.hero-credit {
  margin-top: 16px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-muted);
}
.hero-credit:empty { display: none; }
.hero-links {
  display: flex;
  gap: 24px;
  align-items: center;
}
.hero-links a {
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  transition: color var(--transition);
  border-bottom: 1px solid var(--border);
  padding-bottom: 2px;
}
.hero-links a:hover { color: var(--text); border-color: var(--text-secondary); }

/* --- Section Header --- */
.section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 64px;
  padding-bottom: 24px;
  border-bottom: 1px solid var(--border);
}
.section-header h2 { font-size: clamp(20px, 2.5vw, 32px); font-weight: 300; }
.section-header a {
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  transition: color var(--transition);
}
.section-header a:hover { color: var(--text); }

/* --- Project Grid --- */
.project-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 2px;
}
.project-card {
  display: block;
  position: relative;
  overflow: hidden;
  background: var(--surface);
  aspect-ratio: 4/3;
  cursor: pointer;
}
.project-card.hidden { display: none; }
.project-card-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), filter 0.4s ease;
  filter: brightness(0.85);
}
.project-card:hover .project-card-img {
  transform: scale(1.04);
  filter: brightness(0.6);
}
.project-card-overlay {
  position: absolute;
  inset: 0;
  padding: 32px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  background: linear-gradient(to top, rgba(0,0,0,0.7) 0%, transparent 50%);
  opacity: 1;
}
.project-card-info { transform: translateY(8px); transition: transform var(--transition); }
.project-card:hover .project-card-info { transform: translateY(0); }
.project-card-org {
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.5);
  margin-bottom: 6px;
}
.project-card-title {
  font-size: clamp(16px, 2vw, 22px);
  font-weight: 400;
  color: #fff;
  margin-bottom: 8px;
  line-height: 1.2;
}
.project-card-sub {
  font-size: 13px;
  color: rgba(255,255,255,0.55);
  line-height: 1.4;
  opacity: 0;
  transition: opacity var(--transition);
}
.project-card:hover .project-card-sub { opacity: 1; }
.lock-badge {
  position: absolute;
  top: 20px;
  right: 20px;
  width: 28px;
  height: 28px;
  background: rgba(0,0,0,0.6);
  border: 1px solid rgba(255,255,255,0.15);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(4px);
}
.lock-badge svg { width: 12px; height: 12px; stroke: rgba(255,255,255,0.6); fill: none; stroke-width: 2; }

/* --- Portfolio Page Filters --- */
.portfolio-header {
  padding-top: calc(var(--nav-height) + 80px);
  padding-bottom: 64px;
}
.portfolio-title { margin-bottom: 48px; }
.filters {
  display: flex;
  gap: 8px;
  align-items: center;
}
.filter-btn {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 8px 16px;
  border: 1px solid var(--border);
  border-radius: 2px;
  transition: all var(--transition);
  cursor: pointer;
}
.filter-btn:hover { color: var(--text); border-color: var(--border-hover); }
.filter-btn.active { color: var(--text); border-color: var(--text-secondary); }

/* --- Project Page --- */
.project-hero {
  padding-top: calc(var(--nav-height) + 80px);
  padding-bottom: 80px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 80px;
}
.project-meta-row {
  display: flex;
  gap: 48px;
  margin-top: 48px;
  padding-top: 40px;
  border-top: 1px solid var(--border);
  flex-wrap: wrap;
}
.project-meta-item {}
.project-meta-item .label { margin-bottom: 8px; }
.project-meta-item p { font-size: 14px; color: var(--text-secondary); }
.project-tags {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 24px;
}
.tag {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  border: 1px solid var(--border);
  padding: 4px 10px;
  border-radius: 2px;
}
.project-hero-img {
  width: 100%;
  max-height: 560px;
  object-fit: cover;
  margin-bottom: 64px;
  border-radius: var(--radius);
}
.project-body { max-width: none; }   /* matches .project-hero-img width */
.project-body h3 {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 16px;
  margin-top: 64px;
}
.project-body h3:first-child { margin-top: 0; }
.project-body p { font-size: 17px; line-height: 1.8; color: var(--text-secondary); margin-bottom: 24px; }
.project-img { width: 100%; margin: 64px 0; border-radius: var(--radius); }
/* --- Gallery carousel (case-study [data-gallery] blocks) ---
   The track is a native scroll-snap container: with no JS it is still a
   swipeable, keyboard-scrollable strip showing every image. Arrows and dots are
   display:none until js/gallery.js sets data-gallery-ready, so they can never
   sit there as dead controls. */
.gallery { margin: 64px 0; }
.gallery-viewport { position: relative; }

.gallery-track {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  border-radius: var(--radius);
}
.gallery-track::-webkit-scrollbar { display: none; }
.gallery-track:focus-visible { outline: 2px solid var(--text-secondary); outline-offset: 3px; }

/* Film strip: every frame is the same height and takes whatever width its own
   aspect ratio asks for. Nothing is cropped and nothing is letterboxed, the widths
   vary the way the source images do, and the next frame is always part-visible at
   the edge — which is what signals the strip can be scrolled. */
.gallery-slide {
  flex: 0 0 auto;
  scroll-snap-align: start;
  /* A frame wider than the track could never be seen whole: snap lands on its
     left edge, so the overflow is unreachable. 16:9 at 640 tall is 1138px in a
     1120px column, so the clamp does bite. */
  max-width: 100%;
}
.gallery-slide img {
  display: block;
  height: 640px;
  width: auto;
  max-width: 100%;
  object-fit: contain;          /* only engages on a frame the clamp shrank */
  border-radius: var(--radius);
  background: var(--surface);
}

.gallery-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: none;                       /* enabled by the ready flag below */
  width: 44px;                         /* WCAG 2.5.5 target size */
  height: 44px;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--text);
  background: rgba(var(--bg-rgb), 0.62);
  backdrop-filter: blur(6px);
  border: 1px solid var(--border);
  transition: opacity 160ms var(--ease-out), background 160ms var(--ease-out);
}
.gallery-nav svg { width: 20px; height: 20px; }
.gallery-nav--prev { left: 12px; }
.gallery-nav--next { right: 12px; }
.gallery-nav:disabled { opacity: 0.28; cursor: default; }
@media (hover: hover) and (pointer: fine) {
  .gallery-nav:not(:disabled):hover { background: rgba(var(--bg-rgb), 0.88); }
}
[data-gallery-ready] .gallery-nav { display: flex; }

.gallery-foot {
  display: none;
  align-items: center;
  gap: 8px;
  margin-top: 20px;
}
[data-gallery-ready] .gallery-foot { display: flex; }

/* Same indicator as the About manifesto: a hairline rule with a moving fill.
   Sized as a thumb (share of the strip that is on screen) rather than a 0-to-1
   progress fill, so it is never invisible at rest. */
.gallery-progress {
  position: relative;
  flex: 1;
  height: 1px;
  background: var(--border);
  overflow: hidden;
}
.gallery-progress > span {
  position: absolute;
  inset: 0 auto 0 0;
  display: block;
  width: 0;
  background: var(--text-secondary);
}

/* Small screens drop the film strip. A landscape frame at a fixed height is wider
   than the viewport, and snap would jump past its right-hand third — content you
   could never scroll to. Uniform frames instead: same peek, nothing unreachable. */
@media (max-width: 700px) {
  .gallery-slide { flex: 0 0 86%; }   /* % of the track, which is already inset by the gutter */
  .gallery-slide img {
    height: 420px;
    width: 100%;
    max-width: none;
    object-fit: cover;
  }
  .gallery-nav { display: none !important; }   /* swipe is the gesture here */
}

.project-img-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin: 64px 0;
}
.project-img-grid img { width: 100%; border-radius: var(--radius); }
.project-nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 48px 0;
  margin-top: 80px;
  border-top: 1px solid var(--border);
}
.project-nav a {
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  transition: color var(--transition);
}
.project-nav a:hover { color: var(--text); }

/* --- Password Gate --- */
.password-gate {
  position: fixed;
  inset: 0;
  background: var(--bg);
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
}
.password-gate-inner {
  width: 100%;
  max-width: 480px;
  padding: 0 var(--gutter-mobile);
}
.password-gate .gate-logo {
  display: inline-flex;
  align-items: center;
  line-height: 0;
  color: var(--text);
  margin-bottom: 40px;
  transition: color var(--transition), transform var(--transition);
}
.password-gate .gate-logo-mark { width: 56px; height: 56px; }
.password-gate .gate-logo:hover { color: #14a8ad; transform: translateY(-1px); }
.password-gate .label { margin-bottom: 16px; display: block; }
.password-gate h2 { margin-bottom: 16px; }
.password-gate p { margin-bottom: 40px; font-size: 15px; }
.password-form { display: flex; flex-direction: column; gap: 16px; }
.password-input {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 16px;
  font-family: inherit;
  padding: 16px 20px;
  border-radius: var(--radius);
  width: 100%;
  transition: border-color var(--transition);
  outline: none;
}
.password-input:focus { border-color: var(--text-secondary); }
.password-input::placeholder { color: var(--text-muted); }
.password-error {
  font-size: 13px;
  color: var(--danger);
  display: none;
  margin-top: 4px;
}
.password-error.visible { display: block; }
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 16px 32px;
  border-radius: var(--radius);
  transition: all var(--transition);
  cursor: pointer;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
}
.btn:hover { border-color: var(--text-secondary); background: var(--surface); }
.btn-primary {
  background: var(--text);
  color: var(--bg);
  border-color: var(--text);
}
.btn-primary:hover { background: var(--text-secondary); border-color: var(--text-secondary); }

/* --- About Page --- */
.about-hero {
  padding-top: calc(var(--nav-height) + 80px);
  padding-bottom: 80px;
}
/* The hero is split around the full-bleed manifesto, so the trailing half keeps
   the bottom padding only. */
.about-hero--tail { padding-top: 0; }
.about-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 80px;
  align-items: start;
}
.about-skills {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 40px;
}
.exp-list { margin-top: 48px; }
.exp-item {
  padding: 24px 0;
  border-bottom: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
}
.exp-item:first-child { border-top: 1px solid var(--border); }
.exp-role { font-size: 15px; font-weight: 400; color: var(--text); }
.exp-company { font-size: 14px; color: var(--text-secondary); margin-top: 4px; }
.exp-period { font-size: 12px; color: var(--text-muted); white-space: nowrap; }

/* --- Contact Page --- */
.contact-hero {
  padding-top: calc(var(--nav-height) + 80px);
  padding-bottom: 80px;
}
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 80px;
  align-items: start;
  margin-top: 80px;
  border-top: 1px solid var(--border);
  padding-top: 80px;
}
.contact-info p { font-size: 15px; color: var(--text-secondary); margin-bottom: 40px; }
.contact-links { display: flex; flex-direction: column; gap: 16px; }
.contact-link {
  font-size: 14px;
  color: var(--text-secondary);
  transition: color var(--transition);
  display: flex;
  align-items: center;
  gap: 10px;
}
.contact-link:hover { color: var(--text); }
.form-group { margin-bottom: 20px; }
.form-label {
  display: block;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 10px;
}
.form-input, .form-textarea {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 15px;
  font-family: inherit;
  padding: 14px 18px;
  border-radius: var(--radius);
  transition: border-color var(--transition);
  outline: none;
}
.form-input:focus, .form-textarea:focus { border-color: var(--text-secondary); }
.form-input::placeholder, .form-textarea::placeholder { color: var(--text-muted); }
.form-textarea { height: 160px; resize: vertical; }
.form-success {
  font-size: 14px;
  color: var(--success);
  margin-top: 16px;
  display: none;
}
.form-success.visible { display: block; }
.form-error-msg {
  font-size: 14px;
  color: var(--danger);
  margin-top: 16px;
  display: none;
}
.form-error-msg.visible { display: block; }

/* Honeypot: taken out of the layout without `display:none`, which the better
   spam bots know to skip. Off-screen and out of tab order, so no human meets it. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- Footer --- */
.footer {
  border-top: 1px solid var(--border);
  padding: 40px 0;
}
.footer-inner {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.footer-copy { font-size: 12px; color: var(--text-muted); }
.footer-links { display: flex; align-items: center; gap: 24px; }
.footer-links a { font-size: 12px; color: var(--text-muted); transition: color var(--transition); }
.footer-links a:hover { color: var(--text); }

/* Way back into the fox intro, which otherwise only ever plays once a session.
   It sits among the footer links but is bordered, because it starts something
   rather than going somewhere — the border is the .tag treatment, so it reads as
   part of the same quiet vocabulary and not as a call to action. */
.footer-game {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 11px;
  border: 1px solid var(--border);
  border-radius: 2px;
  white-space: nowrap;
  transition: color var(--transition), border-color var(--transition);
}
.footer-links a.footer-game:hover { color: var(--text); border-color: var(--border-hover); }
.footer-game:focus-visible { outline: 1px solid var(--text-secondary); outline-offset: 3px; }
.footer-game svg { display: block; flex: none; opacity: 0.75; }
.footer-game:hover svg { opacity: 1; }

/* HIDDEN. The game now runs in the page itself — the strip above the footer on
   the home page — so this link was a second door to the same room. The markup is
   still in all five footers and nothing else changed; delete this one rule to
   bring it back. Note that while it is hidden, the full-page version at
   intro.html is only reachable by typing the URL. */
.footer-game { display: none; }

/* --- Embedded fox strip ------------------------------------------------------
   The intro game, cut down to a band the fox runs along. Same engine as
   intro.html (js/intro-game.js, switched by data-embed on the canvas); the
   difference is that it paints no sky, so the page's own ground and ring
   pattern carry straight through and the grass reads as part of the page
   rather than as a picture dropped onto it.

   TO REMOVE: delete the .hero-game block from index.html. Nothing else on the
   page depends on it, and intro.html is unaffected. */
.hero-game {
  position: relative;
  width: 100%;
  height: 260px;
  overflow: hidden;
  isolation: isolate;
}
.hero-game canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}
.hero-game canvas:focus { outline: none; }
.hero-game canvas:focus-visible { outline: 1px solid var(--border-hover); outline-offset: -2px; }

/* The one line of instruction, low and quiet — it is a toy, not a task. Held
   back until the pointer is over the strip so it does not read as chrome. */
.hero-game__cue {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #dfe8d6;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s var(--ease-out);
  white-space: nowrap;
}
.hero-game:hover .hero-game__cue,
.hero-game canvas:focus-visible ~ .hero-game__cue { opacity: 0.75; }

/* Hint band and thought bubble. The game positions both in canvas coordinates,
   so inside the strip they only need to be absolute rather than fixed for the
   numbers it already writes to line up. */
.hero-game .hint {
  position: absolute;
  left: 0; right: 0;
  top: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: #dfe8d6;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s var(--ease-out);
  padding: 0 16px;
  text-align: center;
}
.hero-game .hint.is-visible { opacity: 1; }
.hero-game .hint .key { height: 26px; width: auto; display: block; }
.hero-game .hint .key + .key { margin-left: -3px; }
.hero-game .hint .label { margin: 0 10px 0 4px; }
.hero-game .hint .label:last-child { margin-right: 0; }

.hero-game .thought {
  position: absolute;
  left: 0; top: 0;
  width: 0; height: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s var(--ease-out), transform 0.3s var(--ease-out);
  transform: translateY(6px);
}
.hero-game .thought.is-visible { opacity: 1; transform: translateY(0); }
.hero-game .thought-bubble {
  position: absolute;
  left: 50%;
  bottom: 36px;
  transform: translateX(-50%);
  width: max-content;
  max-width: min(240px, 70vw);
  padding: 10px 14px;
  color: #23302a;
  font-size: 12px;
  line-height: 1.4;
  text-align: center;
}
.hero-game .thought-text { position: relative; z-index: 1; }
.hero-game .thought-bubble .key { height: 22px; width: auto; vertical-align: middle; margin-left: 3px; }
.hero-game .thought-cloud { position: absolute; left: 0; top: 0; z-index: 0; pointer-events: none; }

/* Touch controls, inside the strip rather than pinned to the viewport. */
.hero-game .touch-controls {
  display: none;
  position: absolute;
  left: 16px; right: 16px;
  bottom: 12px;
  justify-content: space-between;
  align-items: flex-end;
  pointer-events: none;
}
.hero-game .touch-group { display: flex; gap: 8px; }
.hero-game .touch-btn {
  pointer-events: auto;
  height: 40px;
  padding: 0;
  border: 0;
  background: none;
  user-select: none;
  -webkit-user-select: none;
  transition: transform 0.12s var(--ease-out);
}
.hero-game .touch-btn img { height: 40px; width: auto; display: block; }
.hero-game .touch-btn--jump {
  width: 99px;
  height: 40px;
  background: url('../images/keys/blank-wide.svg') no-repeat center / 100% 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.hero-game .touch-btn--jump span {
  font-size: 12px;
  font-weight: 700;
  color: #282828;
  transform: translateY(-3px);
}
.hero-game .touch-btn.is-down { transform: translateY(2px); }
.hero-game .touch-btn:focus { outline: none; }

@media (pointer: coarse) {
  .hero-game .touch-controls { display: flex; }
  .hero-game__cue { display: none; }   /* the buttons are the instruction */
}
@media (max-width: 768px) {
  .hero-game { height: 200px; }
}

/* --- Divider --- */
.divider { border: none; border-top: 1px solid var(--border); margin: 80px 0; }

/* --- Scroll fade-in --- */
.fade-up {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-up.visible { opacity: 1; transform: translateY(0); }

/* --- Mobile Hamburger --- */
.nav-toggle {
  display: none;
  flex-direction: column;
  gap: 5px;
  padding: 4px;
  cursor: pointer;
}
.nav-toggle span {
  display: block;
  width: 22px;
  height: 1px;
  background: var(--text);
  transition: all var(--transition);
}
.mobile-menu {
  display: none;
  position: fixed;
  inset: 0;
  background: var(--bg);
  z-index: 99;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 32px;
}
.mobile-menu.open { display: flex; }
.mobile-menu a {
  font-size: 28px;
  font-weight: 300;
  letter-spacing: -0.01em;
  color: var(--text-secondary);
  transition: color var(--transition);
}
.mobile-menu a:hover { color: var(--text); }

/* ============================================
   RESPONSIVE
   ============================================ */
@media (max-width: 1024px) {
  :root { --gutter: 48px; }
  .project-grid { grid-template-columns: repeat(2, 1fr); }
  .about-grid { grid-template-columns: 1fr; gap: 48px; }
  .contact-grid { grid-template-columns: 1fr; gap: 48px; }
}

@media (max-width: 768px) {
  :root { --gutter: var(--gutter-mobile); }
  .nav-links { display: none; }
  .nav-toggle { display: flex; }
  .hero-title { font-size: clamp(40px, 11vw, 72px); }
  .hero-footer { flex-direction: column; align-items: flex-start; gap: 32px; }
  .project-grid { grid-template-columns: 1fr; }
  .section { padding: 80px 0; }
  .project-meta-row { gap: 24px; }
  .project-img-grid { grid-template-columns: 1fr; }
  .section-header { flex-direction: column; gap: 16px; }
  .portfolio-header { padding-top: calc(var(--nav-height) + 40px); }
  .footer-inner { flex-direction: column; gap: 16px; text-align: center; }
}

@media (max-width: 480px) {
  .hero-title { font-size: 36px; }
  .filters { flex-wrap: wrap; }
}

/* Video embed (16:9) */
.project-video {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--surface);
  margin: 48px 0 0;
}
.project-video iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* --- Logo mark --- */
.nav-logo { display: inline-flex; align-items: center; line-height: 0; }
.nav-logo-mark {
  width: 26px;
  height: 26px;
  display: block;
  color: #0e8b8f;                      /* brand teal — the site's single accent */
  transition: color 0.3s ease, transform 0.3s ease;
}
.nav-logo:hover .nav-logo-mark { color: #14a8ad; transform: translateY(-1px); }
@media (max-width: 480px) { .nav-logo-mark { width: 22px; height: 22px; } }

/* --- About: role note + definition callout --- */
.exp-note {
  font-size: 12px;
  color: var(--text-muted, #666);
  margin-top: 3px;
  letter-spacing: 0.01em;
}
#aboutDefinition p:last-child { margin-bottom: 0; }
#aboutDefinition strong { color: var(--text, #f0f0f0); font-weight: 500; }

/* ============================================
   MOBILE / TOUCH REFINEMENTS
   ============================================ */

/* iOS Safari zooms the whole page when a focused field is under 16px.
   16px on form controls prevents that jump. */
@media (max-width: 768px) {
  input[type="text"], input[type="email"], input[type="password"],
  input[type="tel"], input[type="url"], textarea, select,
  .form-input, .form-textarea {
    font-size: 16px;
  }
}

/* Touch targets — 44x44 minimum (Apple HIG, WCAG 2.5.5).
   Hit areas are expanded with a transparent overlay rather than padding,
   so the visual layout is unchanged. */
@media (pointer: coarse), (max-width: 768px) {

  /* Hamburger: was 30x15, the primary nav control on mobile. */
  .nav-toggle {
    width: 44px;
    height: 44px;
    align-items: center;
    justify-content: center;
    margin-right: -12px;          /* keeps the icon on the gutter line */
  }

  /* Logo mark sits in a 44px target while staying visually 22px. */
  .nav-logo {
    min-width: 44px;
    min-height: 44px;
    margin-left: -9px;
    padding-left: 9px;
  }

  /* Inline text links: transparent overlay centred on the text. */
  .hero-links a,
  .section-header a,
  .footer-links a,
  .contact-link,
  .project-nav a {
    position: relative;
  }
  .hero-links a::after,
  .section-header a::after,
  .footer-links a::after,
  .contact-link::after,
  .project-nav a::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: 44px;
    min-width: 44px;
  }

  /* Spacing so the enlarged targets don't overlap each other. */
  .hero-links { gap: 28px; row-gap: 20px; flex-wrap: wrap; }
  .footer-links { gap: 24px; row-gap: 14px; flex-wrap: wrap; }

  /* Filter pills are tapped, not clicked. */
  .filter-btn { min-height: 44px; min-width: 44px; padding-top: 10px; padding-bottom: 10px; }
}

/* Long project metadata should stack rather than squeeze on small phones. */
@media (max-width: 420px) {
  .project-meta-row { flex-direction: column; gap: 20px; }
  .exp-item { gap: 12px; }
}

/* Never let a wide asset break the page horizontally. */
img, video, iframe, table, pre { max-width: 100%; }
body { overflow-x: hidden; }


/* ============================================
   BACKGROUND — gradient + animated pattern
   ============================================ */

html { background: var(--bg); }
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  background: var(--bg);
}

/* Pattern canvas sits between the background and the content. */
#bgPattern {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 900ms var(--ease-out);

  /* Mask follows the off-centre origin: fullest at the ring centre on the left,
     softening as the arcs sweep right. */
  -webkit-mask-image: radial-gradient(ellipse 125% 95% at 20% 52%,
                        #000 0%, rgba(0,0,0,0.82) 52%, rgba(0,0,0,0.28) 100%);
          mask-image: radial-gradient(ellipse 125% 95% at 20% 52%,
                        #000 0%, rgba(0,0,0,0.82) 52%, rgba(0,0,0,0.28) 100%);
}
#bgPattern.ready { opacity: 1; }

/* Nav and footer sit over a moving backdrop. */
.nav:not(.scrolled) {
  background: rgba(var(--bg-rgb), 0.80);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
}
.footer { background: rgba(var(--bg-rgb), 0.55); }
.mobile-menu { background: rgba(var(--bg-rgb), 0.97); backdrop-filter: blur(16px); }

/* Nav and footer span the full viewport rather than the 1280px content column,
   so the logo and links sit on the same gutter line as the page edges. */
.nav-inner,
.footer-inner { max-width: none; }

/* Case study body copy sits over the pattern — give it a touch of separation. */
.project-body { position: relative; }

/* --- About page: manifesto layout ---
   Three parts, each one idea. The name of the part is deliberately small and the
   explanation is display type — that inversion is what makes it read as a
   statement rather than as a blog post. Reference: marsrouge.com/manifeste. */
.manifesto-title {
  font-size: clamp(40px, 6.2vw, 96px);
  font-weight: 300;
  letter-spacing: -0.03em;
  line-height: 1.05;
  max-width: 10em;   /* em, not ch — ch in Nunito Sans is ~0.37em and collapses the measure */
  margin-top: 32px;
  margin-bottom: 120px;
}

.manifesto-part {
  display: grid;
  grid-template-columns: minmax(140px, 200px) 1fr;
  gap: 64px;
  padding-top: 48px;
  margin-top: 96px;
  border-top: 1px solid var(--border);
}
.manifesto-part:first-child { margin-top: 0; }

/* Explicit placement, because the panel re-flows into a different grid when the
   carousel takes over and auto-placement would put the media in the wrong cell. */
.manifesto-part__index { grid-column: 1; grid-row: 1 / span 2; }
.manifesto-part__body  { grid-column: 2; grid-row: 1; }
.manifesto-part .manifesto-media { grid-column: 2; grid-row: 2; }

.manifesto-part__index { position: sticky; top: calc(var(--nav-height) + 40px); align-self: start; z-index: 1; }
.manifesto-num {
  display: block;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.12em;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.manifesto-label {
  font-size: 16px;
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.4;
  color: var(--text);
  margin-top: 12px;
}

.manifesto-statement p {
  /* The measure lives on the paragraph, not the wrapper: em here resolves against
     the clamped display size, so the line length holds at every step. */
  max-width: 22em;
  font-size: clamp(24px, 2.6vw, 40px);
  font-weight: 300;
  line-height: 1.3;
  letter-spacing: -0.015em;
  color: var(--text);
}
.manifesto-statement p + p { margin-top: 0.7em; }

.manifesto-media { margin-top: 64px; max-width: 620px; }   /* the words lead; the photo supports */
.manifesto-media img {
  width: 100%;
  max-height: 520px;
  object-fit: cover;
  border-radius: var(--radius);
}
/* The portrait is the only part whose image is a person — hold it back so the
   words still lead, and keep the face in frame when the crop bites. */
.manifesto-part:first-child .manifesto-media img {
  max-height: 480px;
  object-position: 50% 58%;   /* the subject sits ~60% down the source frame */
  filter: grayscale(1) contrast(1.05);
}

.about-closing { margin-top: 128px; max-width: 620px; }
.resume-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 64px; align-items: start; }

/* 1099px is the manifesto breakpoint — below it the pinned carousel hands over to
   the swipe carousel. */
@media (max-width: 1099px) {
  .manifesto-title { margin-bottom: 72px; }
  /* Panel box, margins and media spacing are set by the swipe-carousel block
     further down — this rule only unsticks the index. */
  .manifesto-part__index { position: static; }
}

/* The resume is unrelated to the carousel and keeps its own 900px breakpoint. */
@media (max-width: 900px) {
  .resume-grid { grid-template-columns: 1fr; gap: 40px; }
  .about-closing { margin-top: 80px; }
}

/* --- Resume second column: four foldable sections -----------------------------
   Education, Certifications, Skills and Tools are <details>. On desktop they are
   forced open by about.html and read as a plain heading above a list — no marker,
   no pointer, nothing to click. Below 900px they fold shut so the stacked column
   does not bury the footer. */
.resume-aside { display: flex; flex-direction: column; gap: 44px; }
.resume-fold > summary {
  display: block;          /* drops the native disclosure triangle */
  list-style: none;
  cursor: default;
}
.resume-fold > summary::-webkit-details-marker { display: none; }
.resume-fold > summary::marker { content: ''; }
.resume-fold > .exp-list,
.resume-fold > .about-skills { margin-top: 20px; }
.resume-fold > .about-skills { margin-top: 16px; }


/* --- About page: manifesto carousel -----------------------------------------
   Only active once js/manifesto.js has confirmed GSAP and built the pin. Without
   that class every rule below is inert and the stacked layout above stands, so a
   dead CDN costs the reader a scroll effect, never the content. */
/* .manifesto lives outside .container (see about.html) so the carousel can be
   full-bleed without any inline width. In stacked mode it re-creates the gutter
   the container would have given it. */
.manifesto { padding: 0 var(--gutter); }
.manifesto-track { display: contents; }
.manifesto-progress { display: none; }

body.is-manifesto-carousel .manifesto {
  padding: 0;                 /* the track carries the gutter once it can scroll */
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
}
body.is-manifesto-carousel .manifesto-track {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: clamp(72px, 7vw, 160px);
  padding: 0 var(--gutter);
}
body.is-manifesto-carousel .manifesto-part {
  flex: 0 0 auto;
  width: min(1180px, 82vw);
  height: min(660px, calc(100vh - var(--nav-height) - 160px));
  /* The media column is never narrower than 400px. minmax(0,1fr) on the text side
     so a long unbroken word cannot push the image below its floor. */
  grid-template-columns: minmax(0, 1fr) clamp(400px, 34vw, 470px);
  grid-template-rows: auto 1fr;
  gap: 40px 64px;
  margin-top: 0;
  padding-top: 0;
  border-top: none;
  /* The observer cannot see panels parked off to the right, so the carousel
     opts out of the scroll reveal entirely rather than risk blank panels. */
  opacity: 1 !important;
  transform: none !important;
}
body.is-manifesto-carousel .manifesto-part__index { position: static; grid-column: 1; grid-row: 1; }
/* The panel has a hard height inside an overflow:hidden track, so copy longer than
   the panel would be clipped with no scrollbar and no way to reach it — and this copy
   is admin-editable. Give the body its own scroll instead. min-height:0 is required:
   a grid item will not shrink below its content without it. */
body.is-manifesto-carousel .manifesto-part__body  {
  grid-column: 1; grid-row: 2;
  /* stretch, not center: centring sizes the box to its content, and a 1px
     sub-pixel rounding overflow then showed a scrollbar on every panel. Filling
     the row means overflow-y only engages when the copy really is too long.
     min-height:0 is required or a grid item will not shrink below its content. */
  align-self: stretch;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}
/* auto margins centre it while staying scrollable — justify-content:center would
   make the top unreachable once the content does overflow. */
body.is-manifesto-carousel .manifesto-statement { margin: auto 0; }
/* A panel column is narrower than a full-bleed page column, so the display step
   comes down a notch — 2.6vw put only ~26 characters on a line here. */
body.is-manifesto-carousel .manifesto-statement p { font-size: clamp(24px, 2.15vw, 34px); }
body.is-manifesto-carousel .manifesto-part .manifesto-media {
  grid-column: 2; grid-row: 1 / span 2;
  margin-top: 0; max-width: none; height: 100%;
}
/* The portrait's :first-child height cap out-specifies a plain carousel rule, so
   it is named explicitly here. filter/object-position are left to that rule. */
body.is-manifesto-carousel .manifesto-part:first-child .manifesto-media img,
body.is-manifesto-carousel .manifesto-media img { height: 100%; max-height: none; }

body.is-manifesto-carousel .manifesto-progress {
  display: block;
  flex: 0 0 auto;
  height: 1px;
  background: var(--border);
  margin: 56px var(--gutter) 0;
}
body.is-manifesto-carousel .manifesto-progress > span {
  display: block;
  height: 100%;
  background: var(--text-secondary);
  transform: scaleX(0);
  transform-origin: left center;
}

/* Touch: a native swipe carousel instead of a hijacked scroll. Pure CSS, so it
   needs no JS and does not care whether GSAP loaded. */
@media (max-width: 1099px) {
  .manifesto {
    padding: 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .manifesto::-webkit-scrollbar { display: none; }
  .manifesto-track {
    display: flex;
    align-items: stretch;
    gap: 20px;
    padding: 0 var(--gutter);
  }
  /* Same scrollWidth quirk as the desktop track: neither the padding-right nor a
     trailing margin is counted, so without a real flex item the last panel snaps
     flush against the screen edge. */
  .manifesto-track::after { content: ''; flex: 0 0 var(--gutter); }
  .manifesto-part {
    flex: 0 0 86%;
    scroll-snap-align: center;
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto;
    margin-top: 0;
    padding-top: 32px;
    border-top: none;
  }
  .manifesto-part__index { grid-column: 1; grid-row: 1; }
  .manifesto-part__body  { grid-column: 1; grid-row: 2; }
  .manifesto-part .manifesto-media { grid-column: 1; grid-row: 3; margin-top: 8px; }
  /* Named explicitly, or the portrait's :first-child cap wins and stretches every
     panel to match the tallest one. */
  .manifesto-part:first-child .manifesto-media img,
  .manifesto-media img { max-height: 300px; }
  /* Panels parked off-screen horizontally never intersect, so skip the reveal. */
  .manifesto-part.fade-up { opacity: 1 !important; transform: none !important; }
}

/* --- About page: full-bleed layout --- */
/* The container is capped at 1280px site-wide. The About page runs edge to edge
   instead, so the portrait and the two image sections get real scale. Text keeps
   a readable measure inside the wider frame — full width should widen the layout,
   not the line length. */
.page-about .container { max-width: none; }

.page-about .about-hero { width: 100%; }
.page-about .about-closing { max-width: 52ch; }
.page-about .resume-grid { grid-template-columns: 1fr 1fr; gap: 96px; }

/* Full bleed widens the layout, not the line length. The statement keeps its
   measure and the extra width goes to the gutter between index and body. */
.page-about .manifesto-part { grid-template-columns: minmax(180px, 260px) 1fr; gap: 96px; }
.page-about .manifesto-media { max-width: 760px; }

@media (min-width: 1600px) {
  .page-about .manifesto-part { gap: 128px; }
}
/* Between the stack breakpoint and full desktop the index column and its gutter
   were eating 350px of a 900px line. Tighten both until there is room. */
@media (max-width: 1200px) {
  .page-about .manifesto-part { grid-template-columns: minmax(110px, 150px) 1fr; gap: 48px; }
}
@media (max-width: 1099px) {
  .page-about .manifesto-part { grid-template-columns: 1fr; gap: 20px; }
}
@media (max-width: 900px) {
  .page-about .about-closing { max-width: none; }
  /* The one-column rule above is set on .resume-grid, which this page overrides
     at full specificity further up — so it has to be restated here or the About
     resume stays two cramped columns on a phone. */
  .page-about .resume-grid { grid-template-columns: 1fr; gap: 40px; }

  /* Folded: the summary becomes the tappable row, with a +/- on the right. */
  .resume-aside { gap: 0; }
  .resume-fold { border-bottom: 1px solid var(--border); }
  .resume-fold:first-child { border-top: 1px solid var(--border); }
  .resume-fold > summary {
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    min-height: 56px;
    padding: 8px 0;
    color: var(--text);
    -webkit-tap-highlight-color: transparent;
  }
  .resume-fold > summary::after {
    content: '+';
    font-size: 16px;
    font-weight: 400;
    line-height: 1;
    letter-spacing: 0;
    color: var(--text-muted);
  }
  .resume-fold[open] > summary::after { content: '\2212'; }   /* minus */
  .resume-fold > .exp-list,
  .resume-fold > .about-skills { margin-top: 4px; }
  .resume-fold > :last-child { margin-bottom: 24px; }
  /* Inside a folded section the first row's rule would double up with the
     summary's own bottom border. */
  .resume-fold .exp-item:first-child { border-top: 0; }
}

/* ============================================
   RING CONTROLS
   ============================================ */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.ring-controls {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 60;
  /* The toggle defines the anchor and stays put; the panel is taken out of flow
     and hung above it, so opening the panel cannot move the button. */
  line-height: 0;
}

.ring-controls__toggle {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: rgba(var(--bg-rgb), 0.72);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: var(--text-muted);
  cursor: pointer;
  transition: color 200ms var(--ease-out), border-color 200ms var(--ease-out);
}
.ring-controls__toggle svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.2; }
.ring-controls__toggle:focus-visible { outline: 2px solid var(--text-secondary); outline-offset: 2px; }
.ring-controls.is-open .ring-controls__toggle { color: var(--text); border-color: var(--border-hover); }
@media (hover: hover) and (pointer: fine) {
  .ring-controls__toggle:hover { color: var(--text); border-color: var(--border-hover); }
}

.ring-controls__panel {
  position: absolute;
  right: 0;
  bottom: calc(100% + 12px);
  width: 268px;
  line-height: 1.5;   /* the wrapper zeroes it to kill the button's inline gap */
  text-align: left;
  padding: 18px 18px 14px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: rgba(var(--bg-rgb), 0.94);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  box-shadow: 0 18px 48px var(--shadow);
  /* Never taller than the space available above the toggle. */
  max-height: calc(100vh - 120px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.ring-controls__panel[hidden] { display: none; }

/* Entrance from the toggle it is anchored to. */
.ring-controls.is-open .ring-controls__panel {
  animation: ringPanelIn 200ms var(--ease-out);
  transform-origin: bottom right;
}
@keyframes ringPanelIn {
  from { opacity: 0; transform: translateY(6px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .ring-controls.is-open .ring-controls__panel { animation: none; }
}

.ring-controls__head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 14px;
}
.ring-controls__reset {
  background: none; border: 0; padding: 4px 0;
  color: var(--text-muted); cursor: pointer;
  font: inherit; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  transition: color 200ms var(--ease-out);
}
.ring-controls__reset:hover { color: var(--text); }

.ring-controls__row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 4px 10px;
  margin-bottom: 11px;
}
.ring-controls__name { font-size: 12px; color: var(--text-secondary); }
.ring-controls__row output {
  font-size: 11px; color: var(--text-muted);
  font-variant-numeric: tabular-nums; text-align: right; min-width: 46px;
}
.ring-controls__row input[type="range"] {
  grid-column: 1 / -1;
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 18px; background: none; cursor: pointer;
}
.ring-controls__row input[type="range"]::-webkit-slider-runnable-track {
  height: 2px; background: var(--border-hover); border-radius: 2px;
}
.ring-controls__row input[type="range"]::-moz-range-track {
  height: 2px; background: var(--border-hover); border-radius: 2px;
}
.ring-controls__row input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 11px; height: 11px; margin-top: -4.5px;
  border-radius: 50%; background: var(--text); border: 0;
}
.ring-controls__row input[type="range"]::-moz-range-thumb {
  width: 11px; height: 11px; border-radius: 50%; background: var(--text); border: 0;
}
.ring-controls__row input[type="range"]:focus-visible { outline: 2px solid var(--text-secondary); outline-offset: 3px; }

.ring-controls__theme {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-bottom: 14px; padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.ring-controls__seg {
  display: inline-flex; padding: 2px;
  border: 1px solid var(--border); border-radius: 999px;
}
.ring-controls__seg button {
  font: inherit; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  min-height: 30px; padding: 0 12px; border: 0; border-radius: 999px;
  background: none; color: var(--text-muted); cursor: pointer;
  transition: color 160ms var(--ease-out), background 160ms var(--ease-out);
}
.ring-controls__seg button[aria-pressed="true"] { background: var(--text); color: var(--bg); }
.ring-controls__seg button:focus-visible { outline: 2px solid var(--text-secondary); outline-offset: 2px; }
@media (pointer: coarse) { .ring-controls__seg button { min-height: 40px; padding: 0 14px; } }

.ring-controls__note {
  margin-top: 12px; padding-top: 12px;
  border-top: 1px solid var(--border);
  font-size: 11px; line-height: 1.5; color: var(--text-muted);
}

@media (max-width: 480px) {
  .ring-controls { right: 16px; bottom: 16px; }
  .ring-controls__panel { width: calc(100vw - 32px); max-width: 300px; }
}

/* --- Body copy lists --- */
/* The global reset sets `ul { list-style: none }`, so markers must be restored
   explicitly here — and padding too, since the reset zeroes that as well. */
ul.copy-list { list-style: disc outside; }
ol.copy-list { list-style: decimal outside; }
.copy-list {
  margin: 0 0 22px;
  padding-left: 22px;
  color: var(--text-secondary);
  max-width: 720px;
}
.copy-list li {
  margin-bottom: 9px;
  padding-left: 6px;
  line-height: 1.65;
}
.copy-list li:last-child { margin-bottom: 0; }
.copy-list li::marker { color: var(--text-muted); font-size: 0.9em; }

/* --- Client logo in the project meta --- */
/* Logos are supplied as white knockouts, so they sit on the dark background
   without a chip. Held to a common optical height rather than a common width,
   since the marks vary from square to wide wordmarks. */
/* The knockouts were authored white for the dark theme. On light they are
   inverted to dark knockouts — alpha is untouched, so the shape is identical.
   A mark with a colour icon cannot be inverted (its greens go magenta), so a
   project may supply `logo_light` in content.json; both variants are rendered
   and the theme picks one. */
:root:not([data-theme="dark"]) .project-logo:not([data-variant]) { filter: invert(1); }
:root:not([data-theme="dark"]) .project-logo[data-variant="dark"]  { display: none; }
:root[data-theme="dark"]       .project-logo[data-variant="light"] { display: none; }
.project-logo {
  height: 26px;
  width: auto;
  max-width: 180px;
  object-fit: contain;
  object-position: left center;
  margin-top: 6px;
  opacity: 0.85;
}
@media (max-width: 480px) { .project-logo { height: 22px; } }

/* --- Problem / Goals: paired two-column band --- */
.problem-goals {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 48px;
  margin: 56px 0;
  padding: 44px 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
/* The section already carries its own rules, so the headings sit tight to the top. */
.problem-goals h3 { margin-top: 0; margin-bottom: 18px; }
.problem-goals .copy-list { margin-bottom: 0; }
.problem-goals p:last-child { margin-bottom: 0; }

/* A single column should not sit at half width. */
.problem-goals:has(> .problem-goals__col:only-child) { grid-template-columns: 1fr; }

@media (max-width: 768px) {
  .problem-goals {
    grid-template-columns: 1fr;
    gap: 32px;
    margin: 44px 0;
    padding: 36px 0;
  }
}

/* --- Project body: aligned to the hero image, text kept readable --- */
/* The body block now spans the full container so images, grids and the
   problem/goals band line up with the hero image edges. Running text is capped
   at a readable measure and left-aligned to that same edge — at the full 1120px
   a line would run past 130 characters, roughly double a comfortable measure. */
.project-body > p,
.project-body > .copy-list,
.project-body > figure > figcaption {
  max-width: 76ch;
}
.project-body > blockquote { max-width: 60ch; }

/* Inside the two-column band the columns are already narrow enough. */
.problem-goals .copy-list,
.problem-goals p { max-width: none; }
