/*
  SHARED COMPONENTS — used on both index.html and project.html.
  Depends on tokens.css being loaded first.
*/

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  /* The true page is WHITE. The homepage's blue is its own layer on
     .hero-stage (css/main.css) that fades in over this, with the paper
     texture fading in over the blue — so a slow load reads as
     white -> blue -> texture, never a bare flash of blue. */
  background: var(--color-surface-paper);
  /* `clip` not `hidden`: setting overflow-x alone to `hidden` forces the
     browser to compute overflow-y as `auto` too (a CSS 2.1 rule — only one
     axis may be `visible`, so the other axis gets promoted), which quietly
     turns html/body into their OWN scroll containers. That breaks every
     position:sticky element on the page (a sticky element's nearest
     scrolling ancestor becomes ambiguous between html and body instead of
     the viewport). `overflow-x: clip` prevents horizontal scroll without
     that side effect, since clip never implies a scroll container. */
  overflow-x: clip;
}

body {
  font-family: var(--font-mono);
  /* NOT text-transform:uppercase here — this is just the page-wide
     DEFAULT font, not every element using it. Since text-transform
     inherits, putting it here would force uppercase onto every piece of
     text on the page (including ones that override to a different font,
     like the hero title) unless THEY also explicitly set it back to
     none. Every element that should actually be uppercase mono text
     (nav, labels, captions, etc.) sets text-transform: uppercase itself,
     right alongside its own font-family: var(--font-mono) — see
     components.css/main.css/project.css. */
  color: var(--color-text-on-dark);
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; }

/* TEXT RULE (site-wide): lines of running text stay ~50 characters max
   and never end a paragraph on a widow (a last line of only 1-2 words).
   `pretty` makes the browser pull words down to avoid widows; `balance`
   evens out short multi-line headings. Body-copy classes also cap their
   width at 50ch (see .text-project-body). Where a design calls for
   different line lengths, set explicit lines instead (e.g. the contact
   modal in js/contact.js) and note it there. */
p, li, blockquote, figcaption { text-wrap: pretty; }
h1, h2, h3, h4 { text-wrap: balance; }

/* RULE: every button on this site inherits text-transform from its
   context, full stop. `font: inherit` below only carries family / size /
   weight / style / line-height — text-transform isn't part of the font
   shorthand, and buttons (unlike plain text) don't inherit it from an
   ancestor by default, so a button sitting inside an uppercase Syne Mono
   context (nav, footer, etc.) would otherwise render in whatever case it
   was typed in HTML. This is what silently broke the nav's "Contact"
   button. Fixing it once here means no button anywhere on the site can
   slip through un-uppercased, instead of re-declaring text-transform on
   every individual button. */
button { font: inherit; color: inherit; background: none; border: none; cursor: inherit; text-transform: inherit; }

/* ---------- type utility classes (map 1:1 to the tokens) ---------- */

.text-title {
  font-family: var(--font-display);
  font-weight: var(--text-title-weight);
  /* 98pt (--text-title-size) is the mockup's desktop value, not a fixed
     size for every screen — clamp() uses it as the ceiling and scales
     down on narrower viewports so it can never overflow/overlap the nav
     the way an un-clamped 98pt does below ~1200px wide. */
  font-size: clamp(42px, 9vw, var(--text-title-size));
  line-height: var(--text-title-line-height);
  /* NOT using text-transform:capitalize here on purpose: the hero title's
     letters are each split into their own `display: inline-block` span
     for the letter-by-letter animation (see js/main.js), and that breaks
     the word-boundary detection capitalize relies on — every isolated
     letter-box reads as "start of a word," capitalizing ALL of them
     instead of just the first letter per word (this is what caused the
     accidental all-caps look). Simplest fix: just type titles in the
     case you want in the HTML — it's a title, not user input, so there's
     nothing to sanitize here anyway. */
}

.text-h1 {
  font-family: var(--font-display);
  font-weight: var(--text-h1-weight);
  font-size: var(--text-h1-size);
  line-height: var(--text-h1-line-height);
}

.text-h2 {
  font-family: var(--font-mono);
  text-transform: uppercase; /* all Syne Mono text is uppercase, per spec */
  font-weight: var(--text-h2-weight);
  font-size: var(--text-h2-size);
}

.text-subheader {
  font-family: var(--font-mono);
  text-transform: uppercase; /* all Syne Mono text is uppercase, per spec */
  font-weight: var(--text-subheader-weight);
  font-size: var(--text-subheader-size);
}

.text-subheader-section {
  font-family: var(--font-display);
  font-weight: var(--text-subheader-section-weight);
  font-size: var(--text-subheader-section-size);
  line-height: var(--text-subheader-section-line-height);
  letter-spacing: var(--text-subheader-section-tracking);
}

.text-body1 {
  font-family: var(--font-mono);
  text-transform: uppercase; /* all Syne Mono text is uppercase, per spec */
  font-weight: var(--text-body1-weight);
  font-size: var(--text-body1-size);
}

.text-project-body {
  font-family: var(--font-display);
  font-weight: var(--text-project-body-weight);
  font-size: var(--text-project-body-size);
  line-height: var(--text-project-body-line-height);
  max-width: 50ch; /* TEXT RULE (top of file) */
}

.on-light { color: var(--color-text-on-light); }
.on-dark  { color: var(--color-text-on-dark); }

/* ---------- nav (identical on every page) ---------- */

.site-nav {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px var(--grid-margin);
  /* border-top sits flush with the viewport edge (top:0 + no outer
     margin); border-bottom closes the nav off as its own bordered strip,
     per the mockup. currentColor so it adapts automatically to whichever
     page's text color is active (white on the hero, blue on light pages) —
     same trick as .site-footer's divider below. */
  border-top: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  font-family: var(--font-mono);
  text-transform: uppercase; /* all Syne Mono text is uppercase, per spec */
  font-size: var(--text-nav1-size);
  pointer-events: none; /* re-enabled on the interactive children below */
}

.site-nav * { pointer-events: auto; }

/* Homepage-only load-in (see js/main.js) — a modifier, not the base
   .site-nav rule, so project.html's nav (no load sequence) is unaffected
   and just renders normally at full opacity from the start. */
.site-nav--fade-in {
  opacity: 0;
  transition: opacity 500ms ease;
}

.site-nav--fade-in.is-in {
  opacity: 0.9;
}

.nav-left {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* Logo placeholder: a round-cornered mark standing in for a real logo
   later. A real <a href="index.html"> on every page (including the
   homepage itself) — clicking it always does a genuine page reload, so
   it also resets the homepage's intro animations, not just a scroll. */
.logo-button {
  width: 48px;
  height: 48px;
  border-radius: 16px;
  background: rgba(252, 252, 252, 0.15);
  border: 1px solid rgba(252, 252, 252, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10pt;
  letter-spacing: 0.05em;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-smooth);
}

.logo-button:hover {
  background: rgba(252, 252, 252, 0.28);
}

.nav-right {
  display: flex;
  align-items: center;
  gap: 28px;
  font-size: 14pt;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.nav-right a,
.nav-right button {
  cursor: pointer;
  opacity: 0.85;
  transition: opacity var(--duration-fast) ease, color var(--duration-fast) ease;
}

.nav-right a:hover,
.nav-right button:hover {
  opacity: 1;
  color: var(--color-ink);
}

@media (max-width: 760px) {
  .site-nav { font-size: 16pt; }
}

/* Letter-by-letter load-in, reusable anywhere (title lines, the "I'm a…"
   row, the project page title) — spans are injected by main.js /
   project.js. Each user sets its own
   --letter-duration (title: slow + dramatic; "I'm a…": fast, matching
   the role-cycle text's own motion language) and toggles .is-in on
   itself once its letters should animate in. */
.letter-reveal .letter {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.4em);
  transition: opacity var(--letter-duration, 400ms) ease, transform var(--letter-duration, 400ms) ease;
}

.letter-reveal.is-in .letter {
  opacity: 1;
  transform: translateY(0);
}

.letter-reveal--fast {
  --letter-duration: 220ms;
}

/* Page load-in: blocks fade and glide up into place (same motion family
   as the letter reveals above), staggered by each element's own
   --load-delay. A CSS animation, so it plays from the very first paint
   with no flash of the final state; the empty `to` frame means each
   element lands on its OWN resting opacity/transform (e.g. a body
   paragraph's 0.85 opacity), not a forced 1. */
.load-rise {
  animation: load-rise 900ms var(--ease-smooth) var(--load-delay, 0ms) both;
}

@keyframes load-rise {
  from {
    opacity: 0;
    transform: translateY(28px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .load-rise { animation: none; }
  .letter-reveal .letter { transition: none; }
}

/* ---------- custom cursor ---------- */
/*
  The real cursor hides (desktop/pointer:fine only) and this label follows
  it. At rest it's just a small dot; hovering a [data-cursor-label] element
  grows it into a filled rectangle showing that element's label text, with
  a fill/text pairing read from --cursor-fill / --cursor-text (set per
  element via inline style or a modifier class) so any paper can have its
  own on-brand contrast pair.
*/

body.custom-cursor-active,
body.custom-cursor-active * {
  cursor: none !important;
}

.cursor-label {
  position: fixed;
  left: 0;
  top: 0;
  z-index: var(--z-cursor-label);
  pointer-events: none;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  overflow: hidden; /* BUG FIX guard: resting dot can never show leftover label text (see collapse() in js/cursor.js) */
  font-family: var(--font-mono);
  text-transform: uppercase; /* all Syne Mono text is uppercase, per spec */
  font-size: 10pt;
  letter-spacing: 0.08em;
  border-radius: 999px;
  background: #FFFFFF;
  color: var(--color-ink);
  /* `difference` inverts against whatever's underneath, so the resting
     dot stays visible on ANY background color automatically — no need to
     hand-pick a contrasting fill per section. Turned off below once the
     label expands with an explicit --cursor-fill/--cursor-text pair,
     since blending would distort those deliberately-chosen colors. */
  mix-blend-mode: difference;
  opacity: 0;
  width: 10px;
  height: 10px;
  padding: 0;
  transition:
    opacity var(--duration-fast) ease,
    width var(--duration-base) var(--ease-smooth),
    height var(--duration-base) var(--ease-smooth),
    padding var(--duration-base) var(--ease-smooth),
    background-color var(--duration-fast) ease,
    color var(--duration-fast) ease;
}

.cursor-label.is-visible {
  opacity: 0.9;
}

/* Over nav-right's "Mind Cloud"/"Contact" (see data-cursor-solid="white"
   below), the text itself turns charcoal (var(--color-ink)) on hover —
   the dot's usual mix-blend-mode:difference trick would react to THAT
   text color change in a way that reads as muddy here, so this state
   forces it to plain, un-blended white instead for a clean, deliberate
   contrast against the now-dark text. */
.cursor-label.is-solid-white {
  background: #FFFFFF;
  mix-blend-mode: normal;
}

.cursor-label.is-expanded {
  width: auto;
  height: auto;
  padding: 10px 20px;
  border-radius: 24px;
  mix-blend-mode: normal;
}

/* Hand-gesture cursors (contact modal wave, Resume thumbs-up) — see
   HAND GESTURE CURSORS in js/cursor.js. Figma frames from
   assets/cursor/, all stacked; only .is-current shows. Pops in with the
   site's bounce ease. */
.cursor-gesture {
  position: fixed;
  left: 0;
  top: 0;
  z-index: var(--z-cursor-label);
  pointer-events: none;
  width: 64px;  /* frame canvas; the hand itself fills ~60% of it */
  height: 64px;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.5);
  transition:
    opacity var(--duration-fast) ease,
    transform var(--duration-base) var(--ease-bounce);
}

.cursor-gesture.is-active {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

.cursor-label.is-gesture-mode {
  opacity: 0;
}

.cursor-gesture__frame,
.touch-gesture__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  visibility: hidden;
}

.cursor-gesture__frame.is-current,
.touch-gesture__frame.is-current {
  visibility: visible;
}

@media (pointer: coarse) {
  .cursor-label, .cursor-gesture { display: none; }
}

/* ---------- touch stand-ins (js/touch.js; html.is-touch = phone/tablet) ---------- */

html.is-touch a,
html.is-touch button {
  -webkit-tap-highlight-color: transparent;
}

/* label flash: the cursor label's expanded pill, popping up above the finger */
.touch-label {
  position: fixed;
  z-index: var(--z-cursor-label);
  pointer-events: none;
  padding: 10px 20px;
  border-radius: 24px;
  background: #FFFFFF;
  color: var(--color-ink);
  font-family: var(--font-mono);
  text-transform: uppercase; /* all Syne Mono text is uppercase, per spec */
  font-size: 10pt;
  letter-spacing: 0.08em;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, -100%) translateY(-28px) scale(0.85);
  transition:
    opacity var(--duration-fast) ease,
    transform var(--duration-base) var(--ease-bounce);
}

.touch-label.is-shown {
  opacity: 0.95;
  transform: translate(-50%, -100%) translateY(-36px) scale(1);
}

/* hands that play in place (contact popup corner, beside Resume) */
.touch-gesture {
  position: absolute;
  z-index: 2;
  width: 72px;
  height: 72px;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.5);
  transition:
    opacity var(--duration-fast) ease,
    transform var(--duration-base) var(--ease-bounce);
}

.touch-gesture.is-shown {
  opacity: 1;
  transform: scale(1);
}

.touch-gesture--corner {
  right: 20px;
  bottom: 20px;
}

.touch-gesture--resume {
  margin-top: -36px; /* top is set to the button's vertical middle in JS */
}

/* light tap feedback — a quick press-in, since there's no hover */
html.is-touch .logo-button,
html.is-touch .nav-right a,
html.is-touch .nav-right button,
html.is-touch .contact-modal__email,
html.is-touch .contact-modal__resume {
  transition: transform 120ms ease, opacity 120ms ease, background var(--duration-fast) ease, color var(--duration-fast) ease, border-color var(--duration-fast) ease;
}

html.is-touch .logo-button:active,
html.is-touch .nav-right a:active,
html.is-touch .nav-right button:active {
  transform: scale(0.94);
  opacity: 0.7;
}

/* contact: the email underline + Resume fill show on tap (their hover looks) */
html.is-touch .contact-modal__email:active {
  border-color: var(--color-blue);
}

html.is-touch .contact-modal__resume:active {
  background: var(--color-blue);
  color: var(--color-paper-white);
  transform: scale(0.96);
}

/* ---------- paper component (shared base; homepage/project page extend it) ---------- */
/*
  A "paper" is any draggable/clickable card styled like a sheet of paper.
  Hover = drift a few px away from the cursor (set via --drift-x/--drift-y,
  JS-driven). Click = .is-active, which a page-specific rule promotes to
  --z-paper-active. This base class owns the drift + shadow + tape visuals;
  pages own position/size/z-index for their own specific papers.
*/

/*
  Two-layer structure, deliberately: .paper owns WHERE the sheet sits
  (left/top/rotate — set per-paper, either inline or via a modifier
  class) and never gets a hover/drift transform itself. .paper__inner
  owns HOW it reacts (the hover-drift translate, and the click-to-front
  scale bump) via its own independent `transform`. Keeping these on two
  separate elements means the drift animation and the base placement
  never fight over one `transform` property.
*/

.paper {
  position: absolute;
  cursor: pointer;
}

.paper__inner {
  --drift-x: 0px;
  --drift-y: 0px;
  --drift-rot: 0deg;
  position: relative;
  width: 100%;
  height: 100%;
  background: var(--color-paper-white);
  box-shadow: 0 20px 48px rgba(29, 29, 30, 0.22);
  transform: translate(var(--drift-x), var(--drift-y)) rotate(var(--drift-rot));
  transition:
    transform var(--duration-base) var(--ease-smooth),
    box-shadow var(--duration-base) ease;
  will-change: transform;
}

.paper:hover .paper__inner {
  box-shadow: 0 28px 64px rgba(29, 29, 30, 0.3);
}

.paper.is-active {
  z-index: var(--z-paper-active);
}

/* Actively being dragged (js/drag.js) — above the nav, above everything.
   No transition while dragging: it needs to track the pointer 1:1, not
   ease toward it. */
.paper.is-dragging {
  z-index: var(--z-dragging);
  cursor: grabbing;
}

.paper.is-dragging .paper__inner {
  transition: none;
}

/* Persists after the drag ends (see js/drag.js) — once moved, a box stays
   above normal page content permanently, not just mid-drag, since it may
   now sit on top of text/sections at its new, arbitrary position. */
.paper.was-dragged {
  z-index: var(--z-dragging);
}

.paper.is-active .paper__inner {
  box-shadow: 0 36px 80px rgba(29, 29, 30, 0.38);
}

/* Homepage papers only (js/paper.js's hover-to-front rule) — a smooth
   "lift" cue to sell the z-index jump, which has no transition of its
   own. translate() still comes from --drift-x/--drift-y (the hover
   repulsion) — scale is ALL this adds, deliberately no rotate(), ever,
   per spec. */
.paper.is-hover-front .paper__inner {
  transform: translate(var(--drift-x), var(--drift-y)) scale(1.03);
  box-shadow: 0 32px 72px rgba(29, 29, 30, 0.34);
}

.paper__tape {
  position: absolute;
  width: 34px;
  height: 18px;
  background: var(--color-tape);
  opacity: 0.85;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}

/* ---------- contact modal ---------- */
/*
  Full-viewport paper graphic with a 12px buffer on every side (the outer
  wrapper is transparent — "0% opacity fill" — and sits inset:0; the
  visible paper surface is the inner element, inset by the 12px buffer).
*/

.contact-modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  background: rgba(0, 0, 0, 0); /* 0% opacity fill, per spec — purely a click-catcher */
  display: flex;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-base) var(--ease-smooth);
}

.contact-modal.is-open {
  opacity: 1;
  pointer-events: auto;
}

.contact-modal__paper {
  position: absolute;
  inset: 12px;
  background: var(--color-paper-white);
  display: flex;
  align-items: flex-start;   /* content sits high, not centered */
  justify-content: flex-start; /* left-justified */
  overflow-y: auto;
  transform: scale(0.96);
  transition: transform var(--duration-base) var(--ease-bounce);
}

.contact-modal.is-open .contact-modal__paper {
  transform: scale(1);
}

/* Left-justified column with a large left gutter, starting high on
   the paper. Built by js/contact.js. */
.contact-modal__content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
  padding: clamp(72px, 14vh, 160px) 24px 48px clamp(24px, 12vw, 200px);
  color: var(--color-blue);
}

.contact-modal__email {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(28px, 6vw, 64px);
  color: var(--color-blue);
  letter-spacing: 0.02em;
  border-bottom: 2px solid transparent;
  transition: border-color var(--duration-fast) ease;
}

.contact-modal__email:hover {
  border-color: var(--color-blue);
}

/* Smaller, lighter text under the email. Lines are written out one per
   span (see js/contact.js); each is its own block, and if a narrow
   screen forces one to wrap, `balance` splits it evenly — no widows. */
.contact-modal__meta,
.contact-modal__bio {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(15px, 1.4vw, 19px);
  line-height: 1.45;
  opacity: 0.75;
}

.contact-modal__meta {
  margin-top: 28px;
  font-weight: 500; /* one step heavier than the bio below */
}

.contact-modal__bio {
  margin-top: 20px;
}

.contact-modal__line {
  display: block;
  text-wrap: balance;
}

.contact-modal__resume {
  margin-top: clamp(64px, 12vh, 140px);
  padding: 10px 22px;
  border: 1px solid var(--color-blue);
  border-radius: 999px;
  font-family: var(--font-mono);
  text-transform: uppercase; /* all Syne Mono text is uppercase, per spec */
  font-size: 10pt;
  letter-spacing: 0.08em;
  color: var(--color-blue);
  transition: background var(--duration-fast) ease, color var(--duration-fast) ease;
}

.contact-modal__resume:hover {
  background: var(--color-blue);
  color: var(--color-paper-white);
}

/* dead-link rule: no file yet -> greyed out, not clickable (js/contact.js) */
.contact-modal__resume.is-unavailable {
  opacity: 0.35;
  pointer-events: none;
}

.contact-modal__close {
  position: absolute;
  top: 24px;
  right: 28px;
  font-family: var(--font-mono);
  text-transform: uppercase; /* all Syne Mono text is uppercase, per spec */
  font-size: 10pt;
  letter-spacing: 0.08em;
  color: var(--color-blue);
  cursor: pointer;
}

/* ---------- site footer (identical on every page) ---------- */

.site-footer {
  padding: 24px var(--grid-margin) 32px;
  border-top: 1px solid currentColor;
  /* blue type on the white page, on every page */
  background: var(--color-surface-paper);
  color: var(--color-blue);
}

.site-footer p {
  opacity: 0.6;
  letter-spacing: 0.06em;
}

@media (prefers-reduced-motion: reduce) {
  .paper, .cursor-label, .contact-modal, .contact-modal__paper {
    transition: none !important;
  }
}
