/* ==========================================================================
   enhancements.css — additive "live layer" for portfolio-v2
   --------------------------------------------------------------------------
   Loaded AFTER index.html's <style> block. Nothing here is structural:
   removing this file (and its <link> in index.html) returns the site to its
   prior state. All motion is gated behind prefers-reduced-motion.
   ========================================================================== */

/* ── Easing / timing tokens (the site defines none of its own) ───────────── */
:root {
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-util: cubic-bezier(0.4, 0, 0.2, 1);
  --dur-quick: 240ms;
  --dur-calm:  420ms;
}

/* Stop the elastic over-scroll bounce from revealing the blue page background
   past the content edges. Without this, the home page (which ends in the red
   Work section) shows a blue strip on bounce, since the page background is the
   theme blue. Disables pull-to-refresh / rubber-banding (fine for this site).
   Supported in iOS Safari 16+. */
html, body { overscroll-behavior-y: none; }

/* ==========================================================================
   [2] Mobile navigation menu
   Below 768px the inline nav links are hidden and replaced by a "Menu"
   toggle that opens a full-screen red panel. Covers both the overlay nav
   (.py-nav) and the project-page chrome nav (.py-chrome). The panel is only
   in the DOM while open (conditionally rendered in App.jsx).
   ========================================================================== */

/* Toggle: a standalone fixed element (NOT inside .py-nav). A <button> inside
   the nav's mix-blend-mode:difference group fails to composite, so it lives
   on its own here. Hidden on desktop, where the inline links serve. */
.py-nav-toggle {
  display: none;
  position: fixed;
  top: 14px;
  right: 18px;
  z-index: 210;                     /* above the panel (200) so the x closes it */
  margin: 0;
  padding: 5px;
  background: none;
  border: 0;
  cursor: pointer;
  color: #f4ece8;                   /* cream + difference: legible over the blue pages and the red work band */
  mix-blend-mode: difference;
}
/* When the menu is open the toggle sits on the red panel — render it crisply in
   white (no blend) so the x reads cleanly. */
.py-nav-toggle[aria-expanded="true"] {
  color: #fff;
  mix-blend-mode: normal;
}
/* Circular info/close icon. Circle is shared; the inner "i" and "x" crossfade. */
.py-nav-icon { display: block; width: 27px; height: 27px; }
.py-nav-icon circle,
.py-nav-icon line { stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; }
.py-nav-icon .ico-dot { fill: currentColor; stroke: none; }
.py-nav-icon .ico-info,
.py-nav-icon .ico-close { transform-origin: 14px 14px; }
.py-nav-icon .ico-close { opacity: 0; }
.py-nav-toggle[aria-expanded="true"] .ico-info  { opacity: 0; }
.py-nav-toggle[aria-expanded="true"] .ico-close { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .py-nav-icon .ico-info,
  .py-nav-icon .ico-close { transition: opacity 200ms ease, transform 280ms cubic-bezier(0.16, 1, 0.3, 1); }
  .py-nav-icon .ico-close { transform: rotate(-90deg); }
  .py-nav-toggle[aria-expanded="true"] .ico-info  { transform: rotate(90deg); }
  .py-nav-toggle[aria-expanded="true"] .ico-close { transform: rotate(0deg); }
}

/* Full-screen panel. Sits above the fixed nav (z 100). */
.py-mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: #A50208;
  display: flex;
  align-items: center;
  padding: 80px 32px 120px;          /* asymmetric bottom pad lifts the centred link stack toward optical centre */
}
.py-mobile-menu nav {
  display: flex;
  flex-direction: column;
  align-items: center;     /* centre the menu links */
  text-align: center;
  gap: 2px;
  width: 100%;
}
.py-mobile-menu nav a {
  font-family: "Goudy Bookletter 1911", "Cormorant Garamond", Garamond, serif;
  font-style: italic;
  font-size: clamp(40px, 13vw, 76px);
  line-height: 1.12;
  color: #fff;
  text-decoration: none;
  padding: 6px 0;
}
/* Show the menu / hide the inline links below the breakpoint. */
@media (max-width: 767px) {
  .py-nav > a:not(.py-nav-logo) { display: none; }   /* overlay nav: keep logo, drop links */
  .py-chrome > nav { display: none; }                /* chrome nav: drop links */
  .py-nav-toggle { display: inline-flex; align-items: center; justify-content: center; }
  .py-nav .py-nav-logo { font-size: 30px; }          /* logo larger than the toggle icon */
  .py-chrome .mark .glyph { font-size: 30px; }
}

/* Focus rings for keyboard users only. iOS Safari mis-fires :focus-visible on
   tap, and the menu moves focus programmatically on open/close, leaving a box
   stuck after touch. So gate outlines on a detected input modality (data-input
   is set to "kbd" on Tab, "mouse" on any pointer — see the script in index.html). */
html { -webkit-tap-highlight-color: transparent; }
html:not([data-input="kbd"]) :focus { outline: none; }
html[data-input="kbd"] .py-nav-toggle:focus { outline: 2px solid currentColor; outline-offset: 4px; }
html[data-input="kbd"] .py-mobile-menu nav a:focus { outline: 2px solid rgba(255,255,255,0.5); outline-offset: 4px; }

/* Entrance motion — gentle; entirely off under reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .py-mobile-menu { animation: pm-fade var(--dur-quick) var(--ease-soft) both; }
  .py-mobile-menu nav a { animation: pm-link var(--dur-calm) var(--ease-soft) both; }
  .py-mobile-menu nav a:nth-child(1) { animation-delay:  80ms; }
  .py-mobile-menu nav a:nth-child(2) { animation-delay: 130ms; }
  .py-mobile-menu nav a:nth-child(3) { animation-delay: 180ms; }
  .py-mobile-menu nav a:nth-child(4) { animation-delay: 230ms; }
  .py-mobile-menu nav a:nth-child(5) { animation-delay: 280ms; }
}
@keyframes pm-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes pm-link { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* Decorative motifs in the opened menu — a flying crane (top) + a cloud band
   (bottom) framing the links, to make the panel a touch livelier. These are the
   blue Work-section motifs, the only ones that read on the red panel (the koi /
   birds are red and would vanish). Faint, behind the links (nav is raised to
   z-index 1), and non-interactive. They load only when the menu first opens,
   since the panel is conditionally rendered. Static — no motion to gate.
   Mobile-only wrapper keeps desktop provably untouched. */
@media (max-width: 767px) {
  .py-mobile-menu nav { position: relative; z-index: 1; }
  .py-mobile-menu::before,
  .py-mobile-menu::after {
    content: "";
    position: absolute;
    z-index: 0;
    pointer-events: none;
    background-repeat: no-repeat;
  }
  .py-mobile-menu::before {                 /* flying crane, upper-right */
    top: 7vh; right: 7vw;
    width: 132px; height: 132px;
    background-image: url(assets/motifs/crane2.png);
    background-size: contain;
    background-position: top right;
    opacity: 0.6;
    transform: rotate(-6deg);
  }
  .py-mobile-menu::after {                   /* cloud band, along the bottom */
    left: 0; right: 0; bottom: 0;
    height: 178px;                           /* fits the ~183px below "Contact" */
    /* Trimmed to the motif's real content box, so the file edges ARE the cloud
       edges (no transparent padding skewing placement). Bottom-anchored; the
       height + scale are tuned so the moon (mid-artwork) clears the top edge. */
    background-image: url(assets/motifs/clouds-trim.png);
    background-size: 290px auto;
    background-position: bottom center;
    opacity: 0.55;
  }
}

/* ==========================================================================
   [3] Motifs on mobile (<=767px)
   The decorative illustrations are positioned with desktop coordinates and
   collide with text on phones. Mobile-only: hide the colliding ones, keep a
   couple repositioned + scaled as quiet corner framing. Desktop is untouched
   (these rules only apply <=767px) and the original motif CSS in index.html
   is not modified — this is a pure additive override.
   ========================================================================== */
@media (max-width: 767px) {
  /* Work section (red): a dense card column on mobile — hide all motifs;
     the grain texture already carries the surface. */
  .py-motif { display: none; }

  /* About: a scaled-down "cats playing with butterflies" scene tucked along the
     bottom. Cats ~115px peeking from the corners; butterflies ~1/4 that size
     (matching the desktop cat:butterfly ratio), placed where each cat looks.
     Kitten + third butterfly stay hidden on mobile. */
  .info-motif { display: none; }
  /* Anchor the motif backdrop to the page, not the viewport, on mobile: a fixed
     backdrop makes the bottom cats shift + clip as the iOS address bar shows/hides
     during scroll (the glitch). Absolute = they scroll with content, stable. */
  .info-backdrop { position: absolute; }
  .info-m1, .info-m2 { display: block; width: 115px; opacity: 0.5; }
  .info-m1 { bottom: -1vh; left: -3vw; }                 /* cat 1 (flipped), bottom-left, reaching up-right */
  .info-m2 { bottom: -1vh; right: -3vw; transform: scaleX(-1); }  /* cat 2, flipped to gaze up-left toward its butterfly */
  .info-m4, .info-m5 { display: block; width: 30px; opacity: 0.6; }
  .info-m4 { top: auto; bottom: 14vh; left: 21vw; }      /* cat 1's target — up & right of it */
  .info-m5 { top: auto; bottom: 15vh; right: 19vw; }     /* cat 2's target — up & left of it */

  /* Contact: the lower area is clear, so keep one swallow framing the
     bottom-left; hide the birds that sat over the headline/list. */
  .contact-motif { display: none; }
  .cm-3 { display: block; width: 140px; bottom: 3vh; left: -3vw; opacity: 0.5; }

  /* Play: fixed motifs float over content — keep two small, faint koi tucked
     near the corners; hide the rest. Both are TOP-anchored so the mobile
     address-bar hide/show doesn't shift them, and kept fully on-screen + within
     the always-visible band so they read at rest (no reveal-after-scroll). */
  .py-play-motif { display: none; }
  /* Compound selector to match the desktop rule's specificity (.py-play-motif.pm-N)
     so these overrides actually win. */
  .py-play-motif.pm-1 { display: block; width: 78px; opacity: 0.3; top: 5vh; left: -4vw; right: auto; }
  .py-play-motif.pm-6 { display: block; width: 88px; opacity: 0.3; top: 68vh; bottom: auto; right: 4vw; left: auto; }
}

/* ==========================================================================
   [4] Home hero (mobile) — "best experienced on desktop" note + scroll cue
   Both are mobile-only, absolutely positioned inside .py-hero. The
   `.py-hero > .x` selector outranks the `.py-hero > *` { position: relative }
   rule, so the absolute positioning takes effect. Desktop: display:none.
   ========================================================================== */
.hero-note, .hero-scroll { display: none; }
@media (max-width: 900px) {
  .py-hero > .hero-note {
    display: block;
    position: absolute;
    top: 60px; left: 22px; right: 22px;
    z-index: 2;
    margin: 0;
    text-align: center;
    font-family: Helvetica, "Helvetica Neue", Arial, sans-serif;
    font-size: 10.5px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: rgba(0, 0, 0, 0.5);
  }
  .py-hero > .hero-scroll {
    display: flex;
    justify-content: center;
    position: absolute;
    bottom: 12vh; left: 0; right: 0;   /* up from the 100vh bottom so it clears mobile browser chrome and shows at rest */
    z-index: 2;
    color: #A50208;
    font-size: 22px;
    line-height: 1;
    pointer-events: none;
  }
}
@media (max-width: 900px) and (prefers-reduced-motion: no-preference) {
  .py-hero > .hero-scroll span { display: inline-block; animation: hero-bob 1.7s var(--ease-util) infinite; }
}
@keyframes hero-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(7px); } }

/* ==========================================================================
   [5] Project artifact gallery (mobile) — custom scroll indicator
   Native scrollbars are hidden (iOS hides them anyway; this avoids a double
   bar on Android). A thin track + thumb (positioned by JS in ProjectDetail)
   makes the swipe obvious. The bar lives inside the md:hidden gallery wrapper,
   so it's mobile-only.
   ========================================================================== */
.py-artifact-scroll { scrollbar-width: none; }              /* Firefox */
.py-artifact-scroll::-webkit-scrollbar { display: none; }   /* WebKit/Blink */
.py-artifact-bar {
  position: relative;
  height: 4px;
  margin-top: 12px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.14);
  overflow: hidden;
}
.py-artifact-thumb {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 30%;                 /* JS overrides width + left from scroll position */
  border-radius: 999px;
  background: rgba(165, 2, 8, 0.6);   /* accent red — clearly visible, on-brand */
}

/* ==========================================================================
   [6] Play page tweaks
   - Force the external-link arrow to render as text (not an iOS emoji), so it
     matches the desktop arrow. (markup carries a U+FE0E text-presentation
     selector too; this is the belt-and-suspenders.)
   - Remove the source/arrow overlay that sits on the cards (e.g. "Substack ↗")
     on mobile — the card title already flags external links. Desktop keeps it.
   ========================================================================== */
.p-card .meta .pname-link .link-arrow { font-variant-emoji: text; }
@media (max-width: 767px) {
  .p-card .img::before,
  .p-couple .couple-card .img::before,
  .p-triple .triple-card .img::before { content: none; }
}

/* ==========================================================================
   [7] Editorial entrance — Home hero (desktop + mobile)
   "Focus pull" @ 0.8× (chosen via the hero-entrance explorer): on mount the
   name enters blurred / out of focus and sharpens to crisp as it settles; the
   blurb reveals once the name has landed; then the three accent words light
   from ink to brand red — the payoff. Slow, deliberate (~4s). Entirely off
   under reduced motion: the resting state below IS the site's default, so
   nothing here changes the design at rest.
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) {
  .py-hero-name  { animation: hero-focus 1440ms var(--ease-soft) 100ms both; }
  .py-hero-blurb { animation: hero-rise 1125ms var(--ease-soft) 1440ms both; }
  /* the three .pop words light from ink to accent-red, in sequence, as the
     closing beat (echoing the red name above) */
  .py-hero-blurb .pop { color: #000; animation: hero-ink 800ms var(--ease-soft) both; }
  .py-hero-blurb .pop:nth-of-type(1) { animation-delay: 2690ms; }
  .py-hero-blurb .pop:nth-of-type(2) { animation-delay: 2940ms; }
  .py-hero-blurb .pop:nth-of-type(3) { animation-delay: 3190ms; }
}
/* Blur must scale with the wordmark: ~30px reads as soft-focus on the full
   ~300px desktop name, but that same 30px nearly erases the ~55px mobile name.
   Step it down on small screens via a custom property the keyframe reads. */
.py-hero-name { --hero-blur: 30px; }
@media (max-width: 900px) { .py-hero-name { --hero-blur: 10px; } }
@keyframes hero-focus { from { opacity: 0; filter: blur(var(--hero-blur, 30px)); transform: scale(1.04); } to { opacity: 1; filter: blur(0); transform: none; } }
@keyframes hero-rise  { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
@keyframes hero-ink   { from { color: #000; } to { color: #A50208; } }

/* ==========================================================================
   [8] Contact — "Or just say hello." types out (JS typer in ContactPage.jsx)
   The caret is a thin bar that follows the text and blinks. The component
   only renders it (and only types) when motion is allowed; under reduced
   motion it shows the full line with no caret, so this is purely cosmetic.
   ========================================================================== */
.contact-or .type-caret {
  display: inline-block;
  width: 0.07em;
  height: 1em;
  margin-left: 0.06em;
  background: currentColor;          /* matches the muted line (parent opacity:0.7) */
  vertical-align: -0.12em;
  transform: skewX(-12deg);          /* leans with the italic line */
}
@media (prefers-reduced-motion: no-preference) {
  .contact-or .type-caret { animation: caret-blink 1.05s steps(1, end) infinite; }
}
@keyframes caret-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

/* ==========================================================================
   [9] About — gentle scroll-reveals (IntersectionObserver in InfoPage.jsx)
   The divider, tabs, and expertise panel rise + fade in as they enter view.
   Native scroll-timelines don't work here (.py-info is overflow:hidden, so it
   becomes a non-scrolling scrollport), so InfoPage drives it with an observer.
   It adds .reveals-armed only when JS + IO run and motion is allowed, then
   toggles .is-in per element; without that class the content shows normally
   (so nothing can be left hidden). The panel's own tab-switch fade lives on
   .info-panel (a child of the revealed .info-panels), so they don't collide.
   ========================================================================== */
.py-info.reveals-armed .py-reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 700ms var(--ease-soft), transform 760ms var(--ease-soft);
}
.py-info.reveals-armed .py-reveal.is-in {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .py-info.reveals-armed .py-reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
}

/* ==========================================================================
   [10] Reduced-motion gating for PRE-EXISTING animations
   The motion added in this layer is already gated; these animations predate it
   and didn't respect prefers-reduced-motion. Additive override quiets them.
   - .info-panel : the About tab-switch slide-fade (index.html:~1326).
   - .p-couple / .p-triple : the Play reveal transitions (index.html gated only
     .p-card). The ProjectDetail auto-scroll ticker is RAF-driven, so it's
     gated in JS instead (ProjectDetail.jsx — skips the RAF under reduce).
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .info-panel { animation: none; }
  .p-couple, .p-triple { transition: none; opacity: 1; transform: none; }
}

/* ==========================================================================
   [11] Nav wordmark — give the "p.y." signature more presence (desktop)
   At 22px it sat at nearly the same weight as the 16px Goudy links; the
   wordmark is the signature, so it should dominate. Desktop only (min-width:
   768px) — the mobile nav logo is sized separately in [2] (30px) and is
   deliberately left untouched here so mobile doesn't regress.
   ========================================================================== */
@media (min-width: 768px) {
  .py-nav .py-nav-logo    { font-size: 34px; }   /* overlay nav (content pages) */
  .py-chrome .mark .glyph { font-size: 34px; }   /* chrome nav (project pages)  */
}

/* ==========================================================================
   [12] Page transitions — cross-page crossfade (View Transitions API)
   Hash-route page swaps were an abrupt hard-cut between the blue and red page
   backgrounds. App.jsx wraps true page-to-page route changes in
   document.startViewTransition (feature-detected; browsers without the API and
   reduced-motion users get the original instant swap, unchanged). In-page anchor
   jumps (#hero<->#work, #play<->#play-XX) are deliberately NOT transitioned.
   Here we retime the default root crossfade to the site's calm cadence and add a
   whisper of upward drift on the incoming page — editorial weight, not flourish.
   All of it lives behind no-preference, so reduced motion is untouched.
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(root) { animation-duration: var(--dur-calm); }
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: var(--dur-calm);
    animation-timing-function: var(--ease-soft);
  }
  ::view-transition-old(root) { animation-name: py-page-out; }
  ::view-transition-new(root) { animation-name: py-page-in; }
}
@keyframes py-page-out {
  to { opacity: 0; }
}
@keyframes py-page-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* ==========================================================================
   [13] Active section indicator — persistent nav underline
   The current section's nav link stays underlined so you always know where you
   are. It reuses the existing hover underline's END state (the ::after bar fully
   extended, right:0), so it's visually identical to a held hover — no new style.
   App.jsx sets aria-current="page" on the active link (route-based, plus a scroll
   observer for the Work subsection of the home page). aria-current also tells
   screen readers the current page. Not motion-gated: it's a state indicator, and
   it mirrors the pre-existing (ungated) hover underline.
   On mobile the inline links are hidden; the indicator shows on the matching link
   inside the opened menu.
   ========================================================================== */
.py-nav a[aria-current="page"]::after,
.py-chrome nav a[aria-current="page"]::after { right: 0; }

.py-mobile-menu nav a[aria-current="page"] {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 9px;
}
