/* ===========================================================
   Jayden Lin — personal site
   Type:   Inter throughout. One face, no monospace.
   Mood:   cinematic black, white type, one accent
   ----------------------------------------------------------
   This is the original design language, restored. The accent is
   #46e0a0 because that is what the site actually rendered — the
   old app.jsx assigned it to --accent on mount, overriding the
   cyan-blue literal that sat in this file and never showed.
   =========================================================== */

:root {
  --bg:        #050507;
  --bg-2:      #0a0b0f;
  --ink:       #f4f6fb;
  --muted:     #9aa0ad;
  --faint:     #5b606c;
  --line:      rgba(255, 255, 255, 0.12);
  --accent:    #46e0a0;
  --accent-dk: #2aa876;

  /* paper */
  --paper:     #f2efe6;
  --paper-ink: #1c1b18;
  --warm:      #ff7a45;
  --maxw:      1320px;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body { overflow-x: hidden; }

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 800;
  line-height: 0.92;
  letter-spacing: -0.01em;
}

p { margin: 0; }
ol, ul { margin: 0; padding: 0; list-style: none; }
dl, dd, dt { margin: 0; }
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

.mono {
  /* Still called .mono because it labels the same things it always did, but it
     is Inter now like the rest of the site. One face, differentiated by size,
     weight and tracking rather than by a second typeface. */
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.nowrap { white-space: nowrap; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.skip {
  position: fixed;
  top: 10px; left: 10px;
  z-index: 100;
  padding: 10px 18px;
  background: var(--accent);
  color: #041209;
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  transform: translateY(-160%);
  transition: transform 0.24s;
}
.skip:focus { transform: none; }

/* ---------- fixed backdrop canvases ---------- */
#bg-layer {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(120% 90% at 78% 18%, rgba(20, 40, 80, 0.22), transparent 60%),
    radial-gradient(100% 80% at 10% 90%, rgba(40, 20, 60, 0.14), transparent 55%),
    var(--bg);
}

#stars, #earth {
  position: fixed; inset: 0; width: 100%; height: 100%;
  z-index: 0; pointer-events: none;
}
#earth { z-index: 1; will-change: opacity; }

/* No-WebGL fallback disc. Mirrors planetGeom() in main.js, or the CSS planet
   lands somewhere the layout does not expect. */
.earth-fallback {
  --earth-x: 74%;
  --earth-y: 46%;
  --earth-r: min(42vmin, 460px);
}
@media (max-width: 820px) {
  .earth-fallback {
    --earth-x: 50%;
    --earth-y: 82%;
    --earth-r: min(52vw, 300px);
  }
}
/* Home puts the planet on the bottom edge as a dome. Mirrors the .hero branch
   of planetGeom() in main.js: change one and change the other. */
/* Mirrors the .hero branch of planetGeom(): a large sphere whose centre sits
   below the fold, leaving VISIBLE of the arc on screen. Change one and change
   the other, or the no-WebGL planet lands somewhere the layout does not expect. */
body:has(.hero) .earth-fallback {
  --earth-x: 50%;
  --earth-r: 75vw;
  --earth-y: calc(100vh + 75vw - 46vh);
}
@media (max-width: 700px) {
  .scroll-hint { bottom: 26px; }
  .hero { padding-bottom: 40vh; }
  body:has(.hero) .earth-fallback {
    --earth-r: 115vw;
    --earth-y: calc(100vh + 115vw - 34vh);
  }
}

/* ---------- foreground scroll content ---------- */
main { position: relative; z-index: 2; }

.shell {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 clamp(22px, 5vw, 72px);
}

/* ---------- nav ---------- */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between;
  padding: 22px clamp(22px, 5vw, 72px);
  transition: background 0.4s, backdrop-filter 0.4s, border-color 0.4s;
  border-bottom: 1px solid transparent;
  /* Original scrim, deepened and extended. The planet's atmosphere limb passes
     directly under the links at the top right, and at the original 0.6 the
     muted link colour lost contrast against it. */
  background: linear-gradient(180deg,
    rgba(5, 5, 7, 0.88) 0%,
    rgba(5, 5, 7, 0.55) 55%,
    rgba(5, 5, 7, 0) 100%);
}
.nav .brand, .nav .links a { text-shadow: 0 1px 14px rgba(0, 0, 0, 0.55); }
.nav.scrolled {
  background: rgba(5, 5, 7, 0.72);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}
.nav .brand {
  display: flex; align-items: center; gap: 12px;
  flex: none;                         /* or the name collapses to nothing */
  font-weight: 800; letter-spacing: 0.02em; font-size: 17px;
}
.brand .brand-short { display: none; }
.nav .links { display: flex; gap: 30px; }
.nav .links a { font-size: 12px; }
.nav .links a span { display: inline-block; transition: color 0.2s; color: var(--muted); }
.nav .links a:hover span { color: var(--ink); }
@media (max-width: 720px) { .nav .links { gap: 18px; } }

/* ---------- projects dropdown ----------
   "Projects" stays an ordinary link, so it works with JavaScript off and a
   click still goes to the page. The caret beside it is a real <button> that
   toggles the panel, which is what makes this usable by keyboard and by touch:
   hovering is not something a finger can do. CSS opens the panel on hover and
   on focus-within too, so the no-JS path is not a dead end either. */
.menu { position: relative; display: flex; align-items: center; gap: 5px; }

.menu-btn {
  display: grid; place-items: center;
  width: 18px; height: 18px; padding: 0;
  background: none; border: 0; cursor: pointer;
  color: var(--muted); transition: color 0.2s, transform 0.25s;
}
.menu:hover .menu-btn, .menu-btn:hover { color: var(--ink); }
.menu-btn[aria-expanded="true"] { color: var(--ink); transform: rotate(180deg); }

.menu-panel {
  position: absolute; top: calc(100% + 14px); left: -14px; z-index: 30;
  min-width: 172px;
  display: flex; flex-direction: column;
  padding: 6px;
  background: rgba(9, 9, 12, 0.94);
  border: 1px solid var(--line);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  /* hidden by default, and unreachable by tab while hidden */
  opacity: 0; visibility: hidden; transform: translateY(-6px);
  transition: opacity 0.18s, transform 0.18s, visibility 0.18s;
}
/* Hover and focus-within are the no-JS path ONLY. With JS running, clicking the
   caret a second time has to actually close the panel, and it cannot do that
   while the pointer is still inside .menu holding :hover true. So once main.js
   tags <html class="js">, the click state is the only thing that opens it. */
html:not(.js) .menu:hover .menu-panel,
html:not(.js) .menu:focus-within .menu-panel,
.menu.open .menu-panel { opacity: 1; visibility: visible; transform: translateY(0); }

/* a hover bridge, so the pointer can cross the gap without the panel closing */
.menu-panel::before {
  content: ''; position: absolute; left: 0; right: 0; top: -14px; height: 14px;
}

.menu-panel a {
  display: flex; align-items: baseline; justify-content: space-between; gap: 18px;
  padding: 9px 12px; font-size: 12px;
  transition: background 0.18s;
}
.menu-panel a span:first-child { color: var(--muted); transition: color 0.18s; }
.menu-panel a:hover, .menu-panel a:focus-visible { background: rgba(255, 255, 255, 0.05); }
.menu-panel a:hover span:first-child, .menu-panel a:focus-visible span:first-child { color: var(--ink); }
.menu-panel .menu-n { color: var(--faint); font-size: 10px; letter-spacing: 0.18em; }

/* the underline that marks the current page belongs under the word, not under
   the word plus its caret */
.menu .menu-top[aria-current="page"]::after { right: auto; width: 100%; }

@media (max-width: 720px) {
  /* right-align so a 320px viewport cannot push the panel off screen */
  .menu-panel { left: auto; right: -10px; min-width: 156px; }
}

/* The caret costs about 23px of a nav row that was already at its limit on a
   small phone. Tighten the row, and below 520px show initials instead of the
   full name so nothing gets clipped off the right edge. */
@media (max-width: 640px) {
  .nav { padding-left: 18px; padding-right: 18px; }
  .nav .links { gap: 14px; }
  .nav .links a { font-size: 11px; }
  .nav .links a .mono, .menu-panel .mono { letter-spacing: 0.14em; }
  .menu { gap: 3px; }
  .menu-btn { width: 14px; height: 14px; }
}
@media (max-width: 520px) {
  .brand .brand-full { display: none; }
  .brand .brand-short { display: inline; }
}
@media (prefers-reduced-motion: reduce) {
  .menu-panel, .menu-btn { transition: opacity 0.18s, visibility 0.18s; }
  .menu-btn[aria-expanded="true"] { transform: none; }
}


/* ============================================================================
   PAPER
   ----------------------------------------------------------------------------
   Below the hero the site leaves space and lands on warm paper. The switch is
   done by scrolling an opaque surface up over the fixed planet, not by
   animating a colour: it costs nothing per frame, it cannot half-apply, and it
   behaves the same with reduced motion or with JavaScript off.

   Everything inside .paper re-points the same custom properties the dark
   sections use, so component rules did not have to be duplicated.
   ========================================================================== */
.paper {
  position: relative;
  z-index: 3;
  background: var(--paper);
  color: var(--paper-ink);

  --ink:    #1c1b18;
  --muted:  #55534c;
  --faint:  #6f6c63;
  --line:   rgba(28, 27, 24, 0.16);
  --bg-2:   #e9e5da;
  /* the dark-background green fails on paper, so this is the same hue darkened
     until it clears 4.5:1 against #f2efe6 */
  --accent: #0f7048;
}

/* The seam. A short gradient from transparent to paper, so the planet is not
   cut off by a hard horizontal line as it scrolls under. */
.paper::before {
  content: '';
  position: absolute; left: 0; right: 0; top: -130px; height: 130px;
  background: linear-gradient(180deg, rgba(242, 239, 230, 0) 0%,
                                      rgba(242, 239, 230, 0.12) 38%,
                                      rgba(242, 239, 230, 0.52) 70%,
                                      rgba(242, 239, 230, 0.88) 88%,
                                      var(--paper) 100%);
  pointer-events: none;
}
/* Work and Projects start on paper immediately, so there is nothing to blend. */
.paper-top::before { display: none; }

.paper h1, .paper h2, .paper h3, .paper h4 { color: var(--paper-ink); }
.paper .accent { color: var(--accent); }


/* The nav is fixed over both surfaces, so it has to switch with them. On the
   home page that happens when paper reaches it; on Work and Projects paper is
   there from the first pixel. */
.nav.on-paper {
  background: rgba(242, 239, 230, 0.82);
  border-bottom: 1px solid rgba(28, 27, 24, 0.14);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
}
.nav.on-paper .brand { color: #1c1b18; }
.nav.on-paper .brand, .nav.on-paper .links a { text-shadow: none; }
.nav.on-paper .links a span { color: #55534c; }
.nav.on-paper .links a:hover span,
.nav.on-paper .links a[aria-current="page"] span { color: #1c1b18; }
.nav.on-paper .links a[aria-current="page"]::after { background: #0f7048; }
.nav.on-paper .menu-btn { color: #55534c; }
.nav.on-paper .menu-btn:hover,
.nav.on-paper .menu-btn[aria-expanded="true"] { color: #1c1b18; }
.nav.on-paper .menu-panel {
  background: rgba(248, 246, 240, 0.97);
  border-color: rgba(28, 27, 24, 0.16);
}
.nav.on-paper .menu-panel a span { color: #55534c; }
.nav.on-paper .menu-panel a:hover,
.nav.on-paper .menu-panel a:focus-visible { background: rgba(28, 27, 24, 0.06); }
.nav.on-paper .menu-panel a:hover span,
.nav.on-paper .menu-panel a:focus-visible span { color: #1c1b18; }

/* ---------- hero ---------- */
/* The hero holds no copy any more. It is a full viewport of planet with a
   scroll cue pinned to the bottom, and it stays transparent so the fixed
   canvases show through. */
.hero {
  min-height: 100vh;
  position: relative;
  /* The arc occupies the bottom 46vh (34vh on a phone). Reserving that as
     padding centres the headline in the sky above it instead of on the
     horizon line. Position has to come from layout rather than a transform,
     because [data-reveal] owns transform and resets it to none on reveal. */
  display: grid; place-items: center;
  padding-bottom: 52vh;
}

/* The only text above the fold. It sits over the sky, above the curve of the
   planet, so it needs no plate behind it: the sky there is close to black. */
.hero .hi .accent { color: #0f7048; }
.hero .hi {
  font-size: clamp(46px, 8.4vw, 118px);
  line-height: 0.9;
  letter-spacing: -0.03em;
  text-align: center;
  text-shadow: 0 6px 60px rgba(0, 0, 0, 0.55);
}



.cta-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }

.btn {
  display: inline-flex; align-items: center; gap: 14px;
  padding: 15px 26px; border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.02);
  font-size: 12px; cursor: pointer; color: var(--ink);
  transition: background 0.25s, border-color 0.25s, transform 0.25s;
}
.btn .mono { letter-spacing: 0.22em; }
.btn .arrow { display: inline-flex; transition: transform 0.3s cubic-bezier(.22,.61,.36,1); }
.btn:hover { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.4); }
.btn:hover .arrow { transform: translateX(7px); }
.btn.solid { background: var(--ink); color: #07080b; border-color: var(--ink); }
.btn.solid:hover { background: #fff; }
.btn.ghost { background: transparent; }

.scroll-hint {
  /* On the planet, not above it. The arc is bright enough down here that dark
     ink reads better than light, and it keeps the cue well clear of the
     headline. */
  position: absolute; left: 50%; bottom: 38px; transform: translateX(-50%);
  /* word on top, line dropping away beneath it */
  display: flex; flex-direction: column-reverse; align-items: center; gap: 12px;
  color: #0d1512; font-size: 11px; opacity: 0.88;
  font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase;
  text-shadow: 0 1px 10px rgba(255, 255, 255, 0.35);
}
.scroll-hint .bar {
  width: 1px; height: 46px;
  background: linear-gradient(rgba(13, 21, 18, 0.85), rgba(13, 21, 18, 0));
  animation: drop 2.2s infinite;
}
@keyframes drop {
  0%   { opacity: 0; transform: scaleY(0.2); transform-origin: top; }
  40%  { opacity: 1; }
  100% { opacity: 0; transform: scaleY(1);   transform-origin: top; }
}


/* ---------- page head (subpages) ----------
   Work and Projects are their own documents now, so each gets a real <h1>
   instead of borrowing the home page's. */
.page-head { padding: clamp(150px, 22vh, 210px) 0 clamp(44px, 6vh, 74px); }
.page-head .idx { color: var(--accent); font-size: 12px; display: block; margin-bottom: 20px; }
.page-head h1 {
  font-size: clamp(38px, 6.4vw, 86px);
  text-transform: uppercase;
  margin-bottom: 24px;
  /* keep the title clear of the planet, which sits from ~48% of the viewport */
  max-width: 15ch;
}
.page-head .lede {
  max-width: 46ch; color: var(--muted);
  font-size: clamp(15px, 1.35vw, 18px); line-height: 1.6; text-wrap: pretty;
}
/* the page head already supplies the top gap */
.page-head + .section { padding-top: 0; }

/* current page marker in the nav */
.nav .links a[aria-current="page"] span { color: var(--ink); }
.nav .links a[aria-current="page"] { position: relative; }
.nav .links a[aria-current="page"]::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -7px;
  height: 1px; background: var(--accent);
}

/* inline link inside prose */
.ilink { color: var(--accent); border-bottom: 1px solid rgba(70,224,160,0.35); transition: border-color 0.25s; }
.ilink:hover { border-bottom-color: var(--accent); }

/* next-page link at the foot of a subpage */
.page-next { margin-top: clamp(48px, 7vh, 80px); display: flex; }

/* ---------- generic section ---------- */
.section { position: relative; padding: clamp(90px, 13vh, 170px) 0; }
/* keep the fixed nav from covering a section you just jumped to */
.section[id], main [id] { scroll-margin-top: 96px; }
.section-head { display: flex; align-items: baseline; gap: 20px; margin-bottom: 56px; }
.section-head .idx { color: var(--accent); font-size: 12px; flex: none; }
.section-head h2 { font-size: clamp(26px, 4.4vw, 60px); text-transform: uppercase; flex: none; }
.section-head .line { flex: 1; height: 1px; background: var(--line); align-self: center; }
@media (max-width: 720px) {
  .section-head { flex-wrap: wrap; gap: 10px 16px; }
  .section-head .line { display: none; }
  /* flex: none sizes to max-content — "Where I've worked" then overflowed a
     320px viewport. Let it shrink and wrap instead. */
  .section-head h2 { flex: 1 1 auto; min-width: 0; }
}

/* ---------- about ---------- */
.about-grid { display: grid; grid-template-columns: 1fr; gap: clamp(34px, 6vw, 90px); align-items: start; }
/* Without a portrait this column spans the full shell, which put body copy at
   about 107 characters a line. Cap the measure until a photo brings the second
   column back. */
/* A ch value here would be measured against this container's 16px base, which
   left the 32px opening paragraph at about 31 characters a line. A px cap keeps
   both sizes in range: roughly 52 characters for the big line, 62 for the rest. */
.about-grid:not(:has(.about-portrait)) .about-copy { max-width: 690px; }
/* when a portrait is added back, the original two-column layout returns */
.about-grid:has(.about-portrait) { grid-template-columns: 0.85fr 1.15fr; }

.about-portrait {
  width: 100%; aspect-ratio: 4/5; border: 1px solid var(--line);
  background: var(--bg-2); display: block; object-fit: cover;
}
.about-copy p { font-size: clamp(17px, 1.7vw, 22px); line-height: 1.6; color: #cdd2dc; margin: 0 0 22px; text-wrap: pretty; }
.about-copy p.big { font-size: clamp(21px, 2.1vw, 27px); color: var(--ink); font-weight: 600; line-height: 1.36; margin-bottom: 28px; }
.about-copy p:last-child { margin-bottom: 0; }

/* Plain facts, no "Focus:" / "Status:" labels in front of them. Each line says
   what it is on its own, so a label would only be a word to skip past. */
.about-facts {
  margin-top: 40px; border-top: 1px solid var(--line); padding-top: 26px;
  display: grid; gap: 11px;
}
.about-facts li {
  position: relative; padding-left: 20px;
  font-size: 16px; line-height: 1.5; color: var(--muted);
}
.about-facts li::before {
  content: ''; position: absolute; left: 0; top: 0.62em;
  width: 8px; height: 1px; background: var(--accent); opacity: 0.75;
}
.about-facts li:first-child { color: var(--ink); }

/* Was an inline style attribute. Moved here so the Content-Security-Policy can
   forbid inline styles outright instead of having to allow them. */
.about-cta { margin-top: 42px; }

@media (max-width: 820px) {
  .about-grid:has(.about-portrait) { grid-template-columns: 1fr; }
  .about-portrait { max-width: 360px; }
}

/* ---------- projects ---------- */
.proj { border-top: 1px solid var(--line); }

/* group header — splits the section into Hardware / Software */
.proj-group {
  display: flex; align-items: center; gap: 18px;
  margin: 64px 0 6px;
}
.section-head + .proj-group { margin-top: 0; }
.proj-group .g-title {
  font-size: 12px; font-weight: 400; color: var(--accent);
  flex: none; letter-spacing: 0.22em;
}
.proj-group .g-line { flex: 1; height: 1px; background: var(--line); }

/* Was 84px 1fr auto, with the first column holding the 01 02 03 numerals.
   Those are gone, so the row is simply body plus the corner arrow. Leaving the
   old three-column template in place collapsed the body to a sliver. */
.proj-item {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 28px;
  align-items: center; padding: 38px 8px; border-bottom: 1px solid var(--line);
  position: relative; transition: padding 0.35s;
}
.proj-body, .work-body { min-width: 0; }   /* else nested grids force the row wider than the viewport */
.proj-item .p-title { font-size: clamp(24px, 3.2vw, 40px); text-transform: uppercase; transition: transform 0.35s, color 0.35s; }
/* --faint fails 4.5:1 and dates are content, so these stay --muted */
.proj-item .when { color: var(--muted); font-size: 11px; margin-top: 12px; }
/* The parts used to be joined by a middot. A gap does the same job without
   putting a character on screen, and it wraps cleanly on a narrow phone
   instead of stranding a lone separator at the start of a line. */
.proj-item .when { display: flex; flex-wrap: wrap; gap: 6px 18px; }
.proj-item .desc { color: var(--muted); font-size: 16px; line-height: 1.55; margin-top: 14px; max-width: 62ch; text-wrap: pretty; }
.proj-item .tags { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.proj-item .tag { font-size: 10px; color: var(--muted); border: 1px solid var(--line); padding: 6px 11px; }
.proj-item .go { color: var(--faint); transition: transform 0.35s, color 0.35s; align-self: start; padding-top: 6px; }
.proj-item::after {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  background: linear-gradient(90deg, color-mix(in oklch, var(--accent) 9%, transparent), transparent);
  transition: width 0.4s; pointer-events: none; z-index: -1;
}
.proj-item:hover { padding-left: 26px; }
.proj-item:hover::after { width: 100%; }
.proj-item:hover .p-title { color: var(--accent); }
.proj-item:hover .go { color: var(--accent); transform: translate(6px, -6px); }

/* linked rows get the pointer; inert rows must not pretend to be clickable */
.proj-item a.proj-body { display: block; color: inherit; }
.proj-item:has(a.proj-body) { cursor: pointer; }

/* media attached to a project (figure, still, diagram) */
.proj-media {
  margin-top: 22px;
  border: 1px solid var(--line);
  background: var(--bg-2);
  max-width: 620px;
}
.proj-media img { width: 100%; height: auto; }
.proj-media figcaption {
  font-size: 13px; line-height: 1.5; color: var(--muted);
  padding: 11px 14px 13px; border-top: 1px solid var(--line);
  max-width: 70ch; text-wrap: pretty;
}

/* compact variant — real but minor/early work, kept honest and low-key */
.proj-item.mini { padding-block: 24px; }
.proj-item.mini .p-title { font-size: clamp(15px, 1.5vw, 19px); }
.proj-item.mini .desc { font-size: 14px; margin-top: 10px; }
.proj-item.mini .when { margin-top: 8px; }
.proj-item.mini .tags, .proj-item.mini .go { display: none; }

@media (max-width: 560px) {
  .figs { grid-template-columns: 1fr; }     /* one figure per row on phones */
}

@media (max-width: 720px) {
  .proj-item { grid-template-columns: 1fr; gap: 12px; }
  .proj-item .go { display: none; }
  .proj-item:hover { padding-left: 8px; }
}


/* ---------- work ---------- */
.work { border-top: 1px solid var(--line); }
.work-item {
  display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 28px;
  padding: 38px 8px; border-bottom: 1px solid var(--line);
  position: relative;
}
.work-meta { padding-top: 6px; }
.work-item .w-date { color: var(--ink); font-size: 11px; }
.work-item .w-loc  { color: var(--muted); font-size: 10px; margin-top: 8px; }  /* --faint fails 4.5:1 */
.work-item .w-name { font-size: clamp(19px, 2.1vw, 27px); text-transform: uppercase; line-height: 1.06; }
.work-item .w-role { color: var(--accent); font-size: 11px; margin-top: 9px; }

/* Unused since the resume bullets came out of work.html. Kept so a single role
   can have detail added back with just a <ul class="bullets"> in the markup. */
.bullets { margin-top: 18px; display: grid; gap: 11px; }
.bullets li {
  position: relative; padding-left: 20px;
  color: var(--muted); font-size: 16px; line-height: 1.55; text-wrap: pretty;
}
.bullets li::before {
  content: ''; position: absolute; left: 0; top: 0.62em;
  width: 7px; height: 1px; background: var(--accent); opacity: 0.75;
}

@media (max-width: 720px) {
  .work-item { grid-template-columns: 1fr; gap: 14px; }
  .work-meta { display: flex; gap: 16px; align-items: baseline; padding-top: 0; }
  .work-item .w-loc { margin-top: 0; }
}

/* ---------- figures ---------- */
/* Research figures come out of papers as black-on-white raster. They are NOT
   inverted (that would corrupt data colours), so each sits in its own bordered
   card and the caption goes underneath on the dark page. */
.figs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 14px;
  margin-top: 22px;
  max-width: 680px;
}
.figs .fig { margin: 0; max-width: none; }
.figs .fig.wide { grid-column: 1 / -1; }

.fig {
  margin: 22px 0 0; border: 1px solid var(--line);
  background: var(--bg-2); max-width: 620px;
}
/* width/height on the <img> reserve the right box before the file arrives, so a
   link straight to #software does not land short while figures load in. */
.fig img { width: 100%; height: auto; }
/* Captions carry real information on a research page, so they are set to be
   read rather than to decorate: Inter instead of mono, sentence case instead of
   caps, no tracking, and a measure capped near 70 characters. At 10px tracked
   uppercase mono across the full figure width they were closer to a texture
   than to text. */
.fig figcaption {
  font-size: 13px; line-height: 1.5; color: var(--muted);
  padding: 12px 15px 14px; border-top: 1px solid var(--line);
  max-width: 70ch; text-wrap: pretty;
}

/* ---------- youtube facade ---------- */
/* A thumbnail + play badge. The real iframe is only injected on click, so no
   YouTube script, cookies or ~1 MB of player payload load unless asked for. */
.yt {
  display: block; position: relative; margin-top: 22px;
  max-width: 620px; aspect-ratio: 16 / 9;
  border: 1px solid var(--line); background: var(--bg-2);
  overflow: hidden;
}
.yt-thumb {
  width: 100%; height: 100%; object-fit: cover;
  opacity: 0.72; transition: opacity 0.3s, transform 0.5s;
}
/* the thumbnail is third-party; if it fails the badge and label still read */
.yt.no-thumb .yt-thumb { display: none; }
.yt:hover .yt-thumb { opacity: 0.9; transform: scale(1.02); }

.yt-play {
  position: absolute; left: 50%; top: 50%;
  width: 58px; height: 40px; transform: translate(-50%, -50%);
  border-radius: 9px; background: rgba(5, 5, 7, 0.72);
  border: 1px solid var(--line);
  transition: background 0.3s, border-color 0.3s;
}
.yt-play::after {
  content: ''; position: absolute; left: 52%; top: 50%;
  transform: translate(-50%, -50%);
  border-left: 13px solid var(--ink);
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
  transition: border-left-color 0.3s;
}
.yt:hover .yt-play { background: var(--accent); border-color: var(--accent); }
.yt:hover .yt-play::after { border-left-color: #041209; }

.yt-label {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 12px 14px; font-size: 10px; color: var(--ink);
  background: linear-gradient(180deg, rgba(5,5,7,0), rgba(5,5,7,0.85));
}
.yt iframe { width: 100%; height: 100%; border: 0; display: block; }

/* escape hatch under the player, added by main.js */
.yt-out {
  display: inline-block; margin-top: 10px;
  font-size: 10px; color: var(--muted);
  border-bottom: 1px solid transparent;
  transition: color 0.25s, border-color 0.25s;
}
.yt-out:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* ---------- obfuscated mail ---------- */
.contact .mail .at {
  color: var(--faint);
  font-size: 0.5em;
  font-weight: 400;
  letter-spacing: 0.02em;
  -webkit-text-fill-color: var(--faint);   /* opt out of the gradient clip */
  vertical-align: 0.28em;
  margin: 0 0.12em;
}

/* ---------- contact ---------- */
.contact { text-align: center; padding: clamp(90px, 13vh, 160px) 0; }
/* the section lost its "04 / Contact" label, so this heading is what names it */
.contact h2 {
  font-size: clamp(15px, 1.6vw, 18px); font-weight: 500;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 30px;
}
.contact .pre { color: var(--muted); font-size: 13px; margin-bottom: 26px; }
.contact .mail {
  display: inline-block; font-weight: 800; text-transform: uppercase;
  /* the 34px floor overflowed a 320px viewport by 44px — the address is 15
     uppercase characters and cannot wrap usefully, so it scales all the way down */
  font-size: clamp(19px, 6.2vw, 84px); line-height: 1.06; letter-spacing: -0.02em;
  max-width: 100%; text-wrap: balance;
  background: linear-gradient(180deg, #fff, #aab0bd);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  transition: opacity 0.3s;
  cursor: pointer;
}
.contact .mail:hover { opacity: 0.7; }
.contact .links { display: flex; gap: 28px; justify-content: center; margin-top: 46px; flex-wrap: wrap; }
.contact .links a { color: var(--muted); font-size: 12px; transition: color 0.2s; }
.contact .links a:hover { color: var(--accent); }

/* ---------- footer ---------- */
.foot { border-top: 1px solid var(--line); padding: 30px 0; color: var(--faint); font-size: 11px; }
.foot-inner { display: flex; justify-content: space-between; align-items: center; }
@media (max-width: 600px) { .foot-inner { flex-direction: column; gap: 10px; text-align: center; } }

/* ---------- reveal on scroll ---------- */
/* Scoped to .js (set by main.js) so nothing is stranded invisible if the
   script never runs. */
.js [data-reveal] {
  opacity: 0; transform: translateY(26px);
  transition: opacity 0.9s cubic-bezier(.22,.61,.36,1), transform 0.9s cubic-bezier(.22,.61,.36,1);
  transition-delay: calc(var(--reveal-i, 0) * 90ms);
}
.js [data-reveal].in { opacity: 1; transform: none; }

/* ---------- responsive ---------- */

@media (max-width: 480px) {
  /* This used to hide the wordmark outright, leaving no brand at all on a small
     phone. The initials cover the same width problem and still say whose site
     this is, so the rule now only has to stop the full name. */
  .cta-row .btn { flex: 1 1 auto; justify-content: center; }
}


/* ---------- components on paper ---------- */
.paper .about-facts li::before { background: var(--accent); opacity: 1; }
.paper .about-copy p { color: #3b3934; }
.paper .fig, .paper .proj-media { background: #fbfaf6; border-color: rgba(28,27,24,0.16); }
.paper .fig figcaption, .paper .proj-media figcaption { color: #55534c; border-top-color: rgba(28,27,24,0.14); }
.paper .tag { border-color: rgba(28,27,24,0.20); color: #55534c; }
.paper .proj, .paper .proj-item, .paper .work-item { border-color: rgba(28,27,24,0.14); }
.paper .btn.ghost { border-color: rgba(28,27,24,0.28); color: #1c1b18; }
.paper .btn.ghost:hover { border-color: #1c1b18; background: rgba(28,27,24,0.05); }
.paper .btn.solid { background: var(--accent); color: #f7f5ef; border-color: var(--accent); }
.paper .btn.solid:hover { background: #0b5a3a; border-color: #0b5a3a; }
.paper .ilink { color: var(--accent); border-bottom-color: rgba(15,112,72,0.4); }
.paper .ilink:hover { border-bottom-color: var(--accent); }
/* The dark-page address is a white-to-grey gradient clipped to the glyphs, which
   on paper renders as near-invisible light text. Swap the gradient for ink. */
.paper .contact .mail {
  background: linear-gradient(180deg, #23211d, #4a4740);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
.paper .contact .mail .at { -webkit-text-fill-color: #7a776d; color: #7a776d; }
.paper .go { color: #6f6c63; }
.paper .yt { border-color: rgba(28,27,24,0.16); }

/* the footer belongs to paper on every page */
.foot { background: var(--paper); color: #55534c; border-top: 1px solid rgba(28,27,24,0.14); position: relative; z-index: 3; }
.foot .mono { color: #55534c; }

/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0ms !important;
  }
  .js [data-reveal] { opacity: 1; transform: none; }
  .scroll-hint .bar { animation: none; opacity: 0.6; }
  .proj-item:hover { padding-left: 8px; }
  .proj-item:hover .p-title, .proj-item:hover .go { transform: none; }
}
