:root{
    /* Core ink: warm parchment. ink-muted is intentionally dimmer (lower L). */
    --ink: #EDE6D6;
    --ink-dim: #B8B0A0;
    --ink-faint: rgba(237,230,214,0.45);
    --bg: #141210;
    --bg-raised: #1C1916;
    --bg-surface: rgba(255,255,255,0.04);
    /* Palette — restrained use: moss is the signature colour */
    --moss: #8A9A5B;
    --moss-dim: rgba(138,154,91,0.18);
    --rust: #C1502E;
    --amber: #C8973E;
    --teal: #4C8C86;
    --clay: #B5654A;
    --sage: #9CAF88;
    --line: rgba(237,230,214,0.11);
    --line-strong: rgba(237,230,214,0.20);

    --nav-blur: 20px;
    --nav-sat: 180%;
    --nav-pad-v: 4px;
    --nav-pad-h: 6px;
    --nav-bg-1-a: 0.10;
    --nav-bg-2-a: 0.03;

    /* Single source of truth for the "Aditya" name treatment, shared by
       .hero h1 and .dolly-name (the transition portal text) so the two
       can never silently drift apart again. */
    --name-size: clamp(2.6rem, 7.5vw, 7rem);
    --name-weight: 600;
    --name-tracking: -0.025em;
  }
  /* Section-based accent theming: shifts only --rust (used by section-num,
     eyebrow, selection color, active-nav-pill contrast) as you scroll between
     sections. Deliberately narrow in scope - --bg/--ink stay untouched so
     contrast/readability elsewhere on the page can't be affected. Custom-property
     transitions on non-numeric/color-typed vars aren't reliably animatable across
     browsers without @property, so the shift is intentionally an instant swap
     rather than a tween - still reads as a mood change since it's tied to scroll.*/
  body.theme-projects{ --rust:#C1502E; }
  body.theme-experience{ --rust:#B5654A; }
  *{box-sizing:border-box; margin:0; padding:0;}
  html{}
  body{
    background:var(--bg);
    color:var(--ink);
    font-family:'Work Sans', sans-serif;
    overflow-x:hidden;
  }
  ::selection{ background:var(--rust); color:var(--bg); }

  /* --- Dolly-zoom page transition (index -> gallery) ---
     .dolly-portal is the "world behind the screen" the camera pushes into;
     it sits above everything but the custom cursor. Hidden by default so it
     costs nothing on pages/visits that never trigger the transition. */
  html.dolly-active, html.dolly-active body{ overflow:hidden; cursor:wait; }
  .dolly-portal{
    display:none; position:fixed; inset:0; z-index:9990;
    align-items:center; justify-content:center; pointer-events:none;
    background:
      radial-gradient(ellipse at center, rgba(20,18,15,0) 0%, rgba(11,10,9,0.94) 68%, #0b0a09 100%);
  }
  .dolly-portal-label{
    font-family:'Newsreader', serif; font-weight:500; font-style:italic;
    font-size:clamp(1.6rem,3vw,2.4rem); letter-spacing:0.02em; color:var(--ink);
    opacity:0;
    /* Roughly where .hero h1 actually sits (eyebrow above it is shorter than
       the tagline below), so the portal text doesn't hand off to a hero name
       that visibly jumps up as it takes over. */
    margin-top:-3vh;
  }
  /* "Aditya" half of the portal text - sized/weighted to be pixel-close to
     .hero h1, so it can sit directly under the real hero name and read as
     the same element rather than a placeholder being swapped out. */
  .dolly-name{
    font-family:'Newsreader', serif; font-weight:var(--name-weight); font-style:normal;
    font-size:var(--name-size); line-height:0.94; letter-spacing:var(--name-tracking);
  }
  .dolly-name .letters{ text-shadow:0 0 24px rgba(138,154,91,0.25), 0 0 64px rgba(138,154,91,0.12); }
  /* "Shankar" half - mirrors .hero h1 em/.lang-cycle (italic, moss, ~0.68x). */
  .dolly-surname{
    font-family:'Newsreader', serif; font-weight:500; font-style:italic; color:var(--moss);
    font-size:clamp(1.77rem,5.1vw,4.76rem); text-shadow:0 0 22px rgba(138,154,91,0.35);
  }

  /* --- Aggressive perf mode, scoped ONLY to html.dolly-active ---
     These effects buy nothing once the element is about to be pushed into
     blur/blackness by the dolly tween anyway, but they're some of the most
     expensive paint ops on the page (SVG feDisplacementMap backdrop-filter
     especially, recomputed every frame while the same element is also under
     a 3D transform+blur). Stripping them only during the transition window
     keeps the resting-state visuals untouched. */
  html.dolly-active *,
  html.pt-arriving *,
  html.pt-arriving-home *{
    backdrop-filter:none !important;
    -webkit-backdrop-filter:none !important;
  }
  html.dolly-active .hero h1 .letters,
  html.dolly-active .hero h1 em,
  html.dolly-active .dolly-name .letters,
  html.dolly-active .dolly-surname,
  html.pt-arriving-home .hero h1 .letters,
  html.pt-arriving-home .hero h1 em{
    text-shadow:none !important;
  }

  /* --- Perf mode, round 2: everything below is pure paint/compositing cost
     that the eye can't register anyway once the dolly tween is running (the
     targets are already mid-blur/mid-transform), so it's stripped for the
     full window instead of just the glass elements above. All scoped to the
     same three transition classes, so resting-state visuals never change. */

  /* JS already stops the particle-bg rAF loop (__dollyParticlePause), but the
     canvas element itself stays in the paint/composite tree until it's
     actually removed from rendering - visibility:hidden takes it out
     entirely for the duration instead of just leaving it static. */
  html.dolly-active #particle-bg,
  html.pt-arriving #particle-bg,
  html.pt-arriving-home #particle-bg{
    visibility:hidden !important;
  }

  /* Custom cursor (dot/ring) tracks the mouse every frame via GSAP
     quickSetter - real work, but invisible under cursor:wait + a portal
     covering the screen. */
  html.dolly-active .cursor-dot,
  html.dolly-active .cursor-ring{
    display:none !important;
  }

  /* Any CSS-driven animation/transition not part of the dolly tween itself
     (GSAP drives the tween via inline transform/filter, not @keyframes, so
     this can't touch it) - the field-log ticker, lang-cycle swap, footer
     line, nav pill hover states, magnetic-hover transitions, etc. all stop
     costing a frame the instant the transition starts. */
  html.dolly-active *:not(.dolly-portal):not(.dolly-portal *),
  html.pt-arriving *:not(.dolly-portal):not(.dolly-portal *),
  html.pt-arriving-home *:not(.dolly-portal):not(.dolly-portal *){
    animation-play-state:paused !important;
    transition:none !important;
  }

  /* Hover/magnetic-pull/tilt listeners in JS still fire on mousemove, but
     with the cursor forced to "wait" and a portal over the screen there's
     no real interaction happening - killing pointer-events stops the
     browser's hit-testing/hover recalculation on every element under the
     (motionless) pointer for free. */
  html.dolly-active body *{
    pointer-events:none !important;
  }
  html.dolly-active .dolly-portal{ pointer-events:auto; }

  /* Remaining box-shadow/filter costs on elements that aren't already being
     blurred by the dolly tween (nav pill glow, tag chips, project cards
     off-screen, etc.) - matches the text-shadow strip above. */
  html.dolly-active *,
  html.pt-arriving *,
  html.pt-arriving-home *{
    box-shadow:none !important;
  }
  html.dolly-active .dolly-portal,
  html.dolly-active .dolly-portal *{
    box-shadow:revert !important;
  }

  /* --------------------------------------------------------------
     Minimal utility classes replacing the Tailwind CDN (issue #17).
     Only the utilities actually used by the nav markup in index.html;
     values match Tailwind's defaults so the markup needed no changes.
     -------------------------------------------------------------- */
  .fixed{position:fixed}
  .relative{position:relative}
  .absolute{position:absolute}
  .inset-0{inset:0}
  .top-6{top:24px}
  .bottom-4{bottom:16px}
  .left-0{left:0}
  .right-0{right:0}
  .z-\[25\]{z-index:25}
  .z-10{z-index:10}
  .-z-10{z-index:-10}
  .flex{display:flex}
  .hidden{display:none}
  .items-center{align-items:center}
  .justify-center{justify-content:center}
  .overflow-hidden{overflow:hidden}
  .pointer-events-none{pointer-events:none}
  .gap-1{gap:4px}
  .p-1{padding:4px}
  .px-1{padding-left:4px;padding-right:4px}
  .py-1{padding-top:4px;padding-bottom:4px}
  .px-2{padding-left:8px;padding-right:8px}
  .px-3{padding-left:12px;padding-right:12px}
  .px-4{padding-left:16px;padding-right:16px}
  .py-1\.5{padding-top:6px;padding-bottom:6px}
  .mr-1{margin-right:4px}
  .w-full{width:100%}
  .h-full{height:100%}
  .opacity-0{opacity:0}
  .rounded-\[inherit\]{border-radius:inherit}
  .text-\[13px\]{font-size:13px}
  .font-medium{font-weight:500}
  .text-white{color:#fff}
  .text-white\/70{color:rgba(255,255,255,0.7)}
  .hover\:text-white:hover{color:#fff}
  .transition-colors{transition:color .2s ease}
  .transition-opacity{transition:opacity .26s ease-out}
  .duration-\[260ms\]{transition-duration:260ms}
  .ease-out{transition-timing-function:ease-out}
  @media (min-width:768px){
    .md\:block{display:block}
    .md\:hidden{display:none}
  }
  /* Notch/Dynamic-Island safety at every width, not just mobile (issue #5) */
  [data-app-nav]{ top: max(1.5rem, env(safe-area-inset-top)); }

  /* header */
  header{
    display:flex; justify-content:space-between; align-items:center;
    padding:24px 5vw; font-family:'JetBrains Mono', monospace; font-size:12px;
    color:var(--ink-dim); letter-spacing:0.03em;
    border-bottom: 1px solid var(--line);
  }
  /* Scrollspy pill: a moss-colored slider that slides behind whichever nav link
     ("Projects" / "About Me") is currently in view. Lives inside the real nav
     markup (the <nav data-app-nav> block further down) as an absolutely
     positioned <span class="nav-indicator">, and is driven by moveIndicator()
     in the script at the bottom of the page. */
  .nav-indicator{
    position:absolute; height:32px; border-radius:999px;
    background:var(--moss); opacity:0; z-index:0; top:50%; left:0;
    transform:translateY(-50%);
    pointer-events:none;
  }
  /* Text goes dark-on-moss when a link's pill is active. The link text itself
     lives in a nested <span> with its own hardcoded color class, so the color
     override has to target that span directly (not just the parent <a>). */
  nav[data-app-nav] a.active span{ color:var(--bg) !important; }
  nav[data-app-nav] a, #mobileNav a{ text-decoration:none; }
  .wave{ display:flex; align-items:center; gap:8px; color:var(--moss); }
  .wave span{ font-size:15px; }
  .clock{ text-align:right; line-height:1.3; }
  .clock b{ color:var(--ink); font-size:14px; font-weight:500; }

  /* hero */
  .hero{
    min-height:82vh;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    text-align:center; padding:0 5vw; position:relative;
  }
  .eyebrow{
    font-family:'JetBrains Mono', monospace; font-size:11px; font-weight:500; letter-spacing:.22em;
    color:var(--moss); text-transform:uppercase; margin-bottom:28px;
    opacity:0.85;
  }
  .hero h1{
    font-family:'Newsreader', serif; font-weight:var(--name-weight);
    font-size:var(--name-size);
    line-height:0.94;
    letter-spacing:var(--name-tracking);
    display:flex; flex-wrap:nowrap; align-items:baseline; justify-content:center;
    gap:0.3em; max-width:100%;
  }
  .hero h1 em{ font-style:italic; font-weight:500; color:var(--moss); }
  .letters{ display:inline-flex; flex-wrap:nowrap; flex-shrink:0; letter-spacing:0; transition:letter-spacing .5s ease; }
  .letters:hover{ letter-spacing:0.05em; }
  .letters .letter{ display:inline-block; transition:color .3s ease, transform .3s ease; cursor:pointer; }
  .letter-particle{ position:fixed; width:5px; height:5px; border-radius:50%; pointer-events:none; z-index:9999; transform:translate(-50%,-50%); }
  .letters .letter:hover{ color:var(--hc); transform:translateY(-0.08em) rotate(-3deg); }
  /* Fixed width sized for the widest cycling string (the CJK entries render wider
     per character than the Latin ones). Without this, the box's width tracks
     whatever word is currently showing, and since .hero h1 is a centered flex
     row, that made the whole row re-center on every swap - visibly shifting
     "Aditya" left/right as the surname changed length. A constant-width box
     keeps the row's total width (and "Aditya"'s position) fixed regardless of
     which language is currently displayed. */
  .lang-cycle{
    position:relative; display:inline-block; flex-shrink:0;
    width:5.6em; text-align:left;
    white-space:nowrap; word-break:keep-all; overflow-wrap:normal; overflow:visible;
    font-size:0.68em; vertical-align:baseline; line-height:1.3;
    cursor:pointer;
  }
  .lang-cycle::after{
    display:inline-block; white-space:nowrap; word-break:keep-all;
    content:"Shankar";
    animation:lang-cycle 24s linear infinite;
    animation-delay: var(--lang-delay, 0s);
    font-family:'Noto Sans Devanagari', 'Newsreader', serif;
  }
  /* JS toggles this for one frame to force a reflow, so the animation
     restarts clean at the new --lang-delay instead of blending from
     wherever the 24s loop currently sits. */
  .lang-cycle.lang-reset::after{ animation:none; }
  /* content is a discrete (non-interpolable) property. If it's only declared at the
     boundary keyframes (0/20/40/60%...), the browser is free to flip it at the
     midpoint between those declarations - which lands mid-hold, at full opacity,
     causing a visible snap instead of the intended invisible swap. Re-declaring
     content at every keyframe pins it to the current word until the exact
     opacity:0 keyframe, where the flip happens while invisible. */
  /* Each 20%-wide language block now has FIVE points instead of four:
     enter(0) -> fade-in-end(9) -> hold-end(13) -> fully-invisible dwell(19) -> swap(20).
     That extra dwell keyframe holds opacity at true 0 for a beat, WITH THE OLD WORD
     STILL SET, before content flips at the block boundary - so the swap always
     happens after the text has visibly finished disappearing, not mid-fade. */
  @keyframes lang-cycle{
    0%{ content:"Shankar"; opacity:0; }
    9%{ content:"Shankar"; opacity:1; }
    13%{ content:"Shankar"; opacity:1; }
    19%{ content:"Shankar"; opacity:0; }
    20%{ content:"Шанкар"; opacity:0; }
    29%{ content:"Шанкар"; opacity:1; }
    33%{ content:"Шанкар"; opacity:1; }
    39%{ content:"Шанкар"; opacity:0; }
    40%{ content:"シャンカル"; opacity:0; }
    49%{ content:"シャンカル"; opacity:1; }
    53%{ content:"シャンカル"; opacity:1; }
    59%{ content:"シャンカル"; opacity:0; }
    60%{ content:"샹카르"; opacity:0; }
    69%{ content:"샹카르"; opacity:1; }
    73%{ content:"샹카르"; opacity:1; }
    79%{ content:"샹카르"; opacity:0; }
    80%{ content:"शंकर"; opacity:0; }
    89%{ content:"शंकर"; opacity:1; }
    93%{ content:"शंकर"; opacity:1; }
    99%{ content:"शंकर"; opacity:0; }
    100%{ content:"Shankar"; opacity:0; }
  }
  .tagline{
    margin-top:32px; max-width:500px; color:var(--ink-dim);
    font-size:15px; line-height:1.75; letter-spacing:0.01em;
  }

  /* field log ticker — signature element */
  .fieldlog{
    border-top:1px solid var(--line); border-bottom:1px solid var(--line);
    padding:13px 0; overflow:hidden; white-space:nowrap; position:relative;
    background:linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.015));
    backdrop-filter:blur(12px) saturate(150%);
    -webkit-backdrop-filter:blur(12px) saturate(150%);
    box-shadow:inset 0 1px 0 rgba(255,255,255,0.04), inset 0 -1px 0 rgba(0,0,0,0.15);
    /* Soft fade-out edges so the ticker reads as a continuous stream */
    -webkit-mask-image: linear-gradient(90deg, transparent 0%, black 8%, black 92%, transparent 100%);
    mask-image: linear-gradient(90deg, transparent 0%, black 8%, black 92%, transparent 100%);
  }
  .fieldlog-track{
    display:inline-flex; gap:56px;
  }
  .fieldlog span{
    font-family:'JetBrains Mono', monospace; font-size:11.5px; color:var(--ink-dim);
    letter-spacing:0.02em;
  }
  .fieldlog b{ color:var(--moss); font-weight:500; }


  .scroll-progress{
    position:fixed; top:0; left:0; width:100%; height:3px; z-index:80;
    background:linear-gradient(90deg, var(--rust), var(--amber), var(--moss), var(--teal));
    transform:scaleX(0); transform-origin:left center; pointer-events:none;
  }

  /* nav pill */
  /* sections */
  section{ padding:120px 5vw; max-width:1100px; margin:0 auto; }
  .section-head{ display:flex; align-items:baseline; gap:14px; margin-bottom:52px; }
  /* section-num is metadata, not a design feature — kept small and faint */
  .section-num{
    font-family:'JetBrains Mono', monospace; font-size:10px; letter-spacing:.1em;
    color:var(--ink-faint); font-weight:400; opacity:0.7;
  }
  .section-head h2{
    font-family:'Newsreader', serif; font-size:clamp(1.9rem,3.5vw,2.6rem);
    font-weight:600; letter-spacing:-0.02em; line-height:1;
  }

  .bg-image{
    position:fixed; inset:0; z-index:-2; pointer-events:none;
    background-image:url('media/backgrounds/home.jpg');
    background-size:cover;
    background-position:50% 50%;
    transform: translateZ(0);
    backface-visibility: hidden;
  }
  /* Low-power devices (see LOW_POWER detection in core.js, which sets
     body.low-power) get the small compressed webp instead of the 424KB
     full-res original - everyone else gets full res. */
  body.low-power .bg-image{ background-image:url('media/backgrounds/home-compressed.webp'); }

  .specimen-grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(280px,1fr)); gap:18px; background:transparent; border:none; perspective: 1000px; }
  @media (max-width: 767px) {
    [data-app-nav]{ top: max(1.5rem, env(safe-area-inset-top)); }
  }
  /* Safari can't do the url(#...) SVG-filter distortion in backdrop-filter
     at all - it silently drops the whole declaration, so glass surfaces
     render with zero blur. Swap in a plain blur+saturate there only. */
  html.is-safari [data-app-nav] div[style*="glass-filter"]{
    backdrop-filter: blur(14px) saturate(160%) !important;
    -webkit-backdrop-filter: blur(14px) saturate(160%) !important;
  }
  html.is-safari .specimen{
    backdrop-filter: blur(3px) saturate(180%) !important;
    -webkit-backdrop-filter: blur(3px) saturate(180%) !important;
  }
  html.is-safari .tag-chip{
    backdrop-filter: blur(3px) saturate(160%) !important;
    -webkit-backdrop-filter: blur(3px) saturate(160%) !important;
  }
  html.is-safari .photo-cta{
    backdrop-filter: blur(12px) saturate(180%) !important;
    -webkit-backdrop-filter: blur(12px) saturate(180%) !important;
  }
  html.is-safari .photo-card{
    backdrop-filter: blur(10px) saturate(160%) !important;
    -webkit-backdrop-filter: blur(10px) saturate(160%) !important;
  }
  html.is-safari .photo-subnav-inner{
    backdrop-filter: blur(12px) saturate(180%) !important;
    -webkit-backdrop-filter: blur(12px) saturate(180%) !important;
  }

  /* Low-power tier (body.low-power, set by core.js from deviceMemory/
     hardwareConcurrency/coarse-pointer): the per-pixel SVG feDisplacementMap
     distortion behind these glass surfaces is the single most expensive paint
     op on the page on a weak GPU. Swap it for plain blur+saturate, same as the
     existing Safari fallback - visually very close, costs a fraction as much.
     Capable devices never get this class, so nothing changes for them. */
  body.low-power [data-app-nav] div[style*="glass-filter"]{
    backdrop-filter: blur(14px) saturate(160%) !important;
    -webkit-backdrop-filter: blur(14px) saturate(160%) !important;
  }
  body.low-power .specimen{
    backdrop-filter: blur(3px) saturate(180%) !important;
    -webkit-backdrop-filter: blur(3px) saturate(180%) !important;
  }
  body.low-power .photo-cta,
  body.low-power .photo-subnav-inner{
    backdrop-filter: blur(10px) saturate(170%) !important;
    -webkit-backdrop-filter: blur(10px) saturate(170%) !important;
  }
  body.low-power .photo-card{
    backdrop-filter: blur(8px) saturate(150%) !important;
    -webkit-backdrop-filter: blur(8px) saturate(150%) !important;
  }
  @media (min-width: 768px) {
    .specimen-grid.rail-mode {
      display: flex;
      flex-wrap: nowrap;
      width: max-content;
      gap: 32px;
      padding-right: 12vw;
    }
    .specimen-grid.rail-mode .specimen {
      width: 420px;
      flex-shrink: 0;
    }
  }
  .specimen{
    display:block; text-decoration:none; color:inherit; position:relative;
    --sp-blur: 3px;
    --sp-sat: 180%;
    transform-style: preserve-3d;
    background: rgba(0,0,0,0.22);
    backdrop-filter:url(#card-glass-distortion) blur(var(--sp-blur)) saturate(var(--sp-sat));
    -webkit-backdrop-filter:url(#card-glass-distortion) blur(var(--sp-blur)) saturate(var(--sp-sat));
    border:1px solid rgba(255,255,255,0.13);
    border-radius:20px;
    box-shadow:0 4px 24px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.10);
    padding:36px 32px 32px; transition:.3s cubic-bezier(.2,.8,.2,1); cursor:pointer;
    overflow:hidden;
  }
  .specimen > *{ position:relative; z-index:2; }
  .specimen:hover{
    background:rgba(0,0,0,0.16);
    border-color:rgba(138,154,91,0.5);
    transform:translateY(-5px);
    box-shadow:0 20px 48px rgba(0,0,0,0.4), 0 0 0 1px rgba(138,154,91,0.4), inset 0 1px 0 rgba(255,255,255,0.18);
  }
  .specimen:focus-visible{
    outline: 2px solid var(--moss); outline-offset: 3px;
  }
  .specimen .arrow{
    position:absolute; top:24px; right:24px; opacity:0; transform:translate(-5px,5px);
    transition:.3s ease; color:var(--moss); font-size:16px;
  }
  .specimen:hover .arrow{ opacity:1; transform:translate(0,0); }
  /* tag acts as category metadata — readable but not loud */
  .specimen .tag{
    font-family:'JetBrains Mono', monospace; font-size:10px; font-weight:500;
    color:var(--ink-dim); letter-spacing:.1em; text-transform:uppercase;
    margin-bottom:16px; display:block; opacity:0.75;
  }
  .specimen h3{ font-family:'Newsreader', serif; font-size:1.45rem; font-weight:500; margin-bottom:12px; line-height:1.2; letter-spacing:-0.01em; }
  .specimen p{ color:var(--ink-dim); font-size:13.5px; line-height:1.65; }

  .about-bio{
    font-family:'Newsreader', serif; font-size:1.18rem; line-height:1.72;
    color:var(--ink); max-width:660px; margin-bottom:28px;
    letter-spacing:0.005em;
  }
  .about-subhead{
    display:block; font-family:'JetBrains Mono', monospace; font-size:11px; font-weight:500;
    letter-spacing:.18em; text-transform:uppercase; color:var(--moss); opacity:0.85;
    margin-bottom:12px;
  }
  .about-bio-secondary{ margin-top:36px; margin-bottom:20px; }
  .about-tags{
    display:flex; flex-wrap:wrap; gap:8px; margin-top:0; margin-bottom:44px; clear:both; position:relative; z-index:1;
  }
  .tag-chip{
    font-family:'JetBrains Mono', monospace; font-size:10.5px; letter-spacing:.07em;
    text-transform:uppercase; color:var(--ink-dim);
    border-radius:999px; padding:7px 15px;
    background:rgba(0,0,0,0.3);
    backdrop-filter:url(#card-glass-distortion) blur(3px) saturate(160%);
    -webkit-backdrop-filter:url(#card-glass-distortion) blur(3px) saturate(160%);
    border:1px solid rgba(255,255,255,0.10);
    box-shadow:rgba(255,255,255,0.06) 0px 0px 0px 0.5px inset, rgba(0,0,0,0.12) 0px 2px 8px;
    transition:.2s ease;
  }
  .tag-chip:hover{
    box-shadow:rgba(255,255,255,0.12) 0px 0px 0px 0.5px inset, rgba(0,0,0,0.18) 0px 6px 20px,
               0 0 18px rgba(138,154,91,0.22);
    transform:translateY(-2px); color:var(--ink); border-color:rgba(138,154,91,0.35);
  }
  .tag-chip:focus-visible{ outline:2px solid var(--moss); outline-offset:3px; }
  .photo-cta{
    display:inline-flex; align-items:center; gap:10px;
    text-decoration:none; color:var(--ink-dim); font-size:12.5px; font-weight:500;
    letter-spacing:0.03em;
    padding:9px 18px 9px 16px; border-radius:999px;
    background:rgba(0,0,0,0.25);
    backdrop-filter:url(#glass-filter-_r_12b_) saturate(1);
    -webkit-backdrop-filter:url(#glass-filter-_r_12b_) saturate(1);
    border:1px solid rgba(255,255,255,0.10);
    box-shadow:rgba(255,255,255,0.08) 0px 0px 0px 0.5px inset, rgba(0,0,0,0.12) 0px 4px 16px;
    transition:.22s ease;
  }
  .photo-cta:hover{
    box-shadow:rgba(255,255,255,0.13) 0px 0px 0px 0.5px inset, rgba(0,0,0,0.18) 0px 6px 20px,
               0 0 22px rgba(138,154,91,0.25);
    transform:translateY(-2px); color:var(--ink); border-color:rgba(138,154,91,0.3);
  }
  .photo-cta:focus-visible{ outline:2px solid var(--moss); outline-offset:3px; }
  .photo-cta-arrow{ transition:.22s ease; }
  .photo-cta:hover .photo-cta-arrow{ transform:translateX(4px); }
  .photo-grid{
    display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
    gap:12px; margin-top:16px;
  }
  .photo-card{
    aspect-ratio:4/5; border-radius:10px; overflow:hidden; position:relative;
    background:rgba(0,0,0,0.2); border:1px solid rgba(255,255,255,0.1);
    backdrop-filter:url(#glass-filter-photopanel) saturate(1);
    -webkit-backdrop-filter:url(#glass-filter-photopanel) saturate(1);
    transition:transform .3s ease, opacity .3s ease;
  }
  .photo-card:hover{ transform:translateY(-3px); }
  .photo-card img{ width:100%; height:100%; object-fit:cover; display:block; opacity:.85; transition:opacity .3s ease; }
  .photo-card:hover img{ opacity:1; }
  .photo-card .photo-tag{
    position:absolute; left:8px; bottom:8px; font-family:'JetBrains Mono', monospace;
    font-size:9.5px; letter-spacing:.07em; text-transform:uppercase; color:var(--ink);
    background:rgba(0,0,0,0.45); padding:3px 8px; border-radius:999px;
    backdrop-filter:blur(4px);
  }
  .photo-card.hidden-cat{ display:none; }
  .photo-card{ cursor:zoom-in; }

  .photo-lightbox{
    position:fixed; inset:0; z-index:60; display:flex; align-items:center; justify-content:center;
    padding:5vh 5vw; background:rgba(10,9,7,0.88);
    opacity:0; pointer-events:none; transition:opacity .3s ease;
  }
  .photo-lightbox.visible{ opacity:1; pointer-events:auto; }
  .photo-lightbox img, .photo-lightbox video{
    max-width:100%; max-height:100%; border-radius:8px;
    box-shadow:0 20px 60px rgba(0,0,0,0.5);
  }
  .photo-lightbox-close{
    position:absolute; top:24px; right:28px; width:38px; height:38px; border-radius:50%;
    display:flex; align-items:center; justify-content:center; cursor:pointer;
    background:rgba(0,0,0,0.35); border:1px solid rgba(255,255,255,0.12); color:var(--ink-dim);
    font-size:18px; line-height:1; transition:.2s ease;
  }
  .photo-lightbox-close:hover{ background:rgba(0,0,0,0.55); color:var(--ink); border-color:rgba(255,255,255,0.25); }
  .photo-lightbox-close:focus-visible{ outline:2px solid var(--moss); outline-offset:3px; }

  /* Photo category pill + buttons: styling reused by the standalone gallery
     page's top bar (.gallery-nav wraps these there instead of .photo-subnav). */
  .photo-subnav-inner{
    display:flex; align-items:center; gap:2px; padding:6px; border-radius:18px;
    background:rgba(0,0,0,0.2);
    backdrop-filter:url(#glass-filter-_r_12b_) saturate(1);
    -webkit-backdrop-filter:url(#glass-filter-_r_12b_) saturate(1);
    box-shadow:rgba(255,255,255,0.1) 0px 0px 0px 0.5px inset, rgba(0,0,0,0.1) 0px 4px 16px;
  }
  .photo-subnav-cat{
    font-family:'JetBrains Mono', monospace; font-size:10.5px; letter-spacing:.06em;
    text-transform:uppercase; color:var(--ink-dim); background:none; border:none;
    padding:7px 14px; border-radius:14px; cursor:pointer; transition:.2s ease;
  }
  .photo-subnav-cat:hover{ color:var(--ink); }
  .photo-subnav-cat.active{ color:var(--ink); background:rgba(255,255,255,0.1); }
  .photo-subnav-cat:focus-visible{ outline:2px solid var(--moss); outline-offset:2px; }
  .site-footer{
    display:flex; align-items:center; gap:16px;
    padding:48px 5vw 22vh; max-width:1100px; margin:0 auto;
    font-family:'JetBrains Mono', monospace; font-size:11.5px; color:var(--ink-dim);
    letter-spacing:0.04em;
  }
  .footer-mark{ color:var(--moss); letter-spacing:.06em; font-weight:500; }
  .footer-line{ flex:1; height:1px; background:var(--line); transform-origin:left center; }
  .footer-year{ opacity:.5; }

  /* global focus style for keyboard navigation */
  :focus-visible{
    outline: 2px solid var(--moss);
    outline-offset: 3px;
    border-radius: 4px;
  }
  /* custom cursor */
  body.has-custom-cursor { cursor: none; }
  body.has-custom-cursor a, body.has-custom-cursor .specimen, body.has-custom-cursor .letters .letter { cursor: none; }
  .cursor-dot, .cursor-ring {
    position: fixed; top: 0; left: 0; pointer-events: none; z-index: 9999;
    border-radius: 50%;
    opacity: 0; transition: opacity 0.3s;
  }
  body.has-custom-cursor .cursor-dot, body.has-custom-cursor .cursor-ring { opacity: 1; }
  .cursor-dot { width: 5px; height: 5px; background: #fff; mix-blend-mode: difference; }
  .cursor-ring {
    width: 22px; height: 22px; border: 1.5px solid rgba(255,255,255,0.85);
    mix-blend-mode: difference;
    box-shadow: 0 0 0 0 rgba(255,255,255,0.2);
    transition: width 0.3s, height 0.3s, border-color 0.3s, background-color 0.3s, box-shadow 0.3s;
  }
  .cursor-ripple {
    position: fixed; top: 0; left: 0; pointer-events: none; z-index: 9998;
    width: 10px; height: 10px; border-radius: 50%;
    border: 1.5px solid var(--moss); transform: translate(-50%, -50%) scale(0);
    opacity: 0;
  }

  /* ============================================================
     Visual enhancement pass (branch: visual-enhancements).
     Additive polish only - background layers (particle canvas,
     .bg-image) are deliberately untouched.
     ============================================================ */

  /* -- Hero name: layered moss glow, per-letter colored glow on hover -- */
  .hero h1 .letters{
    text-shadow: 0 0 24px rgba(138,154,91,0.25), 0 0 64px rgba(138,154,91,0.12);
  }
  .hero h1 em{ text-shadow: 0 0 22px rgba(138,154,91,0.35); }
  .letters .letter:hover{
    text-shadow: 0 0 16px var(--hc), 0 0 42px var(--hc);
  }

  /* -- Specimen cards: rotating conic-gradient border on hover.
        @property lets the angle interpolate; browsers without it just
        show the gradient static, which still reads fine. -- */
  @property --spec-angle{
    syntax: '<angle>'; initial-value: 0deg; inherits: false;
  }
  @keyframes spec-border-spin{ to{ --spec-angle: 360deg; } }
  .specimen::before{
    content:""; position:absolute; inset:0; border-radius:inherit;
    padding:1.5px; pointer-events:none; z-index:1;
    background: conic-gradient(from var(--spec-angle),
      transparent 0%, rgba(138,154,91,0.55) 25%, rgba(200,151,62,0.5) 45%,
      rgba(76,140,134,0.45) 65%, transparent 85%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
    opacity:0; transition:opacity .35s ease;
  }
  .specimen:hover::before{
    opacity:1; animation: spec-border-spin 3.2s linear infinite;
  }

  /* -- Specimen cards: cursor-follow spotlight (uses the --x/--y vars
        interactions.js already writes on mousemove) -- */
  .specimen::after{
    content:""; position:absolute; inset:0; border-radius:inherit;
    pointer-events:none; z-index:1;
    background: radial-gradient(320px circle at var(--x,50%) var(--y,50%),
      rgba(138,154,91,0.14), rgba(255,255,255,0.05) 38%, transparent 68%);
    opacity:0; transition:opacity .3s ease;
  }
  .specimen:hover::after{ opacity:1; }

  /* -- Nav pill: ambient moss glow behind the glass (needs !important to
        beat the inline box-shadow on the pill div) -- */
  [data-app-nav] div[style*="glass-filter"]{
    box-shadow: rgba(255,255,255,0.1) 0px 0px 0px 0.5px inset,
                rgba(0,0,0,0.1) 0px 4px 16px,
                rgba(0,0,0,0.08) 0px 8px 24px,
                0 0 30px rgba(138,154,91,0.16) !important;
  }
  .nav-indicator{
    box-shadow: 0 0 16px rgba(138,154,91,0.55), 0 0 4px rgba(138,154,91,0.35);
  }

  /* -- Field log: per-entry accent colors + soft glow (track is duplicated
        by JS for looping, so nth-child keeps cycling seamlessly) -- */
  .fieldlog-track span:nth-child(4n+1) b{ color:var(--moss);  text-shadow:0 0 12px rgba(138,154,91,0.4); }
  .fieldlog-track span:nth-child(4n+2) b{ color:var(--amber); text-shadow:0 0 12px rgba(200,151,62,0.4); }
  .fieldlog-track span:nth-child(4n+3) b{ color:var(--teal);  text-shadow:0 0 12px rgba(76,140,134,0.45); }
  .fieldlog-track span:nth-child(4n)   b{ color:var(--clay);  text-shadow:0 0 12px rgba(181,101,74,0.4); }

  /* -- Section headings: gradient underline draws in with the reveal
        (core.js adds .head-inview in the existing onEnter) -- */
  .section-head h2{ position:relative; padding-bottom:8px; }
  .section-head h2::after{
    content:""; position:absolute; left:0; bottom:0; height:2px; width:100%;
    border-radius:2px;
    background:linear-gradient(90deg, var(--rust), var(--amber), var(--moss), transparent);
    transform:scaleX(0); transform-origin:left center;
    transition:transform .9s cubic-bezier(.2,.8,.2,1) .2s;
  }
  .section-head.head-inview h2::after{ transform:scaleX(1); }

  /* -- Tag chips + photo CTA hover glow merged into their base :hover
        rules above, rather than living here as a second, easy-to-miss
        declaration for the same selector. -- */

  /* -- Photo grid: simple hover lift only -- */
  .photo-card{
    transition: transform .3s ease, box-shadow .3s ease;
  }
  @media (hover:hover) and (pointer:fine){
    .photo-card:hover{
      transform: translateY(-4px) scale(1.02); z-index:2;
      box-shadow: 0 14px 34px rgba(0,0,0,0.45), 0 0 0 1px rgba(138,154,91,0.35);
    }
  }

  /* -- Lightbox: media eases up into place instead of popping -- */
  .photo-lightbox img, .photo-lightbox video{
    transform: scale(.92) translateY(12px);
    transition: transform .35s cubic-bezier(.2,.8,.2,1);
  }
  .photo-lightbox.visible img, .photo-lightbox.visible video{
    transform: scale(1) translateY(0);
  }

  /* -- Footer mark: tiny signature glow -- */
  .footer-mark{ text-shadow:0 0 12px rgba(138,154,91,0.4); }