/* ============================================================================
   Pham Tien Dat — portfolio
   Minimal, dark-first, monospace. Hand-rolled, no framework.
   ========================================================================== */

:root {
  --bg:        #0c0d10;
  --bg-soft:   #131519;
  --line:      #23262d;
  --text:      #e6e7ea;
  --muted:     #8b909a;
  --faint:     #565b66;
  --accent:    #6ee7b7;   /* mint */
  --accent-dim:#3aa17a;

  /* body sans — warm minimal grotesque (the Claude/Styrene feel); mono is
     kept for code-like bits only (toolbox tooltip) */
  --sans: "Hanken Grotesk", "Segoe UI", system-ui, sans-serif;
  --mono: "Inconsolata", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --serif: "Newsreader", Georgia, "Times New Roman", serif;

  --maxw: 90%;   /* near-full-width layout, centered — plus --pad as inner gutter */
  --pad: clamp(1.25rem, 5vw, 3rem);
}

@media (prefers-color-scheme: light) {
  :root {
    --bg:       #f7f6f3;
    --bg-soft:  #ffffff;
    --line:     #e3e1db;
    --text:     #17181b;
    --muted:    #5c616b;
    --faint:    #9aa0aa;
    --accent:   #0f9d6e;
    --accent-dim:#0b7d57;
  }
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; overflow-x: clip; }

/* Lenis smooth scrolling (recommended base styles). Native smooth scroll
   stays on as the fallback when lenis is off (reduced motion / no JS). */
html.lenis, html.lenis body { height: auto; }
html.lenis { scroll-behavior: auto !important; }
html.lenis [data-lenis-prevent] { overscroll-behavior: contain; }
html.lenis.lenis-stopped { overflow: hidden; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ── Loading screen ──────────────────────────────────────────────────────── */
/* Black cover while the page loads. Exit is an iris reveal: the counter
   fades, then a circular aperture opens from the centre (a mask whose
   radius is a registered custom property, so it can transition) and the
   page shows through the growing hole. loader.js drives the sequence.
   Sits above everything, the custom cursor (z 100) included. */
@property --iris {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
.loader {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
  --iris: 0px;
  /* transparent centre with a feathered rim that widens as it grows */
  -webkit-mask-image: radial-gradient(circle at 50% 50%, transparent calc(var(--iris) * 0.8), #000 var(--iris));
  mask-image: radial-gradient(circle at 50% 50%, transparent calc(var(--iris) * 0.8), #000 var(--iris));
  transition: --iris 1.2s cubic-bezier(0.65, 0, 0.35, 1);
}
.loader.reveal { --iris: 130vmax; pointer-events: none; }
.loader.done { display: none; }
.loader__num {
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(1.45rem, 3.1vw, 2.15rem);
  line-height: 1;
  color: #e6e7ea;
  transition: opacity 0.2s ease;
}
.loader.leaving .loader__num { opacity: 0; }

/* Subtle film grain / vignette so the flat dark isn't dead */
.grain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background:
    radial-gradient(120% 120% at 50% -10%, rgba(110,231,183,0.05), transparent 55%);
}

main, .site-header, .site-footer { position: relative; z-index: 1; }

a { color: inherit; text-decoration: none; }

/* ── Masthead box (header + hero) ────────────────────────────────────────── */
/* Fills the first screen and PINS there (sticky): scrolling slides the
   sections over it like a curtain while scrolltrigger.js scrubs the hero.
   z-index 0 keeps it under the blocks that cover it. */
.masthead {
  position: sticky;
  top: 1.5rem;
  z-index: 0;
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - 3rem);
  min-height: calc(100svh - 3rem);
  /* break out of main's column to ~95% of the viewport */
  width: 95vw;
  margin-left: calc((100% - 95vw) / 2);
  margin-top: 1.5rem;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--bg-soft);
  overflow: hidden;
}
/* "scroll" nudge at the card's foot; scrolltrigger.js fades it out fast */
.masthead__hint {
  position: absolute;
  bottom: 1.3rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  color: var(--faint);
  font-size: 0.78rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}
/* Decorative contour backdrop — sits behind the header + hero content. */
.masthead__contours {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  color: #ff8fb3;              /* soft pink — complements the mint/cyan accent */
  opacity: 0.12;
  pointer-events: none;
  z-index: 0;
}
.masthead > .site-header,
.masthead > .hero { position: relative; z-index: 1; }

/* ── Header ──────────────────────────────────────────────────────────────── */
.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.1rem clamp(1.25rem, 4vw, 2rem);
  border-bottom: 1px solid var(--line);
}
.brand {
  font-weight: 700;
  letter-spacing: 0.15em;
  border: 1px solid var(--line);
  padding: 0.35rem 0.6rem;
  border-radius: 6px;
  transition: border-color 0.2s, color 0.2s;
}
.brand:hover { border-color: var(--accent); color: var(--accent); }

.site-header .eyebrow { margin: 0; }

.site-nav { display: flex; gap: 1.6rem; }
.site-nav a {
  color: var(--muted);
  font-size: 0.9rem;
  text-transform: capitalize;
  position: relative;
  transition: color 0.2s;
}
.site-nav a::after {
  content: "";
  position: absolute;
  left: 0; bottom: -4px;
  width: 0; height: 1px;
  background: var(--accent);
  transition: width 0.22s ease;
}
.site-nav a:hover { color: var(--text); }
.site-nav a:hover::after { width: 100%; }

/* Letter-roll hover (spans injected by navroll.js): each character column
   holds two stacked copies of its letter; only the top line is visible.
   Hover slides every column up one line, staggered left to right. */
.site-nav a.nav-roll { display: inline-flex; text-transform: none; }
.nav-roll__ch {
  display: inline-block;
  height: 1.25em;
  line-height: 1.25em;
  overflow: hidden;
  vertical-align: top;
}
.nav-roll__ch span {
  display: block;
  transition: transform 0.45s cubic-bezier(0.33, 0, 0.2, 1);
  transition-delay: calc(var(--i) * 0.055s);
}
.nav-roll:hover .nav-roll__ch span,
.nav-roll:focus-visible .nav-roll__ch span { transform: translateY(-100%); }
@media (prefers-reduced-motion: reduce) {
  .nav-roll__ch span { transition: none; }
}

/* ── Layout blocks ───────────────────────────────────────────────────────── */
main { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--pad); }

.hero {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(2.5rem, 9vh, 5rem) clamp(1.5rem, 5vw, 3rem) clamp(2.5rem, 8vh, 4.5rem);
}

.eyebrow {
  color: var(--accent);
  font-size: 0.85rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  margin: 0 0 1.5rem;
}

.hero h1 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(2.2rem, 7vw, 4rem);
  line-height: 1.08;
  letter-spacing: -0.01em;
  margin: 0 0 1.5rem;
}
.accent { color: var(--accent); font-style: italic; }

.lede {
  color: var(--muted);
  max-width: 52ch;
  font-size: 1.05rem;
  margin: 0 0 2.2rem;
}

.hero-actions { display: flex; gap: 0.9rem; flex-wrap: wrap; }

.btn {
  display: inline-block;
  padding: 0.7rem 1.2rem;
  border: 1px solid var(--accent);
  color: var(--bg);
  background: var(--accent);
  border-radius: 7px;
  font-size: 0.92rem;
  font-weight: 500;
  transition: transform 0.15s, background 0.2s, color 0.2s;
}
.btn:hover { transform: translateY(-2px); }
.btn-ghost {
  background: transparent;
  color: var(--text);
  border-color: var(--line);
}
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }

/* ── Section heads ───────────────────────────────────────────────────────── */
/* opaque + above the pinned masthead, so scrolling covers it cleanly */
.block {
  position: relative;
  z-index: 1;
  background: var(--bg);
  padding: clamp(6rem, 20vh, 13rem) 0;
}
/* the about block opens the page right off the masthead — just a small gap
   above its ticker, the huge inter-section padding starts below it */
#about { padding-top: clamp(2.5rem, 8vh, 5rem); }
/* ...and the footer closes it without a huge empty run-out at the bottom */
.footer { padding-bottom: clamp(2rem, 4vh, 3rem); }
/* the masthead is wider than the column, so each block's backdrop bleeds to
   the full viewport — otherwise the pinned card's wings would peek out at
   the sides while being covered */
.block::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(50% - 50vw);
  right: calc(50% - 50vw);
  z-index: -1;
  background: var(--bg);
}

.block-head {
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  margin-bottom: 2.5rem;
}
.idx { color: var(--faint); font-size: 0.85rem; letter-spacing: 0.1em; }
.block-head h2 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.5rem, 4vw, 2rem);
  margin: 0;
}

/* ── Word ticker ─────────────────────────────────────────────────────────── */
/* Full-bleed looping strip at the top of the first block. The row holds two
   identical halves, so translating -50% loops seamlessly. */
.ticker {
  margin-left: calc(50% - 50vw);
  width: 100vw;
  overflow: hidden;
  white-space: nowrap;
  padding-bottom: clamp(8rem, 20vh, 14rem);   /* air between the strip and "01 About" */
}
.ticker__row {
  display: inline-block;
  width: max-content;
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(1.5rem, 3.2vw, 2.5rem);
  color: var(--faint);
  animation: ticker-scroll 60s linear infinite;
}
@keyframes ticker-scroll {
  to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .ticker__row { animation: none; }
}

/* ── Scroll-fill statement ───────────────────────────────────────────────── */
/* The line is painted through a gradient whose boundary (--fill) sweeps from
   0 to 100% as it scrolls up the viewport (fillline.js). Defaults to fully
   inked, so no JS / reduced motion just shows normal text. */
.about-statement {
  --fill: 100%;
  flex: 1;                /* fills the row next to the photo */
  align-self: center;     /* sits level with the middle of the photo */
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(1.5rem, 3.2vw, 2.4rem);
  line-height: 1.35;
  background: linear-gradient(90deg, var(--text) var(--fill), var(--faint) var(--fill));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ── Reveal-on-enter ─────────────────────────────────────────────────────── */
/* reveal.js stamps `reveals` on <html> and flips each [data-reveal] to .in
   when it scrolls into view. Without JS the class never lands, so nothing is
   ever hidden. --reveal-delay staggers siblings (hero: h1 → lede → buttons). */
html.reveals [data-reveal] {
  opacity: 0;
  transition: opacity 0.7s ease, transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: var(--reveal-delay, 0s);
}
html.reveals [data-reveal="up"]   { transform: translateY(28px); }
html.reveals [data-reveal="left"] { transform: translateX(-32px); }
html.reveals [data-reveal].in { opacity: 1; transform: none; }

/* ── Projects ────────────────────────────────────────────────────────────── */
.projects { list-style: none; margin: 0; padding: 0; }

.project {
  padding: 1.8rem 0;
  border-top: 1px solid var(--line);
  transition: transform 0.2s;
}
.project:first-child { border-top: none; padding-top: 0; }

.project-media {
  display: block;
  margin-bottom: 1.3rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: var(--bg-soft);
}
.project-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.45s ease;
}
.project:hover .project-media img { transform: scale(1.03); }

.project-meta {
  display: flex;
  gap: 1rem;
  align-items: center;
  margin-bottom: 0.6rem;
  font-size: 0.82rem;
  color: var(--faint);
}
.project-meta .tag { color: var(--accent-dim); letter-spacing: 0.04em; }

.project h3 {
  font-size: 1.35rem;
  margin: 0 0 0.5rem;
  font-weight: 500;
  transition: color 0.2s;
}
.project:hover h3 { color: var(--accent); }
.project p { color: var(--muted); margin: 0 0 0.9rem; max-width: 60ch; }

/* stat tiles + terminal line — benchmark card instead of prose */
.project-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 0.8rem;
  margin: 0.9rem 0;
}
.stat {
  background: var(--bg-soft);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.75rem 1rem;
}
.stat b {
  display: block;
  font-size: 1.35rem;
  font-weight: 600;
  line-height: 1.2;
}
.stat span { font-size: 0.8rem; color: var(--muted); }
.stat .ok { color: var(--accent); font-weight: 500; }
.project-term {
  font-family: var(--mono);
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.7rem 1rem;
  margin-bottom: 0.9rem;
  overflow-x: auto;
}
/* pre on the lines (not the box — that would render the template's own
   newlines) keeps the log's column alignment (CPU:, Threads:, …) */
.project-term > div { white-space: pre; }
.project-term .ok { color: var(--accent); }

/* repo link rides at the right end of the meta row */
.project-repo {
  margin-left: auto;
  color: var(--muted);
  transition: color 0.2s;
}
.project-repo:hover { color: var(--accent); }

/* ── Things I enjoy: bento grid ──────────────────────────────────────────── */
/* Tiles auto-place into 3 columns; .tile--wide / .tile--tall span extra
   cells. New tiles can just be appended — no coordinates to manage. */
.bento {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-flow: dense;
  gap: 18px;
  margin-top: 1.5rem;
}
.tile {
  position: relative;
  border: 1px solid var(--line);
  border-radius: 20px;
  background: var(--bg-soft);
  /* padding scales with cell size (bigger cells breathe more); the 18px
     gutter stays constant — that contrast is what gives the grid rhythm */
  padding: 1.15rem 1.25rem 1.25rem;
  overflow: hidden;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
  transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.tile:hover {
  transform: translateY(-4px);
  border-color: var(--accent);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.38);
}
.tile--wide { grid-column: span 2; padding: 1.35rem 1.5rem 1.5rem; }
.tile--tall { grid-row: span 2; padding: 1.35rem 1.5rem 1.5rem; }
/* n x n tile — aspect-ratio keeps it a true square whatever the column width */
.tile--square { grid-column: span 2; grid-row: span 2; aspect-ratio: 1; }

.tile h3 { margin: 0 0 0.5rem; font-size: 1.15rem; font-weight: 600; letter-spacing: 0.01em; color: var(--text); }
.tile p { margin: 0; color: var(--muted); font-size: 0.9rem; line-height: 1.5; }
/* keep text above the tile's backdrop layers (canvas / decorations) */
.tile h3, .tile p { position: relative; z-index: 1; }

/* Simulations tile — particle canvas fills the tile behind the text */
.tile-sim__canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; z-index: 0; }

/* Faint corner decorations — brighten to the accent when the tile is hovered */
.tile__deco {
  position: absolute;
  z-index: 0;
  color: var(--faint);
  opacity: 0.35;
  pointer-events: none;
  transition: opacity 0.25s ease, color 0.25s ease;
}
.tile:hover .tile__deco { opacity: 0.85; color: var(--accent); }
.tile__deco--court { right: 0; bottom: 0; width: 120px; height: 120px; }
/* the tall tile has room — let the arcs grow into it */
.tile--tall .tile__deco--court { width: 230px; height: 230px; }
.tile__deco--math {
  /* watermark on the right half of the wide tile, clear of the text */
  right: 1.6rem;
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--serif);
  font-style: italic;
  font-size: 2.3rem;
  letter-spacing: 0.02em;
}
.tile__deco--math sup { font-size: 0.55em; }
/* keep wide-tile body text clear of the right-side watermark */
.tile--wide > p { max-width: 66%; }

/* Equaliser tile — 5-band graphic EQ (hand-rolled vertical faders) */
.tile--eq { display: flex; flex-direction: column; gap: 0.7rem; }
.eq-rack { display: flex; gap: 0.4rem; }
.eq-scale {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 96px;
  color: var(--faint);
  font-size: 0.62rem;
  text-align: right;
  flex: none;
}
.eq-bands { display: flex; justify-content: space-around; flex: 1; }
.eq-band {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.45rem;
  color: var(--faint);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
}
.eq-slider {
  position: relative;
  width: 22px;
  height: 96px;
  cursor: ns-resize;
  touch-action: none;
}
/* track */
.eq-slider::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 2px;
  transform: translateX(-50%);
  background: var(--line);
  border-radius: 1px;
}
/* 0 dB centre tick */
.eq-slider__zero {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 10px;
  height: 1px;
  transform: translate(-50%, -50%);
  background: var(--faint);
}
/* centre-out fill (positioned by JS) */
.eq-slider__fill {
  position: absolute;
  left: 50%;
  width: 2px;
  transform: translateX(-50%);
  background: var(--accent);
  border-radius: 1px;
}
/* fader handle (positioned by JS) */
.eq-slider__thumb {
  position: absolute;
  left: 50%;
  width: 16px;
  height: 7px;
  transform: translate(-50%, -50%);
  background: var(--accent);
  border-radius: 2px;
  transition: transform 0.1s ease;
}
.eq-slider:hover .eq-slider__thumb { transform: translate(-50%, -50%) scale(1.15); }

.eq-presets { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.eq-preset {
  font-family: inherit;
  font-size: 0.72rem;
  color: var(--muted);
  background: none;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.22rem 0.55rem;
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s;
}
.eq-preset:hover { color: var(--accent); border-color: var(--accent); }
.eq-preset.active { color: var(--bg); background: var(--accent); border-color: var(--accent); }

.eq-note { color: var(--faint); font-size: 0.72rem; letter-spacing: 0.04em; }

/* Favourite shows — thin poster cards, name overlaid at the bottom of the
   picture; the cards wrap into a vertical stack in the tall tile. */
.show-cards { display: flex; flex-wrap: wrap; gap: 0.9rem; justify-content: center; padding: 0.2rem 0; }
.show-card {
  position: relative;
  width: 218px;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--bg);
  transition: border-color 0.2s;
}
.show-card:hover { border-color: var(--accent); }
.show-card img {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  object-position: 50% 20%;
}
/* Scrim so the name stays readable over any artwork; deepens on hover
   to make room for the extra info line. */
.show-card::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 42%;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.78));
  pointer-events: none;
  transition: height 0.3s ease;
}
.show-card:hover::after { height: 68%; }
.show-card__label {
  position: absolute;
  left: 0.55rem;
  right: 0.55rem;
  bottom: 0.45rem;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  color: #fff;
  font-size: 0.78rem;
  font-weight: 500;
  line-height: 1.25;
}
/* Extra details — hidden until the card is hovered, then slides up. */
.show-card__more {
  font-size: 0.68rem;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.72);
  max-height: 0;
  opacity: 0;
  transform: translateY(5px);
  overflow: hidden;
  transition: max-height 0.3s ease, opacity 0.3s ease, transform 0.3s ease;
}
.show-card:hover .show-card__more {
  max-height: 3em;
  opacity: 1;
  transform: none;
}


/* Narrow screens: collapse the bento to a single column. */
@media (max-width: 760px) {
  .bento { grid-template-columns: 1fr; gap: 1rem; }
  .tile--wide, .tile--tall, .tile--square { grid-column: auto; grid-row: auto; }
}

/* ── Things I enjoy: pinned horizontal ride ──────────────────────────────── */
/* enjoyride.js gives the section a tall JS-computed height (the scrub range);
   the pin sticks for the whole ride while the card track travels left. Each
   .enjoy-step carries the ride/appear transforms so the card's own tilt and
   hover transforms below stay untouched. */
.block--enjoy { padding: 0; }   /* the pin IS the section; neighbours give the gap */
.enjoy-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
  /* full-bleed like .pistrip, with the column's gutter kept as inner padding */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  padding: 0 var(--pad);
}
.enjoy-pin .block-head { margin-bottom: clamp(2rem, 6vh, 4rem); }
.enjoy-track {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 3vw, 3rem);
  width: max-content;
  will-change: transform;
}
.enjoy-step {
  flex: none;
  /* scrubbed by enjoyride.js: 0 = off-screen right, 1 = fully arrived;
     --stagger is the card's resting row offset (zigzag below) */
  opacity: var(--appear, 1);
  transform: translateY(calc(var(--stagger, 0rem) + (1 - var(--appear, 1)) * 60px));
}
/* alternate the cards up-down-up-down — the old two-row collage feel, sideways */
.enjoy-step:nth-child(odd)  { --stagger: -3rem; }
.enjoy-step:nth-child(even) { --stagger: 3rem; }
.enjoy-card {
  position: relative;
  /* the vh term keeps card + zigzag offset inside the 100vh pin on short screens */
  width: min(38vw, 46vh, 430px);
  aspect-ratio: 4 / 5;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg-soft);
  cursor: pointer;
  box-shadow: 0 16px 38px rgba(0, 0, 0, 0.45);
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.25s;
}
/* small alternating tilts sell the "photos on a table" look; hover rules sit
   below so equal specificity resolves in their favour */
.enjoy-step:nth-child(odd) .enjoy-card { transform: rotate(-2deg); }
.enjoy-step:nth-child(even) .enjoy-card { transform: rotate(1.6deg); }
.enjoy-step .enjoy-card:hover,
.enjoy-step .enjoy-card:focus-visible,
.enjoy-step .enjoy-card.active {
  transform: rotate(0deg) scale(1.07);
  z-index: 2;
  border-color: var(--accent);
}

.enjoy-card img,
.enjoy-card .tile-sim__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* one shared grade unifies the mixed artwork; hover wakes the card up */
.enjoy-card img { filter: grayscale(0.35) brightness(0.85); transition: filter 0.3s; }
.enjoy-card:hover img, .enjoy-card.active img { filter: none; }

.enjoy-card__label {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  padding: 1.6rem 0.8rem 0.5rem;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.82));
  color: #fff;
  /* card titles share the heading serif, italic — a small signature on each card */
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.1rem;
  letter-spacing: 0.01em;
  text-align: left;
}

/* graphic cards (no photo): centred line art on the soft background */
.enjoy-card__art {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--faint);
  transition: color 0.25s;
}
.enjoy-card:hover .enjoy-card__art, .enjoy-card.active .enjoy-card__art { color: var(--accent); }
.enjoy-card__art--corner { align-items: flex-end; justify-content: flex-end; }
.enjoy-card__art--corner svg { display: block; }
.enjoy-card__art--math { font-family: var(--serif); font-style: italic; font-size: 1.7rem; letter-spacing: 0.02em; }
.enjoy-card__art--math sup { font-size: 0.55em; }

/* ── Music player card — the collage card IS the player ─────────────────── */
/* Cover art fills the card; song info, timeline and controls sit on a
   scrim at the foot. player.js swaps art/labels when changing songs. */
.enjoy-card--player {
  cursor: default;
  /* the anchor card: wider than the rest (square, so no taller) */
  width: min(44vw, 52vh, 480px);
  aspect-ratio: 1;
  z-index: 1;
}
.enjoy-card--player .player { position: absolute; inset: 0; }
.player__art { position: absolute; inset: 0; }
.player__art img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* the cover stays full-colour while a song is playing */
.enjoy-card--player .player.playing img { filter: none; }

.player__overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 0.18rem;
  padding: 2.4rem 0.85rem 0.7rem;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.88) 55%);
}
.player__titlerow { display: flex; align-items: center; gap: 0.45rem; min-width: 0; }
.player__song { color: #fff; font-size: 0.92rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.player__artist { color: rgba(255, 255, 255, 0.78); font-size: 0.74rem; align-self: flex-start; text-decoration: none; border-bottom: 1px solid transparent; transition: color 0.2s, border-color 0.2s; }
a.player__artist:hover { color: var(--accent); border-bottom-color: var(--accent); }
.player__album { color: rgba(255, 255, 255, 0.55); font-size: 0.65rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Equalizer — three bars that dance while playing */
.player__eq { display: inline-flex; align-items: flex-end; gap: 2px; height: 10px; flex: none; opacity: 0; transition: opacity 0.3s; }
.player__eq i { width: 3px; height: 30%; border-radius: 1px; background: var(--accent); }
.player.playing .player__eq { opacity: 1; }
.player.playing .player__eq i { animation: eq-bounce 0.9s ease-in-out infinite; }
.player.playing .player__eq i:nth-child(2) { animation-duration: 0.7s; animation-delay: 0.12s; }
.player.playing .player__eq i:nth-child(3) { animation-duration: 1.1s; animation-delay: 0.25s; }
@keyframes eq-bounce {
  0%, 100% { height: 25%; }
  50%      { height: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  /* static bars still signal "playing" via the opacity fade above */
  .player.playing .player__eq i { animation: none; height: 70%; }
}

.player__timeline {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-top: 0.25rem;
}
.player__time { color: rgba(255, 255, 255, 0.6); font-size: 0.6rem; flex: none; font-variant-numeric: tabular-nums; }
.player__track {
  flex: 1;
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.28);
  cursor: pointer;
  position: relative;
}
.player__fill { width: 0%; height: 100%; border-radius: 2px; background: var(--accent); position: relative; }
/* Seek knob — appears when hovering the timeline */
.player__fill::after {
  content: "";
  position: absolute;
  right: -4px;
  top: 50%;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #fff;
  transform: translateY(-50%) scale(0);
  transition: transform 0.15s ease;
}
.player__track:hover .player__fill::after { transform: translateY(-50%) scale(1); }

.player__controls {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1.1rem;
}
.player__btn {
  font-family: inherit;
  font-size: 0.9rem;
  color: rgba(255, 255, 255, 0.8);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.15rem 0.25rem;
  transition: color 0.2s, transform 0.15s;
}
.player__btn:hover { color: #fff; transform: scale(1.1); }
.player__play {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.45);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.72rem;
}
.player__play:hover { border-color: var(--accent); color: var(--accent); }

/* Custom cursor — a small dot replaces the pointer page-wide (cursor.js
   drives it; fine pointers only). It swells over anything clickable and
   grows into a labelled badge over the enjoy cards. The `cursored` guard
   class only lands when the script runs, so the native cursor survives
   JS being off. */
html.cursored body,
html.cursored body * { cursor: none !important; }
.enjoy-cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  pointer-events: none;
}
.enjoy-cursor__dot {
  position: absolute;
  top: -5px;
  left: -5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--text);
  transform: scale(0);
  transition: transform 0.25s ease, background-color 0.2s ease, opacity 0.2s ease;
}
.enjoy-cursor.on .enjoy-cursor__dot { transform: scale(1); }
/* on clickables the dot morphs outward to fill the ring — translucent so
   whatever sits underneath stays readable */
.enjoy-cursor.on.link .enjoy-cursor__dot { transform: scale(3.6); background: var(--accent); opacity: 0.35; }
/* over solid-accent surfaces (filled .btn) the accent fill would vanish —
   flip it to the page background colour instead */
.enjoy-cursor.on.link.inv .enjoy-cursor__dot { background: var(--bg); }
.enjoy-cursor.on.big .enjoy-cursor__dot { transform: scale(0); }
/* the outline ring rides in the same wrapper as the dot — one object, one
   motion; it swells on clickables and gives way to the badge */
.enjoy-cursor__ring {
  position: absolute;
  top: -18px;
  left: -18px;
  width: 36px;
  height: 36px;
  border: 1px solid var(--muted);
  border-radius: 50%;
  transform: scale(0);
  transition: transform 0.25s ease, border-color 0.25s ease, opacity 0.25s ease;
}
.enjoy-cursor.on .enjoy-cursor__ring { transform: scale(1); }
.enjoy-cursor.on.link .enjoy-cursor__ring { border-color: var(--accent); }
.enjoy-cursor.on.link.inv .enjoy-cursor__ring { border-color: var(--bg); }
.enjoy-cursor.on.big .enjoy-cursor__ring { transform: scale(0.4); opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .enjoy-cursor__ring { transition: none; }
}
.enjoy-cursor__badge {
  position: absolute;
  top: -29px;
  left: -29px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 58px;
  height: 58px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--bg);
  font-family: var(--sans);
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  transform: scale(0);
  transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}
.enjoy-cursor.big .enjoy-cursor__badge { transform: scale(1); }
@media (prefers-reduced-motion: reduce) {
  .enjoy-cursor__dot, .enjoy-cursor__badge { transition: none; }
}

/* Card content dialog — pops up centred over a dimmed, blurred backdrop */
.enjoy-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(5px);
}
.enjoy-modal[hidden] { display: none; }
/* entrance — the unfold: a point stretches into a thin vertical line
   (scaleY first), the line expands sideways into the full panel (scaleX),
   then the content glides up into it. Children are faded in late so they
   never show squashed mid-unfold. The keyframes re-run automatically every
   time the modal flips from display:none. */
.enjoy-modal { animation: modal-fade 0.25s ease; }
.enjoy-modal__box { animation: modal-unfold 0.45s cubic-bezier(0.7, 0, 0.25, 1) both; }
.enjoy-panel.open { animation: panel-glide 0.34s 0.38s cubic-bezier(0.22, 1, 0.36, 1) backwards; }
.enjoy-modal__close { animation: modal-close-in 0.22s 0.4s ease backwards; }
.enjoy-modal.closing { animation: modal-fade-out 0.16s ease forwards; }
.enjoy-modal.closing .enjoy-modal__box { animation: modal-pop-out 0.16s ease forwards; }
@keyframes modal-fade     { from { opacity: 0; } }
@keyframes modal-fade-out { to   { opacity: 0; } }
@keyframes modal-unfold {
  0%   { transform: scale(0.012, 0.02); animation-timing-function: cubic-bezier(0.5, 0, 0.3, 1); }
  38%  { transform: scale(0.012, 1);    animation-timing-function: cubic-bezier(0.25, 0.1, 0.2, 1); }
  100% { transform: scale(1, 1); }
}
@keyframes modal-pop-out  { to   { opacity: 0; transform: scale(0.9); } }
@keyframes panel-glide    { from { opacity: 0; transform: translateY(20px); } }
@keyframes modal-close-in { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .enjoy-modal, .enjoy-modal__box, .enjoy-panel.open, .enjoy-modal__close,
  .enjoy-modal.closing, .enjoy-modal.closing .enjoy-modal__box { animation: none; }
}
.enjoy-modal__box {
  position: relative;
  width: min(620px, 100%);
  max-height: 82vh;
  overflow: auto;
  background: var(--bg-soft);
  padding: 1.7rem 1.9rem;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
}
.enjoy-modal__close {
  position: absolute;
  top: 0.7rem;
  right: 0.8rem;
  background: none;
  border: none;
  font-family: inherit;
  font-size: 1.3rem;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
  padding: 0.2rem 0.4rem;
  transition: color 0.2s;
}
.enjoy-modal__close:hover { color: var(--accent); }

.enjoy-panel { display: none; }
.enjoy-panel.open { display: block; }
.enjoy-panel__body > p { max-width: 62ch; margin: 0; color: var(--muted); font-size: 0.9rem; line-height: 1.5; }

/* ── π convergence toy (Mathematics panel) ─── */
.pi { margin-top: 1.2rem; }
.pi__formula {
  margin: 0 0 0.8rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--faint);
  white-space: nowrap;
  overflow-x: auto;
}
.pi__digits {
  padding: 1rem 1.1rem;
  background: var(--bg);
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.95rem;
  line-height: 1.8;
  letter-spacing: 0.14em;
  word-break: break-all;
}
.pi__digits .is-locked { color: var(--text); }
.pi__digits .is-fresh { color: var(--accent); }
.pi__digits .is-wrong { color: var(--faint); }
.pi__controls {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: 0.8rem;
}
.pi__readout {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--muted);
  white-space: nowrap;
}
.pi__range {
  flex: 1;
  appearance: none;
  -webkit-appearance: none;
  height: 2px;
  background: var(--line);
  outline: none;
  cursor: pointer;
}
.pi__range::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--accent);
  border: none;
}
.pi__range::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--accent);
  border: none;
}
.pi__range:focus-visible { outline: 1px solid var(--accent); outline-offset: 4px; }


@media (max-width: 640px) {
  /* collapse the collage to a tidy 2-up grid — tilts and offsets off */
  /* no pin/ride on small screens — plain wrapped 2-up grid (enjoyride.js bails too) */
  .block--enjoy { padding: clamp(6rem, 20vh, 13rem) 0; }
  .enjoy-pin { position: static; height: auto; width: auto; margin-left: 0; padding: 0; overflow: visible; display: block; }
  .enjoy-track { flex-wrap: wrap; width: auto; gap: 1rem; transform: none !important; }
  .enjoy-step { width: 46%; opacity: 1; transform: none; }
  .enjoy-step .enjoy-card { width: 100%; }
  .enjoy-step:nth-child(odd) .enjoy-card,
  .enjoy-step:nth-child(even) .enjoy-card { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .enjoy-card, .enjoy-panel { transition: none; }
  /* enjoyride.js bails — unpin and let the track wrap as a static grid */
  .enjoy-pin { position: static; height: auto; display: block; }
  .enjoy-track { flex-wrap: wrap; }
  .enjoy-step { opacity: 1; transform: none; }
}

/* ── About ───────────────────────────────────────────────────────────────── */
.about-grid {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: clamp(3.5rem, 7vw, 7rem);
}
.about-text { display: flex; gap: 1.5rem; align-items: flex-start; }
.about-photo {
  position: relative;
  flex: none;
  width: 300px;
  margin: 0 2rem;   /* breathing room from the bio text and the toolbox */
  aspect-ratio: 4 / 5;
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--bg-soft);
}
/* Caption sits behind the photo at the top; revealed when the photo slides down. */
.about-photo__caption {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 0 0.65rem;
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  color: var(--muted);
  z-index: 1;
}
.about-photo__img {
  position: relative;
  z-index: 2;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.35s ease;
}
.about-photo:hover .about-photo__img { transform: translateY(34px); }
/* the bio fills the toolbox's old grid slot, level with the photo */
.about-bio { align-self: center; }
.about-bio p { color: var(--muted); margin: 0 0 1.1rem; }
.about-bio p:last-child { margin-bottom: 0; }
.about-bio em { color: var(--text); font-style: normal; border-bottom: 1px dashed var(--faint); }

.about-side { text-align: right; }
.about-side h4 {
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 1rem;
}
/* narrow, right-anchored list so the divider lines stay short */
.skills { list-style: none; margin: 0 0 0 auto; padding: 0; max-width: 320px; }
.skills li {
  position: relative;
  padding: 0.65rem 0;
  color: var(--text);
  border-bottom: 1px solid var(--line);
  font-size: 1.05rem;
  cursor: default;
  transition: color 0.2s;
}
.skills li:last-child { border-bottom: none; }
.skills li[data-tip]:hover { color: var(--accent); }

/* ── Cursor-tracking tooltip ─────────────────────────────────────────────── */
/* The caption follows the cursor while hovering a skill and hides when the
   cursor leaves it. Positioned/moved by tooltip.js. */
.skill-tip {
  position: fixed;
  top: 0;
  left: 0;
  max-width: 260px;
  padding: 0.5rem 0.7rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  color: var(--muted);                 /* punctuation / base */
  font-family: var(--mono);
  font-size: 0.82rem;
  letter-spacing: 0;
  line-height: 1.5;
  white-space: pre;                    /* render as a code line */
  text-align: left;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease;
  z-index: 10;
  will-change: transform;
}
.skill-tip.show { opacity: 1; }

/* Syntax token colours (dark) */
.skill-tip .tok-str { color: #9ece6a; }   /* strings — green */
.skill-tip .tok-fn  { color: #7aa2f7; }   /* functions / keywords — blue */
.skill-tip .tok-tag { color: #f78c6c; }   /* html tags — orange */

@media (prefers-color-scheme: light) {
  .skill-tip .tok-str { color: #4b8a2f; }
  .skill-tip .tok-fn  { color: #3060c0; }
  .skill-tip .tok-tag { color: #c05a2f; }
}

/* ── Contact ─────────────────────────────────────────────────────────────── */
.contact-links { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 1.5rem; }
.contact-links a,
.contact-links button {
  font-family: inherit;
  font-size: 1.05rem;
  color: inherit;
  background: none;
  border: none;
  border-bottom: 1px solid var(--line);
  padding: 0 0 2px;
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s;
}
.contact-links a:hover,
.contact-links button:hover { color: var(--accent); border-color: var(--accent); }

/* ── Footer small print (bottom row of the contact block) ────────────────── */
.footer__base {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-top: 3.5rem;
  padding-top: 1.4rem;
  border-top: 1px solid var(--line);
  color: var(--faint);
  font-size: 0.85rem;
}
.foot-left { display: flex; flex-direction: column; gap: 0.35rem; }
.colophon { color: var(--muted); }
.footer__base a:hover { color: var(--accent); }

/* ── Responsive ──────────────────────────────────────────────────────────── */
@media (max-width: 640px) {
  .about-grid { grid-template-columns: 1fr; }
  .about-photo { width: 110px; }
  .footer__base { flex-direction: column; align-items: flex-start; gap: 0.8rem; }
}
