/* ==========================================================================
   main.css — Design tokens + CSS reset
   All custom properties here are consumed by Plans 02 (nav) and 03 (hero).
   Do NOT add component-specific styles here.
   ========================================================================== */

:root {
  /* Colors — minhpham.design palette */
  --color-bg: #0d0d0d;
  --color-surface: #161614;
  --color-text: #d0c8b5;
  --color-muted: #7a7464;
  --color-accent: #f16027;
  --color-loader-accent: #d2c1a8;
  --color-loader-accent-rgb: 210, 193, 168;

  /* RGB companions for rgba() usage */
  --color-bg-rgb: 13, 13, 13;
  --color-text-rgb: 208, 200, 181;
  --color-lens-text-rgb: 13, 13, 13;

  /* Semantic tokens */
  --color-reveal-base: #2e2b28;
  --color-border: rgba(255, 255, 255, 0.06);
  --color-backdrop: rgba(10, 10, 10, 0.95);
  --color-backdrop-solid: rgba(10, 10, 10, 0.98);

  /* Spacing scale (multiples of 4px) */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-2xl: 48px;
  --space-3xl: 64px;
  --space-4xl: 96px;

  /* Layout */
  --nav-height: 72px;
  --content-max: 1280px;
  --content-gutter: clamp(24px, 5vw, 80px);

  /* Shared "honest reveal" headline size. Hero/About/Experience all share ONE
     size on mobile — both the visible (professional) layer AND the hidden
     (honest) lens layer read it via this token, so the three reveal sections
     look identical. Desktop is unchanged (each section keeps its own size);
     this token is only applied inside the mobile blocks below. */
  --reveal-fs: clamp(42px, 12vw, 72px);

  /* Fonts */
  /* Slot 1 (headings/big text) */
  --font-display: 'Geist', sans-serif;
  /* Slot 2 (body/nav/paragraphs) */
  --font-body: 'Syne', sans-serif;
  /* Slot 3 (small technical labels/stats) */
  --font-mono: 'Syne', sans-serif;
}

/* Mobile reveal-size steps — keep hero/about/experience locked together.
   Values match the hero's existing mobile sizes so the hero is unchanged and
   the other two rise to meet it. */
@media (max-width: 768px) { :root { --reveal-fs: clamp(42px, 12vw, 72px); } }
@media (max-width: 480px) { :root { --reveal-fs: 32px; } }

/* ==========================================================================
   Cross-document view transitions
   Seamless nav between the main page and the project case-study pages: the page
   body cross-fades while the navbar (logo + links) is morphed in place instead of
   hard-cutting. Both documents opt in via this shared rule (main.css is loaded on
   every page). Chromium animates it; other browsers navigate normally — no flash
   regression. The named elements keep their identity across the navigation, so
   they hold position and morph rather than fade (one of each per page → names stay
   unique). prefers-reduced-motion drops all VT animation → instant navigation.
   ========================================================================== */
@view-transition { navigation: auto; }
.nav-logo    { view-transition-name: site-nav-logo; }
.nav-links   { view-transition-name: site-nav-links; }
.side-social { view-transition-name: site-social; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* ==========================================================================
   Reset
   ========================================================================== */

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

/* Smooth scroll only when user has not requested reduced motion.
   Default is auto (instant) — gated as a baseline for REVIEWS.md priority 4. */
html {
  scroll-behavior: auto;
  /* Nothing should ever scroll the page sideways — large display type,
     reveal-lens overflow, and parallax props stay inside the viewport.
     clip (not hidden) avoids creating a scroll container so position:sticky
     and scroll-driven animations keep working. */
  overflow-x: clip;
  /* Remove the mobile tap-highlight box site-wide. This property is inherited,
     so setting it once at the root kills the rectangle on every clickable element
     (nav links, project-card titles, footer links, etc.) on tap. The keyboard
     focus ring (:focus-visible outline below) is unrelated and stays for a11y. */
  -webkit-tap-highlight-color: transparent;
}

body {
  overflow-x: clip;
}

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* Lenis smooth scroll — recommended baseline.
   .lenis-smooth forces scroll-behavior:auto, overriding the rule above so
   native CSS smoothing never fights Lenis's rAF-driven position. height:auto
   lets Lenis measure the real document height. data-lenis-prevent opts an
   element out of hijack (e.g. an internally-scrolling panel). */
html.lenis,
html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }
.lenis.lenis-smooth iframe { pointer-events: none; }

body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  min-height: 100vh;
  min-height: 100svh;   /* constant small-viewport height — no address-bar reflow jitter */
  -webkit-font-smoothing: antialiased;
}

@media (pointer: fine) {
  /* Show BOTH cursors (minhpham's feel): the native OS pointer for precise click
     feedback AND the trailing disc (.site-cursor) for the smooth follow. The disc
     trailing behind the arrow on fast moves is BY DESIGN — the snap was never the
     two-cursor gap, it was the disc's follow math leaping on a stalled frame (now
     fixed in main.js). `default !important` forces a uniform ARROW everywhere —
     `auto` would let the browser pick per element (I-beam over text, hand over
     links), which is the flicker we don't want. */
  body.site-ready,
  body.site-ready *,
  body.site-ready *::before,
  body.site-ready *::after {
    cursor: default !important;
  }
}

/* minhpham-style cursor: ONE element, mix-blend-mode:difference. It is never
   clipped — wherever it overlaps anything (logo, social icons, text, sections)
   the overlap region inverts automatically via the blend. White over the dark
   bg reads near-white; over light theme / cream logo / orange icons it inverts
   the underlying pixels. No per-target lens, no enter/leave state swap.
   Size still tweens (contract over links, expand over skill cards). */
.site-cursor {
  --cursor-size: 40px;
  position: fixed;
  top: 0;
  left: 0;
  width: var(--cursor-size);
  height: var(--cursor-size);
  border-radius: 50%;
  background: var(--color-accent);
  /* Global orange-invert: the cursor blends with everything beneath it, so it
     inverts over the logo, icons, text and sections automatically. This costs
     a main-thread repaint per frame (a blended moving element can't be a pure
     GPU layer) — tolerable now that the off-screen Three.js render loop and the
     idle lens clip-path repaints have been removed, which is what was actually
     starving the main thread and making this snap. Sits at z 9000 — above all page
     content (which tops out ~200) so the invert is visible everywhere, but BELOW
     the preloader (9999) and below the light-theme social rail (9500), which is
     raised above the cursor so its invert layer can paint over the disc. In dark
     theme the rail stays at z 100, so the difference cursor still inverts it. */
  mix-blend-mode: difference;
  opacity: 0;
  pointer-events: none;
  z-index: 9000;
  will-change: transform, opacity;
}

/* Over the hero's playing video, blending the moving disc against changing
   video frames is the one expensive case (the residual snap). JS adds this
   class while the cursor is over the hero; dropping to a normal solid disc
   makes it cheap to move. Looks near-identical over the dark hero overlay. */
.site-cursor.cursor-solid {
  mix-blend-mode: normal;
}

/* Light (blue/white) theme cursor.
   Dark theme uses mix-blend-mode:difference — content-aware, never an opaque dot,
   one consistent look. Difference can't port to a near-white page (navy inverted on
   white = pale yellow), so the light-canvas analog is mix-blend-mode:multiply:
     • over the white page, multiply(white, electric-blue) = electric blue → the disc
       reads as one consistent electric-blue everywhere (matches the revealed text);
     • over the navbar logo/icons it darkens-through instead of sitting on top as an
       opaque block — so it never covers them, exactly like difference in dark theme.
   Disc colour is the electric blue --color-text (#1E5EFF), the same blue the
   hidden-text reveal shows, so canvas and reveal read as the same cursor. */
:root.theme-light .site-cursor {
  background: var(--color-text);
  mix-blend-mode: multiply;
}

/* ── Hold-to-reveal button — touch devices only ──────────────────────────── */
.lens-reveal-btn {
  display: none; /* hidden on pointer-fine devices */
}

@media (max-width: 1024px) {
  .lens-reveal-btn {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    position: fixed;
    bottom: 28px;
    left: 50%;
    transform: translateX(-50%) translateY(8px);
    background: rgba(255, 255, 255, 0.07);
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 100px;
    padding: 10px 18px 10px 13px;
    color: rgba(255, 255, 255, 0.5);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    z-index: 200;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
    -webkit-tap-highlight-color: transparent;
    cursor: pointer;
    white-space: nowrap;
    /* Contextual fade — hidden until JS adds .is-visible while a dual-text
       section (hero/about/experience) is on or near screen. */
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.22s ease, visibility 0.22s ease, transform 0.22s ease;
  }

  .lens-reveal-btn.is-visible {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: translateX(-50%) translateY(0);
  }

  :root.theme-light .lens-reveal-btn {
    background: rgba(0, 0, 0, 0.05);
    border-color: rgba(0, 0, 0, 0.14);
    color: rgba(0, 0, 0, 0.4);
  }

  .lens-reveal-btn-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-accent);
    flex-shrink: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .lens-reveal-btn { transition: none; }
}

/* ── Mobile edge fade ─────────────────────────────────────────────────────
   minhpham's mobile frame: the page CONTENT dissolves into the background at the
   very top and bottom as it scrolls past, instead of being covered by a coloured
   band. We do this with two fixed gradients painted in the *background colour* —
   so content (and the reveal flood) fades to bg, reading as a soft fade-out, not
   an overlay. They sit above the page content but BELOW the nav (z 100), the hold
   button (z 200) and the cursor, so nothing interactive is touched. Because the
   colour is --color-bg, this works in both themes (fades to near-white on light,
   near-black on dark). Touch viewports only; desktop is untouched. */
@media (max-width: 1024px) {
  body::before,
  body::after {
    content: '';
    position: fixed;
    left: 0;
    right: 0;
    height: 120px;
    z-index: 90;
    pointer-events: none;
    opacity: 1;
    transition: opacity 0.25s ease;
  }
  body::before {
    top: 0;
    background: linear-gradient(to bottom,
      rgba(var(--color-bg-rgb), 1) 0%,
      rgba(var(--color-bg-rgb), 0.86) 22%,
      rgba(var(--color-bg-rgb), 0.45) 55%,
      rgba(var(--color-bg-rgb), 0) 100%);
  }
  body::after {
    bottom: 0;
    background: linear-gradient(to top,
      rgba(var(--color-bg-rgb), 1) 0%,
      rgba(var(--color-bg-rgb), 0.88) 22%,
      rgba(var(--color-bg-rgb), 0.5) 55%,
      rgba(var(--color-bg-rgb), 0) 100%);
  }
  /* While a reveal flood is up the edges must NOT fade — the flood should read as
     solid edge-to-edge. JS adds .lens-revealing to <body> on hold. */
  body.lens-revealing::before,
  body.lens-revealing::after {
    opacity: 0;
  }
}

/* ── Reveal-region group clip (phones only) ───────────────────────────────
   The hold-reveal flood is full-bleed (inset:-1000px) so it can fill the screen.
   These wrappers clip that bleed to the group's vertical span, so the accent fills
   the viewport across the grouped reveal sections but STOPS at the boundary of the
   sections that have no honest layer (Toolbelt, Projects, …). ≤768 only: above that
   the What I Build pin is active and the desktop cursor reveal is untouched. */
@media (max-width: 768px) {
  .reveal-region { overflow: clip; }
}

img, svg {
  display: block;
  max-width: 100%;
}

ul {
  list-style: none;
}

a {
  color: inherit;
  text-decoration: none;
}

button {
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  font: inherit;
}

/* Ensure section headings land below the fixed nav on anchor scroll */
section {
  scroll-margin-top: var(--nav-height);
}

/* Accessible focus ring — accent color on all interactive elements */
:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}

/* ==========================================================================
   Dual-mode wiring — Phase 5 ships the toggle button (MODE-01).
   Wire CSS now so toggling body.mode-honest works immediately.
   ========================================================================== */

[data-professional] { display: block; }
[data-honest]       { display: none; }

body.mode-honest [data-professional] { display: none; }
body.mode-honest [data-honest]       { display: block; }

.reveal-line {
  display: grid;
  position: relative;
  overflow: hidden;
  font: inherit;
  line-height: inherit;
  letter-spacing: inherit;
  white-space: nowrap;
}

.reveal-line-base,
.reveal-line-inner {
  display: block;
  grid-area: 1 / 1;
  font: inherit;
  line-height: inherit;
  letter-spacing: inherit;
  white-space: inherit;
}

.reveal-line-base {
  color: var(--color-reveal-base);
}

.reveal-line-base .about-accent,
.reveal-line-base .timeline-accent {
  color: var(--color-reveal-base);
}

.reveal-line-inner {
  position: relative;
  color: var(--color-text);
  will-change: clip-path;
}

@media (max-width: 768px), (prefers-reduced-motion: reduce) {
  .reveal-line,
  .reveal-line-base,
  .reveal-line-inner {
    display: inline;
    white-space: normal;
    transform: none !important;
    will-change: auto;
  }

  .reveal-line-base {
    display: none;
  }

  .reveal-line-inner {
    position: static;
  }
}

/* ==========================================================================
   Cursor lens reveal — minhpham's mask method (shared: hero/about/timeline)
   --------------------------------------------------------------------------
   The orange "honest" overlay is hidden by a circular SVG mask. Its centre
   tracks the cursor (--x / --y) and its diameter (--size) tweens 0 → full on
   hover. Moving a raster mask via mask-position / mask-size is a compositor
   operation, NOT a per-frame clip-path repaint — this is exactly why minhpham's
   cursor stays smooth over these sections. JS writes --x / --y every frame and
   --size only when the circle grows/shrinks (see paintLens in main.js).

   GPU-layer promotion (translateZ(0)) is applied separately below, desktop-only.
   ========================================================================== */
.hero-lens,
.about-lens,
.timeline-lens,
.wib-lens,
.history-lens,
.region-honest {
  --size: 0px;
  --x: 50%;
  --y: 50%;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='50' fill='%23000'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='50' fill='%23000'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: calc(var(--x) - var(--size) / 2) calc(var(--y) - var(--size) / 2);
          mask-position: calc(var(--x) - var(--size) / 2) calc(var(--y) - var(--size) / 2);
  -webkit-mask-size: var(--size) var(--size);
          mask-size: var(--size) var(--size);
}

/* ==========================================================================
   Mobile hold-reveal — SINGLE honest layer per region (minhpham architecture)
   --------------------------------------------------------------------------
   One .region-honest overlay per .reveal-region is the ONE accent flood + ONE
   circular mask (mask wiring is the shared rule above). On touch, JS relocates
   each section's honest lens (.hero-lens, .about-lens, …) INTO this overlay and
   positions each as a plain block over its section — so the whole region reveals
   as one continuous sheet, never 5 separately-masked boxes. Desktop keeps the
   per-section lenses in place for the cursor reveal, so the overlay is hidden
   there and this whole block is ≤1024 only.
   ========================================================================== */
.region-honest { display: none; }

@media (max-width: 1024px) {
  .reveal-region { position: relative; }   /* containing block for the overlay */

  .region-honest {
    display: block;
    position: absolute;
    inset: 0;                       /* exactly the region box → flood is region-bounded */
    background: var(--color-accent); /* the ONE flood */
    z-index: 10;                    /* above all section content */
    pointer-events: none;
  }
  :root.theme-light .region-honest { background: var(--color-loader-accent); }

  /* Relocated honest nodes: strip their own flood + mask and let the overlay own
     both. JS sets top/left/width/height inline to overlay each section's box. */
  .region-honest .hero-lens,
  .region-honest .about-lens,
  .region-honest .wib-lens,
  .region-honest .timeline-lens,
  .region-honest .history-lens {
    display: block;
    position: absolute;
    inset: auto;
    background: transparent;
    z-index: auto;
    -webkit-mask: none;
            mask: none;
  }
}

/* GPU-layer promotion for the cursor-driven reveal — desktop only.
   The hero <video> used to force this implicitly (anything painted over a video
   layer gets promoted). With the video dropped, the hero lens demoted to the root
   layer, so the brief mask re-raster on grow/shrink — and the blend cursor reading
   the backdrop — thrashed the root layer that holds every line of page text, and
   the reveal went jittery. translateZ(0) gives each lens its own layer so that
   work stays isolated. Scoped to (min-width:1025px) + (pointer:fine): that's where
   the JS cursor lens runs (mqHandheld = max-width:1024px) and where the lens is
   section-sized (inset:0). Below it the lens balloons to inset:-1000px for the
   touch path — promoting THAT would allocate a ~25MB offscreen texture per lens. */
@media (min-width: 1025px) and (pointer: fine) {
  .hero-lens,
  .about-lens,
  .timeline-lens {
    transform: translateZ(0);
  }
}

/* ==========================================================================
   Light (blue fintech) theme — toggled via :root.theme-light
   Applied by js/theme.js; persisted in localStorage('portfolio-theme').
   ========================================================================== */

:root.theme-light {
  --color-bg: #F8FAFC;
  --color-surface: #EEF4FF;
  --color-text: #1E5EFF;
  --color-muted: #5B6B8C;
  --color-accent: #0A1F7A;
  --color-loader-accent: #1E5EFF;
  --color-loader-accent-rgb: 30, 94, 255;

  --color-bg-rgb: 248, 250, 252;
  --color-text-rgb: 30, 94, 255;
  --color-lens-text-rgb: 0, 207, 255;

  --color-reveal-base: #C5D5F5;
  --color-border: rgba(10, 31, 122, 0.1);
  --color-backdrop: rgba(248, 250, 252, 0.95);
  --color-backdrop-solid: rgba(248, 250, 252, 0.98);
}
