/* ===== FOREST THEME =====
   The page stays light -- off-white paper, not a green wash. Forest green is
   the accent and the botany is decoration growing into the margins: grass along
   the bottom edge, vines down the sides, a far treeline. That keeps the reading
   surface calm and puts the colour where it signals rather than where it has to
   be read through. */
:root{
  --paper:#F7F4EC;  --paper-2:#F0EBE0;  --surface:#FFFDF8;  --surface-2:#FAF7F0;
  --rule:#DFD9CB;   --rule-2:#CFC7B5;
  --ink:#1E2B24;    --ink-deep:#101A15;  --ink-mute:#5E6A61;
  /* forest, from deep to light, taken off the supplied swatch */
  --forest:#164A42;  --forest-2:#1E6354;  --forest-3:#2F7A5C;
  --moss:#4C9A6E;    --sage:#8FB89A;      --bark:#7A5B33;
  --gold:#C98A3C;
  --accent:#164A42;  --accent-wash:rgba(22,74,66,.07);
  --bg-0:#F7F4EC; --bg-1:#FBF8F2; --bg-2:#F0EBE0; --bg-3:#E8E2D4;
  --line:rgba(30,43,36,.10); --line-strong:rgba(30,43,36,.22);
  --text:#1E2B24; --text-dim:#55615A; --text-faint:#808C84;
  --emerald:#164A42; --emerald-bright:#164A42; --emerald-soft:#2F7A5C;
  --emerald-deep:#0E342E; --emerald-glow:rgba(22,74,66,.12);
  --ink-figure:#101A15;
}

/* ── the scene ───────────────────────────────────────────────────────────
   Fixed behind everything, pointer-events off, so it never interferes with
   the page it sits under. */
/* Foreground, not background. Behind the content the grass loses every
   stacking fight with the first opaque card that parks on it; in front it reads
   as foliage the page sits in. It takes no pointer events, so nothing under it
   becomes unclickable, and the nav and modals still sit above. */
#forestCanvas{position:fixed;inset:0;z-index:5;pointer-events:none}
/* The grass lives at the bottom of the viewport, so the page needs a margin it
   never writes into -- otherwise an opaque card simply parks on top of it. */
.page,.blog-main,.post-main{padding-bottom:168px!important}
@media(max-width:720px){.page,.blog-main,.post-main{padding-bottom:116px!important}}
.forest-layer{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
.forest-layer svg{position:absolute;display:block}

/* a far treeline along the bottom, hazy so it sits behind the grass */
.treeline{bottom:0;left:50%;transform:translateX(-50%);width:max(1500px,104vw);
  opacity:.17;color:var(--forest)}
/* vines down both margins, where no content lives */
.vine{top:-30px;width:132px;color:var(--forest-3);opacity:.5;
  transform-origin:top center;animation:vine-sway 11s ease-in-out infinite}
.vine-l{left:-16px}
.vine-r{right:-16px;transform:scaleX(-1);animation-duration:14s;animation-delay:-3s}
@keyframes vine-sway{0%,100%{transform:rotate(-1.1deg)}50%{transform:rotate(1.4deg)}}
.vine-r{animation-name:vine-sway-r}
@keyframes vine-sway-r{0%,100%{transform:scaleX(-1) rotate(-1.3deg)}
  50%{transform:scaleX(-1) rotate(1.1deg)}}
/* warm light pooling at the top, like sun through a canopy */
.canopy-light{position:fixed;inset:0;z-index:0;pointer-events:none;
  background:radial-gradient(ellipse 70% 48% at 18% -6%,rgba(201,138,60,.13),transparent 62%),
    radial-gradient(ellipse 60% 44% at 88% 2%,rgba(47,122,92,.11),transparent 60%)}
@media(max-width:860px){.vine{width:88px;opacity:.36}.treeline{opacity:.13}}
@media(prefers-reduced-motion:reduce){.vine{animation:none}}

/* The paper moves to <html>. While it lives on <body> it propagates to the
   root and is painted as the viewport backdrop, which buries a fixed canvas
   sitting at z-index 0 inside <body>. On <html> the canvas paints over it. */
html{background:var(--paper)}
body{background:transparent!important}
/* No blanket z-index on body's children. That was written while the grass still
   sat behind the page, and it flattened the fixed header from 1000 to 1 so the
   content scrolled straight over the nav and took its clicks with it. The grass
   is in the foreground now and takes no pointer events, so nothing needs
   lifting -- only the few things that must stay above it are named. */
.site-header,#navbar{z-index:1000!important}
.scroll-top{z-index:900!important}
.video-modal,.lb{z-index:2000!important}
.ambient,.mesh-grid,.orb{display:none!important}
.grain{opacity:.025!important}

#navbar,.site-header{background:rgba(247,244,236,.82)!important;
  backdrop-filter:blur(14px) saturate(1.2);-webkit-backdrop-filter:blur(14px) saturate(1.2);
  border-bottom:1px solid var(--rule)!important}
.nav-links a{color:var(--ink-mute)!important}
.nav-links a:hover,.nav-links a.active{color:var(--forest)!important}

.project-card,.experience-card,.blog-card,.game-box,.about-card,.resume-viewer{
  background:linear-gradient(176deg,var(--surface) 0%,var(--surface-2) 100%)!important;
  border:1px solid var(--rule)!important;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9),0 2px 3px rgba(30,43,36,.04),
    0 20px 38px -26px rgba(22,74,66,.30)!important}
.project-card:hover,.experience-card:hover,.blog-card:hover{
  border-color:var(--forest-3)!important;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9),
    0 30px 54px -26px rgba(22,74,66,.40)!important}

/* award winners: forest fill, gold badge -- the one place colour goes solid */
.project-card.winner-card,.project-card.revive-winner-card{
  background:linear-gradient(166deg,#1B5648 0%,#103A32 58%,#16463C 100%)!important;
  border-color:rgba(143,184,154,.34)!important;
  box-shadow:0 0 0 1px rgba(143,184,154,.12),
    0 30px 58px -28px rgba(10,34,28,.75)!important}
.project-card.winner-card:hover,.project-card.revive-winner-card:hover{
  border-color:rgba(201,138,60,.62)!important;
  box-shadow:0 0 0 1px rgba(201,138,60,.22),
    0 40px 70px -28px rgba(10,34,28,.85)!important}
.winner-card .project-title,.revive-winner-card .project-title{color:#F3F1E6!important}
.winner-card .project-tech,.revive-winner-card .project-tech{color:#E3B978!important}
.winner-card .project-description,.revive-winner-card .project-description{
  color:rgba(238,240,231,.80)!important}
.winner-card .project-description strong,
.revive-winner-card .project-description strong{color:#FBE8C4!important}
.winner-card .project-link,.revive-winner-card .project-link{
  background:rgba(255,255,255,.07)!important;color:#EFEFE4!important;
  border:1px solid rgba(240,240,228,.24)!important}
.winner-card .project-link:hover,.revive-winner-card .project-link:hover{
  background:rgba(201,138,60,.24)!important;border-color:var(--gold)!important;
  color:#FFF0D6!important}
.winner-card .shot-hint,.revive-winner-card .shot-hint{color:rgba(238,240,231,.55)!important}
.winner-badge,.revive-winner-badge{
  background:linear-gradient(180deg,#E7B96E,#C98A3C)!important;color:#2A1B06!important;
  border:0!important;font-weight:700!important;letter-spacing:.085em!important;
  box-shadow:0 3px 12px -3px rgba(201,138,60,.7)!important}
.winner-card .project-badge.featured,.revive-winner-card .project-badge.featured{
  background:rgba(255,255,255,.08)!important;color:#DDE3D8!important;
  border-color:rgba(240,240,228,.22)!important}

.project-title,.section-title,.experience-title,.about-title{color:var(--ink-deep)!important}
.section-title .em,.about-title .em,.name-italic{color:var(--forest)!important}
.project-tech{color:var(--forest-3)!important}
.project-description,.about-text,.experience-description{color:var(--text-dim)!important}
.section-eyebrow,.about-label{color:var(--forest-3)!important}
.skill-cat .n{color:var(--forest)!important}
.skill-list li:hover{color:var(--forest)!important}
.skill-list li::before{background:var(--forest)!important}
.about-card-photo{box-shadow:0 0 0 1px rgba(30,43,36,.12),0 0 0 7px var(--surface),
  0 0 0 8px var(--forest-3),0 16px 30px -16px rgba(22,74,66,.42)!important}

.cta-button,.filter-pill.active,.resume-button:not(.secondary){
  background:var(--forest)!important;color:#F4F2E8!important;border-color:var(--forest)!important}
.cta-button:hover{background:var(--forest-2)!important}
.cta-button.secondary{background:transparent!important;color:var(--forest)!important;
  border:1px solid var(--forest)!important}
.project-link,.contact-link,.filter-pill{border-color:var(--rule-2)!important;
  color:var(--ink)!important}
.project-link:hover,.contact-link:hover,.filter-pill:hover{
  border-color:var(--forest)!important;color:var(--forest)!important}
.stat-number{color:var(--forest)!important}

/* the court is a lit gym and stays one -- the one room that is not the forest */
.game-canvas-wrap{background:#191512!important}

/* word-level heading reveal: inline-block so each word can be transformed,
   with the spaces between them left as real text nodes so the line still wraps
   and still selects and reads as one string */
.fw{display:inline-block;will-change:transform,opacity}
.fw.pre{opacity:0}
@media(prefers-reduced-motion:reduce){.fw{opacity:1!important;transform:none!important}}


/* ── Poster play button ──────────────────────────────────────────────────
   Centred, it sat on top of the one thing the poster exists to show. It lives
   in the corner now as a disc, and only widens into its label on hover, so the
   title card is legible at rest and the affordance is still obvious. */
.card-play{
  left:auto!important; top:auto!important;
  right:.95rem!important; bottom:.95rem!important;
  transform:none!important;
  padding:.72rem!important; gap:0!important;
  overflow:hidden; max-width:44px;
  transition:max-width .42s cubic-bezier(.2,.9,.25,1),
             padding .42s cubic-bezier(.2,.9,.25,1),
             box-shadow .3s ease!important;
  box-shadow:0 8px 22px -8px rgba(0,0,0,.85),0 0 0 1px rgba(255,255,255,.22)!important;
}
.card-play .lbl{
  font-style:normal; white-space:nowrap; opacity:0; margin-left:0;
  transition:opacity .3s ease .06s, margin-left .42s cubic-bezier(.2,.9,.25,1);
}
.card-media:hover .card-play,
.card-media:focus-visible .card-play{
  max-width:230px; padding:.72rem 1.15rem .72rem .95rem!important;
}
.card-media:hover .card-play .lbl,
.card-media:focus-visible .card-play .lbl{ opacity:1; margin-left:.55rem; }
@media (prefers-reduced-motion: reduce){
  .card-play,.card-play .lbl{transition:none!important}
}
@media (max-width:620px){
  /* no hover on touch, so the label just stays out */
  .card-play{max-width:44px!important;padding:.68rem!important}
  .card-play .lbl{display:none}
}


/* the stack sits in italics beside the company, as \emph{} sets it on the resume */
.experience-company em{font-style:italic;color:var(--forest-3)!important;font-weight:400}


/* ── Monkey on the branch ────────────────────────────────────────────────
   Drawn here rather than dropped in: the reference was Curious George, who is
   Houghton Mifflin Harcourt's, and this is a public portfolio. He sits at the
   opposite end to Haren and sways very slightly, so the branch feels occupied
   rather than decorated. */
.ledge-monkey{
  position:absolute; left:58px; bottom:10px;
  height:168px; width:auto; z-index:2;
  pointer-events:none; user-select:none;
  transform-origin:52% 86%;
  animation:monkey-sway 6.5s ease-in-out infinite;
}
@keyframes monkey-sway{
  0%,100%{transform:rotate(-1.6deg) translateY(0)}
  50%{transform:rotate(1.6deg) translateY(-3px)}
}
@media (max-width:900px){ .ledge-monkey{height:118px; left:26px} }
@media (max-width:620px){ .ledge-monkey{height:92px; left:12px} }
@media (prefers-reduced-motion:reduce){ .ledge-monkey{animation:none} }


/* ── Dark-card legibility ────────────────────────────────────────────────
   Two separate problems on the award cards, both real. Contrast was marginal
   against the lightest band of the gradient -- body 5.4:1 and the tech line
   4.6:1, which clear AA and miss AAA -- and the body was set in Newsreader,
   whose thin serifs optically thin further on a dark ground. The copy moves to
   the sans at a slightly larger size, and every value is lifted to clear 7:1
   against the worst band, not the average one. */
.winner-card .project-description,
.revive-winner-card .project-description{
  font-family:var(--sans)!important;
  font-size:1rem!important;
  line-height:1.72!important;
  letter-spacing:.002em;
  color:rgba(240,244,236,.93)!important;   /* 6.9:1 worst case, 10.0 typical */
}
.winner-card .project-description strong,
.revive-winner-card .project-description strong{
  color:#FFF3DC!important; font-weight:600!important;   /* 7.7:1 worst case */
}
.winner-card .project-tech,
.revive-winner-card .project-tech{
  color:#F0C98A!important;                 /* 5.4:1, up from 4.6 */
}
.winner-card .shot-hint,
.revive-winner-card .shot-hint{ color:rgba(240,244,236,.62)!important }


/* ── Longform: the scene sits at the foot of the article ─────────────────
   Fixed to the viewport the grass crossed the prose on every scroll step. On
   the blog it becomes a band at the end of the document, and the vines and
   treeline drop back so a column of text has nothing moving behind it. */
html.forest-longform body{position:relative}
html.forest-longform #forestCanvas{
  position:absolute!important; inset:auto 0 0 0!important;
  height:300px!important; z-index:0!important;
}
/* the treeline spans the full width and sat behind the text column; the
   grass band at the foot already anchors the page */
html.forest-longform .treeline{display:none}
html.forest-longform .vine{opacity:.30}
html.forest-longform .canopy-light{opacity:.6}
@media(max-width:720px){
  html.forest-longform #forestCanvas{height:200px!important}
}

/* ── Hover physics ───────────────────────────────────────────────────────
   Cards tilt toward the cursor and carry a light that follows it; buttons
   lean as the pointer approaches. The transforms are driven from
   interact.js -- these rules only supply the surfaces and the easing.

   The light lives on an injected .lit-layer rather than a pseudo-element,
   because ::before and ::after are already carrying borders and glows on
   project-card, experience-card and stat-item. */
.project-card,.experience-card,.blog-card,.post-card,
.about-card,.stat-item,.skill-row{
  position:relative;
  transform-style:preserve-3d;
}
.lit-layer{
  position:absolute; inset:0; z-index:0;
  border-radius:inherit; pointer-events:none;
  opacity:0; transition:opacity .38s ease;
  background:radial-gradient(240px circle at var(--mx,50%) var(--my,50%),
             rgba(47,122,92,.15), rgba(47,122,92,.05) 42%, rgba(47,122,92,0) 68%);
}
.is-lit .lit-layer{ opacity:1; }

/* the card's own content has to sit above the light */
.project-card>*:not(.lit-layer),.experience-card>*:not(.lit-layer),
.blog-card>*:not(.lit-layer),.post-card>*:not(.lit-layer),
.about-card>*:not(.lit-layer),.stat-item>*:not(.lit-layer),
.skill-row>*:not(.lit-layer){
  position:relative; z-index:1;
}

/* A lifted card should read as lifted, so the shadow grows with the tilt.
   transform is left out of the transition on purpose -- interact.js eases it
   on a rAF, and a CSS transition on the same property would fight that and
   add lag on every pointermove. */
.project-card,.experience-card,.blog-card,.post-card,.about-card,.stat-item{
  transition:box-shadow .34s ease, border-color .34s ease;
}
.is-lit{
  box-shadow:0 22px 48px -26px rgba(22,74,66,.42),
             0 2px 10px -6px rgba(22,74,66,.18) !important;
  border-color:rgba(47,122,92,.34) !important;
}

/* Magnets get no transition either, for the same reason. will-change is set
   and cleared by the script so idle buttons are not each holding a layer. */
.cta-button,.resume-button,.project-link,.contact-link,
.filter-pill,.repo-link,.read-more{
  transition:background .3s ease, color .3s ease, border-color .3s ease,
             box-shadow .3s ease;
}

@media (hover:none){
  /* touch has no cursor to follow, and a tilt that only fires on tap is noise */
  .lit-layer{ display:none; }
}
@media (prefers-reduced-motion:reduce){
  .lit-layer{ display:none; }
}

/* ── Winning the transform back ──────────────────────────────────────────
   The sheet contains, among several passes of hover styling:
       .project-card:hover { transform: none !important; }
       .blog-card:hover    { transform: none !important; }
   !important beats an inline declaration, so interact.js was computing a
   tilt every frame and having it thrown away on precisely the cards worth
   tilting. These rules are also !important and later in the cascade, and
   they read the value the script writes, so the two stop fighting.

   --tilt falls back to none, which is exactly the resting state. */
.project-card:hover,.blog-card:hover,.post-card:hover,
.experience-card:hover,.about-card:hover,.stat-item:hover,
.skill-row:hover{
  transform:var(--tilt,none) !important;
}
/* The lift those :hover rules used to give is folded into --tilt itself, so
   dropping them loses nothing. Transition stays off transform: the script
   eases it on a rAF and a CSS transition on the same property adds lag. */
.project-card,.blog-card,.post-card,.experience-card,
.about-card,.stat-item,.skill-row{
  transform:var(--tilt,none);
}

.cta-button,.resume-button,.project-link,.contact-link,
.filter-pill,.repo-link,.read-more{
  transform:var(--mag,none);
}
.cta-button:hover,.resume-button:hover,.project-link:hover,
.contact-link:hover,.filter-pill:hover,.repo-link:hover,.read-more:hover{
  transform:var(--mag,none) !important;
}

/* The base sheet transitions transform at 0.42s on cards. interact.js already
   eases transform on a rAF, so a CSS transition on the same property runs a
   second, slower interpolation on top of the first -- the tilt arrives late
   and keeps drifting after the cursor stops. Everything else still animates;
   transform is simply handed to the script. */
.project-card,.blog-card,.post-card,.experience-card,
.about-card,.stat-item,.skill-row{
  transition-property:box-shadow,border-color,background,color !important;
}
