  :root{
    --black:#141210; --white:#faf9f6;
    --warm-gray:#c9c2b4; --warm-gray-dim:#8d867a;
    --accent:#0039a6; --tint:#0c2c5c;
    --dur-med:.8s; --ease:cubic-bezier(.22,.8,.32,1);
    /* Guitar-pick silhouette (rounded triangle, point down) used to mask the Music,
       Navigation, and Return-to-top floating buttons — see "guitar-pick FABs" below.
       mask-size:100% 100% stretches this to whatever box each button already has at
       each breakpoint, so one shape covers every size without duplicating per media query. */
    /* Real guitar-pick outline (traced from the client-supplied reference SVG), reused as a
       CSS mask below rather than a plain hand-drawn teardrop. */
    --pick-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 239 266'%3E%3Cpath d='M144.08,254.18c-14.32,13.57-35.17,15.32-48.83-.13C66,220.93,17.63,148.01,10.26,105.77,3.92,69.48,21,32.52,54.03,16.04c19.96-9.96,41.59-11.82,63.64-12.03,41.37-.4,81.2,7.42,101.78,45.53,20.61,38.15,8.02,73.39-11.54,109.61-18.31,33.9-38.96,65.29-63.83,95.01Z'/%3E%3C/svg%3E");
  }

  /* ---- guitar-pick FABs (Music / Navigation / Return-to-top) ---- */
  /* Same mask on all three so they read as a matching set; each keeps its own
     background color from the rules below it, so the "pick" shows up in a
     different color per button rather than one flat recolor. Border can't be
     drawn with a normal `border` on a masked box (it gets clipped unevenly), so
     the ring you still see on #tpToggle/#backToTop is a second, slightly inset
     masked layer on ::before instead — see those rules. Depth comes from
     filter:drop-shadow, which follows the actual pick outline; a plain
     box-shadow would still draw as a square/circle behind the cutout. */
  #fab,#tpToggle,#backToTop{
    border-radius:0;
    -webkit-mask-image:var(--pick-svg); mask-image:var(--pick-svg);
    -webkit-mask-size:100% 100%; mask-size:100% 100%;
    -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
    -webkit-mask-position:center; mask-position:center;
  }
  *{box-sizing:border-box;margin:0;padding:0;}
  html,body{height:100%;}
  body{background:var(--black);color:var(--white);font-family:'Inter',sans-serif;-webkit-font-smoothing:antialiased;}
  h1,h2,.display{font-family:'Bebas Neue',sans-serif;font-weight:400;letter-spacing:.02em;line-height:.92;}
  a{color:inherit;text-decoration:none;}
  button{font-family:inherit;}

  /* per-vibe headline fonts — swaps section h1/h2 to a vibe-specific display face. Driven
     by --vibe-font-* custom properties (set in JS from each vibe's admin-editable font
     config) rather than one hardcoded rule per vibe key, so the admin panel can change or
     add typography without touching this stylesheet. */
  body[data-vibe]:not([data-vibe="basic"]) .panel h1,
  body[data-vibe]:not([data-vibe="basic"]) .panel h2{
    font-family:var(--vibe-font, 'Bebas Neue', sans-serif);
    font-style:var(--vibe-font-style, normal);
    font-weight:var(--vibe-font-weight, 400);
    letter-spacing:var(--vibe-letter-spacing, .02em);
    line-height:var(--vibe-line-height, .92);
  }

  /* simple frame hugging the browser edge, colored with the active vibe's accent —
     fixed + pointer-events:none so it never blocks clicks or gets scrolled away */
  body::after{
    content:"";
    position:fixed;inset:0;
    border:2px solid var(--accent);
    z-index:950;
    pointer-events:none;
    transition:border-color .3s ease;
  }

  /* Scary vibe only: old-damaged-film treatment — vignette + faint scratches + grain,
     layered as one pseudo-element background so no extra markup is needed. Purely driven
     by the body[data-vibe="scary"] attribute the vibe picker already sets. */
  body::before{
    content:"";
    position:fixed;inset:0;z-index:36;pointer-events:none;
    opacity:0;transition:opacity .5s ease;
    mix-blend-mode:overlay;
    background:
      radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,.55) 100%),
      repeating-linear-gradient(115deg, rgba(255,255,255,.05) 0px, rgba(255,255,255,.05) 1px, transparent 1px, transparent 3px),
      url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" stitchTiles="stitch"/></filter><rect width="100%25" height="100%25" filter="url(%23n)" opacity="0.5"/></svg>');
  }
  body[data-vibe="scary"]::before{
    opacity:1;
    animation:filmFlicker .3s steps(2) infinite;
  }
  @keyframes filmFlicker{ 0%,100%{opacity:1;} 50%{opacity:.85;} }

  /* Scary vibe only: cobweb artwork as a full-bleed overlay — Bill's own SVG (cleaned up;
     the source file had an Illustrator transparency-grid artifact baked in as ~3,500 flat
     rects, stripped out here so only the actual web line art + its built-in edge-vignette
     texture remain). Sits under the particles/grain so those still read on top of it. */
  #spiderWebOverlay{
    position:fixed;inset:0;z-index:30;pointer-events:none;
    background-image:url('../images/spiderWebs.svg');
    background-size:cover;background-position:center;
    opacity:0;transition:opacity .6s ease;
  }
  body[data-vibe="scary"] #spiderWebOverlay{opacity:.4;}

  /* Scary vibe only: Bill's spider silhouette, drops from the top on a 2px black thread
     at random intervals, dangles with a slight sway, then fades away. */
  #dropSpider{
    position:fixed;top:0;z-index:45;pointer-events:none;
    display:flex;flex-direction:column;align-items:center;
    transform:translateX(-50%);
    opacity:0;transition:opacity .6s ease;
  }
  #dropSpider.visible{opacity:1;}
  .ds-thread{
    width:2px;height:0;background:#000;
    transition:height 1.4s cubic-bezier(.3,.9,.4,1);
  }
  .ds-body{
    display:block;
    transform-origin:top center;
    animation:dsSway 2.4s ease-in-out infinite;
  }
  @keyframes dsSway{ 0%,100%{transform:rotate(-4deg);} 50%{transform:rotate(4deg);} }

  /* ---- per-vibe ambient atmosphere ---- */
  /* A very soft, slowly breathing color wash tinted to the active vibe's own --accent/
     --tint — screen-blended and low-opacity so it never fights legibility, just deepens
     the mood uniformly across every panel. Automatically matches whichever vibe is live
     since it reads the same custom properties everything else does, no per-vibe rules
     needed. Requires a real [data-vibe] to be present and not "basic" — same "no rule,
     no effect" pattern the other vibe-only treatments use, so Basic stays plain. */
  #ambientWash{
    position:fixed;inset:0;z-index:34;pointer-events:none;
    opacity:0;transition:opacity 1.2s ease;mix-blend-mode:screen;
    background:
      radial-gradient(ellipse at 15% 20%, var(--accent) 0%, transparent 55%),
      radial-gradient(ellipse at 85% 80%, var(--tint) 0%, transparent 60%);
  }
  body[data-vibe]:not([data-vibe="basic"]) #ambientWash{
    opacity:.16;animation:ambientBreathe 12s ease-in-out infinite;
  }
  @keyframes ambientBreathe{ 0%,100%{opacity:.12;} 50%{opacity:.22;} }

  /* LoFi vibe only: a soft glow held around the screen's edges rather than spread across
     it — an inset box-shadow blurs var(--accent) inward from all four sides at once and
     fades to nothing well before the center, the opposite shape from #ambientWash's two
     corner blooms above. Same "no rule, no effect" gating as the Scary-only treatments
     further up, just keyed to LoFi's own data-vibe value instead. Automatically follows
     whatever color LoFi's Accent field is set to in wp-admin — nothing here is hardcoded. */
  #edgeGlow{
    position:fixed;inset:0;z-index:33;pointer-events:none;
    opacity:0;transition:opacity 1.4s ease;mix-blend-mode:screen;
    box-shadow:inset 0 0 min(14vw,160px) min(4vw,40px) var(--accent);
  }
  body[data-vibe="lofi"] #edgeGlow{
    opacity:.4;animation:edgeGlowBreathe 11s ease-in-out infinite;
  }
  @keyframes edgeGlowBreathe{ 0%,100%{opacity:.32;} 50%{opacity:.48;} }

  /* LoFi vibe only: the actual reference VHS/static GIF (assets/images/lofi-static.gif,
     free-licensed via Pixabay), used directly as a background layer instead of a
     hand-built approximation — layered on top of the edge glow above, not replacing it.
     A real GIF used as a CSS background-image keeps animating normally in every browser
     (this is a bundled theme asset served straight from the theme folder, not something
     run through WordPress's media/upload pipeline, so it's also not exposed to the
     hosting-level GIF-to-WebP conversion issue documented elsewhere in this file/README).
     mix-blend-mode:screen is what supplies "add some transparency" here rather than a
     flat opacity alone — screen keeps the GIF's own black background from ever showing
     as a dark rectangle over your content (black contributes nothing when screened), so
     only its brighter static/streak highlights actually show through; the opacity on top
     of that just tunes how strong even those highlights read. */
  #lofiStatic{
    /* inset:-3% (not 0) is deliberate overscan — lofiWobble below scales/skews this
       element slightly, and without extra bleed on every edge that motion would
       occasionally reveal a sliver of plain background at the screen's border. */
    position:fixed;inset:-3%;z-index:31;pointer-events:none;
    opacity:0;transition:opacity 1.2s ease;
    mix-blend-mode:screen;
    background-image:url('../images/lofi-static.gif');
    background-size:cover;background-position:center;background-repeat:no-repeat;
  }
  body[data-vibe="lofi"] #lofiStatic{
    opacity:.28;
    animation:lofiWobble 6s ease-in-out infinite;
  }
  /* A slow, small skew/scale drift — a "tape held slightly wrong" wobble rather than
     anything jarring. transform, not filter, so it can't trigger the GIF-freeze bug
     documented elsewhere in this file (a `filter` forces a filtered compositing layer
     that Chrome only rasterizes once; `transform` doesn't touch how the layer paints). */
  @keyframes lofiWobble{
    0%,100%{ transform:scale(1.02) skewY(0deg) translateY(0); }
    25%{ transform:scale(1.03) skewY(.15deg) translateY(-.4%); }
    50%{ transform:scale(1.02) skewY(-.1deg) translateY(.3%); }
    75%{ transform:scale(1.03) skewY(.1deg) translateY(-.2%); }
  }

  /* LoFi vibe only: a couple of thin horizontal bands that mostly just sit there faintly,
     then briefly jolt sideways partway through each cycle — a rough, original stand-in
     for VCR tracking noise (the horizontal tearing you'd see when a tape head is
     slightly out of alignment), built from plain CSS gradients + steps() timing rather
     than any per-pixel canvas trickery. */
  #lofiTracking{
    position:fixed;inset:0;z-index:32;pointer-events:none;
    opacity:0;
    background:
      linear-gradient(0deg, transparent 22%, rgba(180,210,255,.14) 22%, rgba(180,210,255,.14) 24%, transparent 24%),
      linear-gradient(0deg, transparent 61%, rgba(255,255,255,.12) 61%, rgba(255,255,255,.12) 63%, transparent 63%);
  }
  body[data-vibe="lofi"] #lofiTracking{
    opacity:1;
    animation:lofiTrackingJolt 4.5s steps(1) infinite;
  }
  @keyframes lofiTrackingJolt{
    0%,72%{ transform:translate(0,0); opacity:.55; filter:none; }
    73%{ transform:translate(-6px,6px); opacity:.9; filter:drop-shadow(2px 0 var(--accent)) drop-shadow(-2px 0 rgba(120,200,255,.7)); }
    76%{ transform:translate(5px,-4px); opacity:.7; filter:drop-shadow(-2px 0 var(--accent)) drop-shadow(2px 0 rgba(120,200,255,.7)); }
    79%,100%{ transform:translate(0,0); opacity:.55; filter:none; }
  }

  /* A single bright bar that continuously sweeps bottom-to-top and loops — different
     from the jolt bands above (which stay put and just flicker), this one is always in
     motion, the way a real tape's tracking line visibly crawls up the screen. Colored
     from the vibe's own --accent rather than an unrelated hue, so it stays inside the
     blue/purple palette LoFi actually uses. */
  #lofiSweep{
    position:fixed;left:0;right:0;height:3px;z-index:32;pointer-events:none;
    top:100%;opacity:0;
    background:linear-gradient(90deg, transparent 0%, var(--accent) 20%, rgba(255,255,255,.6) 50%, var(--accent) 80%, transparent 100%);
    filter:blur(1px);
  }
  body[data-vibe="lofi"] #lofiSweep{
    opacity:.8;
    animation:lofiSweepMove 5s linear infinite;
  }
  @keyframes lofiSweepMove{
    from{ top:105%; }
    to{ top:-5%; }
  }

  /* LoFi vibe only: always-on scanlines + a plain dark vignette (corners only — the
     opposite shape from #edgeGlow's bright screen-blended edges above, so the two read
     as "CRT tube" and "backlight bleed" happening at once rather than canceling out). */
  #lofiScanlines{
    position:fixed;inset:0;z-index:33;pointer-events:none;
    opacity:0;transition:opacity 1s ease;
    background:
      repeating-linear-gradient(0deg, rgba(0,0,0,.22) 0px, rgba(0,0,0,.22) 1px, transparent 1px, transparent 3px),
      radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.42) 100%);
    background-size:100% 60px, 100% 100%;
  }
  body[data-vibe="lofi"] #lofiScanlines{
    opacity:1;animation:lofiScanDrift 8s linear infinite;
  }
  /* The lines themselves slowly crawl downward rather than sitting fixed — only the
     first background layer's position needs to move, so the second (the vignette,
     100% 100%) is repeated at the same offset each step and never visibly shifts. */
  @keyframes lofiScanDrift{
    from{ background-position:0 0, 0 0; }
    to{ background-position:0 60px, 0 0; }
  }

  /* ambient particles matching the active vibe — fixed, full-viewport, never blocks clicks */
  #vibeParticles{position:fixed;inset:0;z-index:35;pointer-events:none;}

  /* cinematic iris wipe, used only for mid-session vibe switches (the welcome screen has
     its own fade-dismiss already) — a circle of the new vibe's accent color expands from
     the button you pressed until it covers the screen, palette swaps underneath while
     covered, then the same circle shrinks back down, "opening" onto the new scene. --vt-x/
     --vt-y are set from JS to the trigger button's center; default to screen center if
     ever used without one. */
  #vibeTransition{
    position:fixed;inset:0;z-index:920;background:var(--accent);
    pointer-events:none;
    clip-path:circle(0% at var(--vt-x,50%) var(--vt-y,50%));
    transition:clip-path .55s cubic-bezier(.65,0,.35,1);
  }
  #vibeTransition.active{clip-path:circle(150% at var(--vt-x,50%) var(--vt-y,50%));}

  /* ---- sound toggle ---- */
  /* No full-screen gate blocking the site. Browsers still require a genuine click before
     audio can play with sound, so this small persistent pill IS that gesture — clicking it
     starts (or stops) track 1 in the same action. Mirrors the always-visible "Sound: On/Off"
     control pattern on lemansclassic.richardmille.com rather than a mandatory entry screen. */
  #soundToggle{
    position:fixed;top:44px;right:20px;z-index:80;
    display:flex;align-items:center;gap:8px;
    background:rgba(12,12,12,.7);border:1px solid rgba(255,255,255,.25);border-radius:100px;
    padding:8px 16px;cursor:pointer;
    font-family:'Space Grotesk',sans-serif;font-size:11px;letter-spacing:.12em;text-transform:uppercase;
    color:var(--white);backdrop-filter:blur(6px);
    transition:border-color .25s ease,background .25s ease;
  }
  #soundToggle:hover{border-color:var(--accent);background:rgba(0,57,166,.25);}
  #soundToggle i{font-size:12px;color:var(--warm-gray);}
  #soundToggle.is-on i{color:var(--accent);}
  @media (min-width:1024px){ #soundToggle{top:52px;right:36px;} }

  /* ---- persistent name mark, upper-left ---- */
  /* Mirrors #soundToggle's approach exactly: fixed at the same z-index (80), so it sits
     UNDER the welcome gate (z-index 900) and only becomes visible once the gate fades —
     no separate show/hide logic needed, it's just always there once you're "in" the site.
     Doubles as a home link (jumpToReal(0), same target as the FAB menu's Home item), and
     its accent half tracks --accent automatically, same as every other themed element. */
  #siteMark{
    position:fixed;top:44px;left:20px;z-index:80;
    display:flex;align-items:baseline;gap:.35em;
    font-family:'Space Grotesk',sans-serif;font-size:13px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
    text-decoration:none;padding:8px 4px;
    text-shadow:0 1px 10px rgba(0,0,0,.55),0 1px 2px rgba(0,0,0,.4);
  }
  #siteMark .mark-first{color:var(--white);transition:color .25s ease;}
  #siteMark .mark-last{color:var(--accent);transition:color .25s ease;}
  #siteMark:hover .mark-first{color:var(--accent);}
  #siteMark:hover .mark-last{color:var(--white);}
  @media (min-width:1024px){ #siteMark{top:52px;left:36px;} }

  /* ---- typing/rotating role word, on the Hero panel ---- */
  /* Was the old welcome/entry screen's role word above "Bill Bergmann" before that screen
     was removed; now sits directly above the Hero panel's "Hi, I'm Billy!" headline
     instead (see .hero-role-typer usage in renderHero(), assets/js/site.js). */
  .hero-role-typer{
    font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:1rem;letter-spacing:.14em;
    text-transform:uppercase;color:var(--accent);text-shadow:0 1px 10px rgba(0,0,0,.4);
    margin-bottom:10px;min-height:1.2em;
    transition:color .5s var(--ease);
  }
  .hero-role-cursor{display:inline-block;margin-left:2px;color:var(--white);animation:cursorBlink 1s step-end infinite;}
  @keyframes cursorBlink{ 0%,100%{opacity:1;} 50%{opacity:0;} }
  @media (prefers-reduced-motion:reduce){ .hero-role-cursor{animation:none;} }

  /* every panel's small eyebrow label types itself out the same way the Hero panel's
     role word does, the moment its panel becomes active */
  .eyebrow-cursor{display:inline-block;color:var(--accent);animation:cursorBlink 1s step-end infinite;}
  @media (prefers-reduced-motion:reduce){ .eyebrow-cursor{animation:none;display:none;} }

  .vibe-label{
    display:inline-block;font-family:'Space Grotesk',sans-serif;
    font-size:12.5px;letter-spacing:.06em;text-transform:uppercase;
  }
  .spec-tag{font-size:11px;color:var(--black);background:var(--white);border:1px dashed var(--black);padding:3px 8px;line-height:1.5;opacity:.85;}
  .imgspec{position:relative;background:repeating-linear-gradient(45deg,#1c1c1c,#1c1c1c 10px,#161616 10px,#161616 20px);border:1px solid var(--warm-gray-dim);display:flex;align-items:flex-end;overflow:hidden;}
  .imgspec .spec-tag{position:absolute;top:12px;left:12px;max-width:80%;}
  .imgspec .spec-label{position:absolute;bottom:12px;right:12px;font-family:'Space Grotesk',sans-serif;font-size:11px;color:var(--warm-gray);letter-spacing:.08em;text-transform:uppercase;}
  .eyebrow{
    font-family:'Space Grotesk',sans-serif;font-size:12px;letter-spacing:.18em;text-transform:uppercase;
    color:var(--white);font-weight:600;margin-bottom:16px;display:flex;align-items:center;gap:10px;
    text-shadow:0 1px 8px rgba(0,0,0,.5);
  }
  .eyebrow::before{content:"";width:24px;height:1px;background:var(--accent);}
  .home-badge{
    display:inline-flex;align-items:center;justify-content:center;
    width:34px;height:34px;font-size:13px;color:var(--black);background:var(--warm-gray);
    border-radius:50%;margin-bottom:20px;
  }

  /* ---- parallax background layer (behind every panel's content) ---- */
  /* outer layer: ONLY handles the JS scroll-driven parallax offset, via transition (no keyframes) */
  .bg-layer{
    position:absolute;inset:-60px;overflow:visible;
    --px:0px;
    transform:translateX(var(--px));
    transition:transform .6s cubic-bezier(.22,.8,.32,1);
    will-change:transform;
  }
  /* inner layer: ONLY handles the idle drift + the active reveal — a pure, uninterrupted CSS animation
     that never gets touched by JS, so it stays perfectly smooth regardless of scroll activity */
  .bg-layer-img{
    /* inset:0, not another -60px — .bg-layer (the parent, and this element's actual
       positioning context) is ALREADY inset -60px relative to .panel. Giving this its
       own additional -60px stacked another 60px on top of that (120px per side total),
       which made background-size:cover fit against a box far bigger than the panel
       actually shows, so every photo looked over-zoomed/cropped too tight. inset:0 here
       fills that already-buffered box exactly — still enough bleed for the parallax
       translateX and the blur below to never show a seam, just no extra unwanted zoom.
       No `filter` here on purpose — see .bg-filter just below for why. */
    position:absolute;inset:0;
    background-size:cover;background-position:center;
    opacity:.68;
    transition:opacity 1.6s var(--ease);
    will-change:opacity;
  }
  .panel.is-active .bg-layer-img{
    opacity:.95;
  }
  /* Optional per-section "Background Video" — sits directly on top of .bg-layer-img above
     (which stays put underneath as the instant-paint poster and the permanent fallback for
     anyone not using this field). Same size/position/opacity treatment as the image layer
     so swapping one for the other never shifts anything else in the panel. JS only ever
     calls .play() on whichever panel is currently .is-active (see the background-video
     play/pause block in site.js) — every other copy sits paused on its poster frame. */
  .bg-layer-video{
    position:absolute;inset:0;
    width:100%;height:100%;
    object-fit:cover;
    opacity:.68;
    transition:opacity 1.6s var(--ease);
    will-change:opacity;
  }
  .panel.is-active .bg-layer-video{
    opacity:.95;
  }
  /* The grayscale/brightness/blur dimming lives on this separate, transparent overlay via
     backdrop-filter instead of a plain `filter` directly on .bg-layer-img above. A `filter`
     forces the element holding it onto its own filtered compositing layer — and in Chrome
     (and other Chromium browsers), that layer gets rasterized once rather than continuously,
     which silently freezes an animated GIF background on its very first frame the instant
     ANY filter, even a subtle one, is applied to the element painting it. backdrop-filter
     instead samples whatever is already painted directly behind this overlay fresh on every
     frame, so a GIF underneath keeps animating exactly as it should while still getting the
     identical dimmed/blurred look. */
  .bg-filter{
    position:absolute;inset:0;
    -webkit-backdrop-filter:grayscale(45%) brightness(.6) saturate(.9) blur(32px);
    backdrop-filter:grayscale(45%) brightness(.6) saturate(.9) blur(32px);
    transition:-webkit-backdrop-filter 1.6s var(--ease), backdrop-filter 1.6s var(--ease);
    will-change:backdrop-filter;
  }
  .panel.is-active .bg-filter{
    -webkit-backdrop-filter:grayscale(20%) brightness(.78) saturate(1.05) blur(0px);
    backdrop-filter:grayscale(20%) brightness(.78) saturate(1.05) blur(0px);
  }
  /* Scary vibe: force true black & white on every background, at rest and active alike —
     more specific than the two rules above so it wins without !important. Paired with the
     grain/vignette overlay above for an old, damaged-film look. */
  body[data-vibe="scary"] .bg-filter{
    -webkit-backdrop-filter:grayscale(100%) contrast(1.15) brightness(.55) blur(32px);
    backdrop-filter:grayscale(100%) contrast(1.15) brightness(.55) blur(32px);
  }
  body[data-vibe="scary"] .panel.is-active .bg-filter{
    -webkit-backdrop-filter:grayscale(100%) contrast(1.25) brightness(.8) blur(0px);
    backdrop-filter:grayscale(100%) contrast(1.25) brightness(.8) blur(0px);
  }
  .bg-tint{
    position:absolute;inset:-30px;
    background:var(--tint);
    mix-blend-mode:color;
    opacity:.8;
    pointer-events:none;
  }
  /* Basic vibe: photos show in their own true colors. Every other vibe recolors the
     backgrounds (the .bg-tint wash above, plus the partial grayscale/desaturation in
     .bg-filter) — Basic is the "no special look" default, so it drops both. A light
     brightness dim stays on purpose so body text remains readable over bright photos;
     the blur at rest (before a section becomes active) is unchanged. */
  body[data-vibe="basic"] .bg-tint{display:none;}
  body[data-vibe="basic"] .bg-filter{
    -webkit-backdrop-filter:brightness(.7) blur(32px);
    backdrop-filter:brightness(.7) blur(32px);
  }
  body[data-vibe="basic"] .panel.is-active .bg-filter{
    -webkit-backdrop-filter:brightness(.82) blur(0px);
    backdrop-filter:brightness(.82) blur(0px);
  }
  /* idle drift animation removed per feedback — bg now only dims/brightens on activation and drifts with scroll parallax */

  /* ---- inline images: float at rest, wake up on hover, expand to lightbox on click ---- */
  .imgspec{position:relative;overflow:hidden;cursor:pointer;border:1px solid var(--warm-gray-dim);background:#161616;animation:floaty 8s ease-in-out infinite;}
  .imgspec:nth-of-type(2n){animation-duration:9.5s;animation-delay:-2.4s;}
  .imgspec:nth-of-type(3n){animation-duration:7s;animation-delay:-4s;}
  .tile-grid .imgspec{animation-name:floatySm;}
  @keyframes floaty{0%,100%{transform:translateY(0);}50%{transform:translateY(-9px);}}
  @keyframes floatySm{0%,100%{transform:translateY(0);}50%{transform:translateY(-5px);}}
  .imgspec-media{
    position:absolute;inset:0;background-size:cover;background-position:center;
    filter:brightness(.95) saturate(.96);
    transform:scale(1);
    transition:transform .5s var(--ease), filter .5s var(--ease);
  }
  .imgspec:hover{box-shadow:0 16px 44px rgba(0,0,0,.55);border-color:var(--accent);}
  .imgspec:hover .imgspec-media{transform:scale(1.07);filter:brightness(1) saturate(1.05);}
  .imgspec .spec-tag{z-index:2;}
  .imgspec .spec-label{z-index:2;}

  /* ---- lightbox ---- */
  #lightbox{position:fixed;inset:0;z-index:500;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .35s ease;}
  #lightbox.open{opacity:1;pointer-events:auto;}
  .lightbox-backdrop{position:absolute;inset:0;background:rgba(6,6,6,.94);}
  .lightbox-stage{position:relative;z-index:2;width:min(1200px,90vw);max-height:88vh;display:flex;flex-direction:column;transform:scale(.94) translateY(10px);transition:transform .4s var(--ease);}
  #lightbox.open .lightbox-stage{transform:scale(1) translateY(0);}
  .lightbox-stage .lb-media{width:100%;height:min(62vh,640px);background-size:cover;background-position:center;border-radius:2px;}
  .lightbox-caption{padding:22px 4px 0;}
  .lightbox-caption h3{font-family:'Space Grotesk',sans-serif;font-size:1.3rem;margin-bottom:8px;}
  .lightbox-caption p{color:var(--warm-gray);font-size:.95rem;line-height:1.6;max-width:64ch;}
  .lightbox-close{
    position:fixed;top:26px;right:28px;z-index:600;width:46px;height:46px;border-radius:50%;
    background:rgba(255,255,255,.12);border:1.5px solid rgba(255,255,255,.4);color:var(--white);
    font-size:18px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;
    transition:background .2s ease, transform .3s ease;
  }
  .lightbox-close:hover{background:var(--accent);transform:rotate(90deg);}

  /* ---- Portfolio grid (thumbnail grid on the homepage) + project detail overlay ---- */
  .portfolio-tile-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:8px;}
  .portfolio-tile{display:block;padding:0;width:100%;text-align:inherit;font:inherit;color:inherit;}
  .portfolio-empty{color:var(--warm-gray-dim);font-size:.95rem;font-style:italic;}
  .portfolio-cta-link{display:block;margin-top:18px;}

  /* ---- Music grid (homepage) — each tile plays inline rather than opening an overlay,
     since the point is to actually listen without an extra click. A video tile is a
     click-to-load YouTube facade (thumbnail + play button) that swaps itself for a real
     iframe on click; an audio tile is just a native <audio> element — no custom player UI
     to keep in sync, the browser already gives play/pause/scrub/volume for free. */
  .music-tile-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-top:8px;}
  .music-tile{background:#161616;border:1px solid var(--warm-gray-dim);overflow:hidden;}
  .music-empty-grid{color:var(--warm-gray-dim);font-size:.95rem;font-style:italic;}
  .music-empty{color:var(--warm-gray-dim);font-size:.8rem;font-style:italic;}
  .music-cta-link{display:block;margin-top:18px;}

  .music-video-frame{position:relative;width:100%;aspect-ratio:16/9;background:#0f0f0f;}
  .music-video-frame iframe{width:100%;height:100%;border:0;display:block;}
  .music-video-facade{
    display:block;width:100%;height:100%;border:0;padding:0;cursor:pointer;
    background-size:cover;background-position:center;position:relative;
  }
  .music-play-btn{
    position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
    width:44px;height:44px;border-radius:50%;background:rgba(0,0,0,.55);border:1.5px solid rgba(255,255,255,.6);
    display:flex;align-items:center;justify-content:center;color:var(--white);font-size:15px;
    transition:background .2s ease,border-color .2s ease,transform .2s ease;
  }
  .music-video-facade:hover .music-play-btn{background:rgba(0,57,166,.6);border-color:var(--accent);transform:translate(-50%,-50%) scale(1.08);}

  .music-tile-audio{display:flex;gap:10px;padding:10px;align-items:flex-start;}
  .music-art{
    width:56px;height:56px;flex:0 0 56px;background:#0f0f0f;background-size:cover;background-position:center;
    display:flex;align-items:center;justify-content:center;color:var(--warm-gray-dim);font-size:18px;
    border:1px solid var(--warm-gray-dim);
  }
  .music-tile-body{flex:1;min-width:0;}
  .music-tile-body audio{width:100%;margin-top:6px;height:32px;}
  .music-tile-title{
    padding:10px 12px 2px;font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:.85rem;color:var(--white);
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  }
  .music-tile-audio .music-tile-title{padding:0;}
  .music-caption{
    padding:0 12px 10px;font-size:.78rem;color:var(--warm-gray-dim);
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  }
  .music-tile-audio .music-caption{padding:2px 0 0;}
  .music-tile-video .music-empty{padding:14px;}

  .portfolio-lightbox{
    position:fixed;inset:0;z-index:960;display:none;align-items:center;justify-content:center;
    padding:24px;
  }
  .portfolio-lightbox.open{display:flex;}
  .plb-backdrop{position:absolute;inset:0;background:rgba(6,8,14,.55);backdrop-filter:blur(2px);}
  .plb-panel{
    position:relative;z-index:2;width:min(1440px,95vw);max-height:90vh;overflow-y:auto;
    background:#111;border:1px solid #262626;border-radius:2px;
    box-shadow:0 24px 70px rgba(0,0,0,.6);
    display:block;padding:32px 24px;
  }
  .plb-text .eyebrow{margin-bottom:10px;}
  .plb-text h2{font-family:'Space Grotesk',sans-serif;font-size:1.8rem;margin-bottom:10px;}
  .plb-text .lede{font-size:1rem;color:var(--warm-gray);margin-bottom:14px;line-height:1.6;}
  .plb-text .body{font-size:.98rem;line-height:1.7;color:#dcd9d2;}
  .plb-visual{position:relative;margin-top:24px;}
  .plb-track{
    display:flex;overflow-x:auto;scroll-snap-type:x mandatory;width:100%;height:380px;
    -webkit-overflow-scrolling:touch;scrollbar-width:none;background:#0a0a0a;border-radius:2px;
  }
  .plb-track::-webkit-scrollbar{display:none;}
  .plb-slide{
    flex:0 0 100%;scroll-snap-align:center;display:flex;align-items:center;justify-content:center;
    padding:12px;
  }
  .plb-slide img{
    max-width:100%;max-height:100%;object-fit:contain;border-radius:2px;
    cursor:zoom-in;transition:transform .3s var(--ease),box-shadow .3s ease;
  }
  .plb-slide img:hover{transform:scale(1.015);box-shadow:0 10px 30px rgba(0,0,0,.5);}
  .plb-empty{
    width:100%;height:100%;display:flex;align-items:center;justify-content:center;
    color:var(--warm-gray-dim);font-size:.9rem;font-style:italic;text-align:center;padding:24px;
  }
  .plb-close,.plb-nav{
    position:absolute;border-radius:50%;border:1px solid rgba(255,255,255,.25);
    background:rgba(0,0,0,.4);color:var(--white);font-size:16px;cursor:pointer;
    display:flex;align-items:center;justify-content:center;z-index:3;
    transition:border-color .2s ease,background .2s ease,transform .2s ease;
  }
  .plb-close:hover,.plb-nav:hover{border-color:var(--accent);background:rgba(0,57,166,.3);}
  .plb-close{
    top:20px;right:20px;width:46px;height:46px;position:fixed;
    background:#111;border:1.5px solid rgba(255,255,255,.4);font-size:18px;
    box-shadow:0 6px 20px rgba(0,0,0,.5);
  }
  .plb-close:hover{transform:rotate(90deg);}
  .plb-nav{top:50%;transform:translateY(-50%);width:40px;height:40px;}
  .plb-prev{left:8px;}
  .plb-next{right:8px;}
  .plb-nav[hidden]{display:none;}
  @media (min-width:1024px){ .plb-close{top:32px;right:32px;} }
  .plb-dots{position:absolute;left:50%;bottom:12px;transform:translateX(-50%);display:flex;gap:8px;z-index:3;}
  .plb-dots[hidden]{display:none;}
  .plb-dot{
    width:8px;height:8px;border-radius:50%;border:0;padding:0;
    background:rgba(255,255,255,.4);cursor:pointer;transition:background .2s ease,transform .2s ease;
  }
  .plb-dot:hover{background:rgba(255,255,255,.65);}
  .plb-dot.is-active{background:var(--accent);transform:scale(1.2);}

  /* Next/previous PROJECT (not photo) — pinned to the outer screen edges, deliberately
     separate from .plb-nav (which pages through the current project's own photos, inside
     the visual). Same circular, translucent-dark, backdrop-blurred look as the homepage's
     own #dirHints .arrow buttons, so it reads as "the same kind of navigation" site-wide. */
  .plb-proj-nav{
    position:fixed;top:50%;transform:translateY(-50%);z-index:3;
    width:44px;height:44px;border-radius:50%;border:1px solid rgba(255,255,255,.4);
    background:rgba(0,0,0,.45);color:var(--white);font-size:18px;cursor:pointer;
    display:flex;align-items:center;justify-content:center;
    backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);box-shadow:0 6px 20px rgba(0,0,0,.4);
    transition:border-color .2s ease,background .2s ease,transform .2s ease;
  }
  .plb-proj-nav:hover{border-color:var(--accent);color:var(--accent);background:rgba(0,0,0,.7);}
  .plb-proj-prev{left:16px;}
  .plb-proj-prev:hover{transform:translateY(-50%) translateX(-3px);}
  .plb-proj-next{right:16px;}
  .plb-proj-next:hover{transform:translateY(-50%) translateX(3px);}
  .plb-proj-nav[hidden]{display:none;}
  @media (min-width:1024px){ .plb-proj-prev{left:28px;} .plb-proj-next{right:28px;} }

  @media (min-width:1024px){
    .plb-panel{padding:48px;}
    .plb-panel{display:grid;grid-template-columns:.9fr 1.2fr;gap:48px;align-items:center;}
    .plb-visual{margin-top:0;}
    .plb-track{height:600px;}
  }

  /* ---- photo zoom: clicking the current photo inside the Portfolio overlay opens it here
     at full size, above everything (including the Portfolio overlay itself), with its own
     dedicated close (X) — same visual language as the other close buttons on the site. ---- */
  .plb-zoom{
    position:fixed;inset:0;z-index:970;display:none;align-items:center;justify-content:center;
    padding:24px;
  }
  .plb-zoom.open{display:flex;}
  .plb-zoom-backdrop{position:absolute;inset:0;background:rgba(4,4,4,.92);}
  .plb-zoom-img{
    position:relative;z-index:2;max-width:92vw;max-height:92vh;object-fit:contain;
    border-radius:2px;box-shadow:0 30px 90px rgba(0,0,0,.65);
  }
  .plb-zoom-close{
    position:fixed;top:20px;right:20px;width:46px;height:46px;border-radius:50%;z-index:3;
    background:#111;border:1.5px solid rgba(255,255,255,.4);color:var(--white);font-size:18px;
    display:flex;align-items:center;justify-content:center;cursor:pointer;
    box-shadow:0 6px 20px rgba(0,0,0,.5);transition:border-color .2s ease,transform .2s ease;
  }
  .plb-zoom-close:hover{border-color:var(--accent);color:var(--accent);transform:rotate(90deg);}
  @media (min-width:1024px){ .plb-zoom-close{top:32px;right:32px;} }

  /* The dark navy gradient that used to sit over every background photo (heaviest on the
     left, behind the text) is removed — photos now show their own colors with no navy cast.
     The element is still created by site.js, just not painted. The photo dimming in
     .bg-filter above is what remains for text readability. */
  .bg-scrim{display:none;}

  /* ---- mobile-first fallback: normal vertical stack ---- */
  #viewport{padding-top:32px;}
  #track{display:flex;flex-direction:column;}
  .panel{width:100%;min-height:100vh;padding:80px 18px 64px;position:relative;display:flex;align-items:center;border-bottom:1px solid #1f1f1f;overflow:hidden;}
  .panel-inner{max-width:720px;width:100%;margin:0 auto;position:relative;z-index:2;}
  .mobile-index{z-index:2;}

  /* ---- cinematic content reveal ---- */
  /* Each panel's content cascades in — eyebrow, then headline, then body, then whatever
     follows — rather than just sitting there static the moment a panel becomes active.
     Targets .panel-inner's direct children, and (for split/two-column sections) the two
     halves of .split individually, so each block gets its own stagger regardless of
     which section layout it is. Re-plays every time a panel re-enters view on desktop;
     plays once on mobile's scroll-in. Respects reduced-motion. */
  .panel-inner > :not(.split), .panel-inner > .split > *{
    opacity:0;transform:translateY(28px);
    transition:opacity .85s var(--ease),transform .85s var(--ease);
  }
  .panel.is-active .panel-inner > :not(.split), .panel.is-active .panel-inner > .split > *{
    opacity:1;transform:translateY(0);
  }
  .panel-inner > :not(.split):nth-child(1), .panel-inner > .split > *:nth-child(1){transition-delay:.05s;}
  .panel-inner > :not(.split):nth-child(2), .panel-inner > .split > *:nth-child(2){transition-delay:.2s;}
  .panel-inner > :not(.split):nth-child(3), .panel-inner > .split > *:nth-child(3){transition-delay:.35s;}
  .panel-inner > :not(.split):nth-child(4){transition-delay:.5s;}
  .panel-inner > :not(.split):nth-child(5){transition-delay:.65s;}
  @media (prefers-reduced-motion:reduce){
    .panel-inner > :not(.split), .panel-inner > .split > *{opacity:1;transform:none;transition:none;}
  }
  .panel h2{font-size:clamp(2.6rem,12vw,4rem);margin-bottom:18px;}
  /* Optional one-line intro between the Heading and Body on every section type (Hero
     already had this as its "Subhead" field — .hero-panel .lede below — this is the same
     treatment made available on the other 8 sections too, via each one's own "Subhead"
     field). Only rendered when the field actually has content, so leaving it blank looks
     identical to before this existed. */
  .panel .lede{font-size:1.1rem;color:var(--warm-gray);max-width:52ch;margin-bottom:18px;line-height:1.6;}
  .panel .body{font-size:1.05rem;line-height:1.7;color:#dcd9d2;max-width:52ch;}
  .panel .body p{margin-bottom:1em;}
  .panel .body p:last-child{margin-bottom:0;}
  .panel .imgspec{width:100%;height:270px;margin-top:28px;}
  .tile-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:24px;}
  .tile-grid .imgspec{height:155px;margin-top:0;}
  .portfolio-tile-grid .imgspec{height:155px;margin-top:0;}
  .hero-panel h1{font-size:clamp(2.6rem,11vw,4.6rem);}
  .hero-panel .lede{font-size:1.1rem;color:var(--warm-gray);max-width:44ch;margin-top:16px;line-height:1.6;}
  .cta{margin-top:28px;display:inline-flex;align-items:center;gap:10px;font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:.95rem;border-bottom:2px solid var(--accent);padding-bottom:4px;}
  /* ---- Weekly Post section: primary "read this post" (decorative, same as other CTAs
     in this mock) plus a real, working secondary link out to the standalone archive page —
     that page is a genuinely separate template in WP terms, not another loop panel. */
  .weekly-actions{margin-top:28px;display:flex;align-items:center;gap:22px;flex-wrap:wrap;}
  .weekly-actions .cta{margin-top:0;}
  .weekly-archive-link{
    font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:.9rem;color:var(--warm-gray);
    text-decoration:underline;text-underline-offset:4px;transition:color .2s ease;
  }
  .weekly-archive-link:hover{color:var(--accent);}
  /* ---- Hero "keep exploring" — real navigation, not decorative text ---- */
  .explore-cta{margin-top:28px;display:flex;align-items:center;gap:16px;flex-wrap:wrap;}
  .explore-label{font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:.95rem;color:var(--white);}
  .explore-arrows{display:none;gap:10px;} /* shown only on desktop, where the loop-nav these drive actually exists */
  .explore-arrow{
    width:38px;height:38px;border-radius:50%;border:1.5px solid rgba(255,255,255,.35);
    background:none;color:var(--white);display:flex;align-items:center;justify-content:center;
    cursor:pointer;font-size:13px;transition:border-color .25s ease,background .25s ease,transform .2s ease;
  }
  .explore-arrow:hover{border-color:var(--accent);background:rgba(0,57,166,.18);transform:scale(1.08);}
  footer.contact-foot{display:flex;flex-wrap:wrap;gap:14px;margin-top:28px;}
  .social-icons a{
    width:42px;height:42px;border-radius:50%;border:1px solid var(--warm-gray-dim);
    display:flex;align-items:center;justify-content:center;color:var(--warm-gray);
    transition:all .25s var(--ease);
  }
  .social-icons a:hover{border-color:var(--accent);color:var(--accent);background:rgba(0,57,166,.14);transform:translateY(-3px);}
  .social-icons i{font-size:16px;}

  /* ---- contact form (WPForms placeholder) ---- */
  .contact-form{display:flex;flex-direction:column;gap:14px;max-width:440px;}
  .contact-form label{
    font-family:'Space Grotesk',sans-serif;font-size:11px;letter-spacing:.08em;text-transform:uppercase;
    color:var(--white);font-weight:700;margin-bottom:6px;display:block;
  }
  .contact-form input,.contact-form textarea{
    width:100%;background:rgba(250,249,246,.9);border:1px solid var(--warm-gray-dim);color:var(--black);
    padding:12px 14px;font-family:'Inter',sans-serif;font-size:.95rem;border-radius:2px;
    transition:border-color .2s ease,background .2s ease;
  }
  .contact-form input::placeholder,.contact-form textarea::placeholder{color:#7a7468;opacity:1;}
  .contact-form input:focus,.contact-form textarea:focus{outline:none;border-color:var(--accent);background:rgba(255,255,255,.98);}
  .contact-form textarea{min-height:110px;resize:vertical;font-family:'Inter',sans-serif;}
  .contact-form button[type="submit"]{
    align-self:flex-start;background:var(--accent);color:var(--white);border:none;
    padding:14px 26px;font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:.95rem;
    letter-spacing:.02em;border-radius:2px;cursor:pointer;transition:background .2s ease,transform .2s ease;
  }
  .contact-form button[type="submit"]:hover{background:#0047c9;transform:translateY(-2px);}
  .contact-form button[type="submit"]:disabled{opacity:.6;cursor:default;transform:none;}
  /* Honeypot: rendered and tab-order-skipped like a real field, but invisible and
     unreachable, so bots that fill in every input still get caught (see
     inc/contact-form-handler.php). Deliberately not display:none/visibility:hidden —
     some spam bots specifically skip fields hidden that way but still fill this in. */
  .bb-hp{position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;overflow:hidden;}
  .form-status{
    font-family:'Inter',sans-serif;font-size:.9rem;padding:10px 14px;border-radius:2px;margin-bottom:2px;
  }
  .form-status--ok{background:rgba(60,180,120,.15);border:1px solid rgba(60,180,120,.5);color:#bdf0d6;}
  .form-status--err{background:rgba(210,60,60,.15);border:1px solid rgba(210,60,60,.5);color:#f5c6c6;}
  .mobile-index{position:absolute;top:80px;right:24px;font-family:'Space Grotesk',sans-serif;font-size:12px;color:var(--warm-gray-dim);}

  #homeHint,#dirHints{display:none;}

  /* Nav jump-to-section menu (the +/× button) — this used to be desktop-only (hidden
     here, only re-shown/positioned by the @media(min-width:1024px) #fab rules further
     down). It's now on mobile too, sitting at the same bottom offset and size as the
     vibes/music-note button in the opposite corner (#trackPlayer/#tpToggle, bottom:20px,
     56px) so the two sit level with each other instead of one being higher than the
     other. #backToTop (which only fades in once you've scrolled) stacks above this one
     rather than the other way around, since this button is the one that's always visible.
     The desktop media query below still fully overrides this block's position/size at
     ≥1024px, same as before. */
  #fab{
    display:flex;align-items:center;justify-content:center;position:fixed;bottom:20px;right:16px;z-index:70;
    width:56px;height:56px;background:var(--accent);color:var(--fab-icon,var(--white));border:none;
    cursor:pointer;filter:drop-shadow(0 4px 14px rgba(0,0,0,.45));
    transition:transform .3s var(--ease),color .25s ease;
  }
  #fab i{font-size:17px;transform:translateY(-3px);transition:transform .3s var(--ease);}
  #fab.open i{transform:translateY(-3px) rotate(45deg);}
  #fabMenu{
    display:block;position:fixed;bottom:88px;right:16px;z-index:65;
    background:rgba(12,12,12,.92);border:1px solid #262626;border-radius:12px;
    backdrop-filter:blur(8px);padding:10px;min-width:200px;max-width:calc(100vw - 32px);
    opacity:0;transform:translateY(12px) scale(.96);pointer-events:none;
    transition:opacity .25s ease, transform .25s ease;
  }
  #fabMenu.open{opacity:1;transform:translateY(0) scale(1);pointer-events:auto;}
  #fabMenu button{
    display:flex;align-items:center;gap:10px;width:100%;text-align:left;
    background:none;border:none;color:var(--white);padding:9px 10px;border-radius:6px;
    font-size:13px;cursor:pointer;
  }
  #fabMenu button:hover{background:rgba(0,57,166,.25);color:#fff;}
  #fabMenu button .n{font-family:'Space Grotesk',sans-serif;color:var(--warm-gray-dim);font-size:11px;width:20px;}
  #fabMenu hr{border:none;border-top:1px solid #262626;margin:6px 4px;}
  #fabMenu .home-item{color:var(--accent);font-weight:600;}

  /* ---- semi-hidden multi-track / vibe player ---- */
  /* Lives at base scope (not inside the desktop media query below) so the vibe selector,
     its volume sliders, and the particle density control are all still reachable on
     mobile — it's effectively its own small FAB in the opposite corner from #backToTop. */
  #trackPlayer{display:block;position:fixed;bottom:20px;left:16px;z-index:70;}
  /* Site Settings → "Enable Vibes" off (see site.js): hide the floating music-note/vibe
     button entirely rather than leaving it visible but empty. */
  body.vibes-disabled #trackPlayer{display:none !important;}
  #tpToggle{
    width:56px;height:56px;position:relative;border:none;
    /* This background now plays the role the 1px border used to — a normal `border`
       can't follow a masked (non-rectangular) shape cleanly, so instead the button's
       own fill is a thin ring color, and the actual dark fill sits on an inset ::before
       layer just inside it (see below), leaving a ~1.5px ring visible around the pick. */
    background:#262626;color:var(--white);cursor:pointer;display:flex;align-items:center;justify-content:center;
    filter:drop-shadow(0 4px 14px rgba(0,0,0,.5));
    transform:rotate(0deg);
    transition:background .25s ease,transform .6s cubic-bezier(.34,1.56,.64,1);
  }
  #tpToggle::before{
    content:"";position:absolute;inset:1.5px;background:rgba(12,12,12,.85);
    -webkit-mask-image:var(--pick-svg); mask-image:var(--pick-svg);
    -webkit-mask-size:100% 100%; mask-size:100% 100%;
    -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
    backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
  }
  #tpToggle:hover{background:var(--accent);transform:rotate(0deg) scale(1.06);}
  /* The pick points down at rest. Opening the panel spins it 900deg (2.5 full turns) so it
     lands pointing up — right at the panel that opens directly above it — rather than just
     snapping there; closing unwinds the same 900deg back to 0, so both directions read as a
     quick, deliberate spin instead of an instant flip. */
  #trackPlayer.open #tpToggle{transform:rotate(900deg);}
  #trackPlayer.open #tpToggle:hover{transform:rotate(900deg) scale(1.06);}
  #tpToggle i,#tpToggle .tp-note-svg,#tpToggle .tp-bars{position:relative;z-index:1;transform:translateY(-3px);}
  #tpToggle i{font-size:17px;}
  /* Self-uploaded SVG icon (Site Settings → Vibes Button Icon): rendered as a CSS mask
     instead of a plain <img> so it recolors with currentColor exactly like the Font
     Awesome option does above — same ring color at rest, --accent on hover. Expects a
     simple single-color SVG per the field's own instructions in wp-admin. */
  .tp-note-svg{
    display:inline-block;width:17px;height:17px;background-color:currentColor;
    -webkit-mask-size:contain;mask-size:contain;
    -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
    -webkit-mask-position:center;mask-position:center;
  }
  .tp-bars{display:none;align-items:flex-end;gap:2px;height:16px;}
  .tp-bars span{width:3px;background:var(--accent);border-radius:1px;animation:tpBar 1s ease-in-out infinite;}
  .tp-bars span:nth-child(2){animation-delay:.15s;}
  .tp-bars span:nth-child(3){animation-delay:.3s;}
  @keyframes tpBar{0%,100%{height:4px;}50%{height:16px;}}
  #trackPlayer.playing .tp-note{display:none;}
  #trackPlayer.playing .tp-bars{display:flex;}

  .tp-panel{
    position:absolute;bottom:70px;left:0;width:290px;max-width:calc(100vw - 32px);
    max-height:calc(100vh - 110px);overflow-y:auto;
    background:rgba(12,12,12,.94);border:1px solid #262626;border-radius:12px;
    backdrop-filter:blur(8px);padding:14px;
    opacity:0;transform:translateY(12px) scale(.96);pointer-events:none;
    transition:opacity .25s ease,transform .25s ease;
  }
  #trackPlayer.open .tp-panel{opacity:1;transform:translateY(0) scale(1);pointer-events:auto;}
  .tp-close{
    position:absolute;top:10px;right:10px;width:24px;height:24px;border-radius:50%;
    display:flex;align-items:center;justify-content:center;
    background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);
    color:var(--warm-gray);cursor:pointer;font-size:11px;
    transition:background .2s ease,color .2s ease,border-color .2s ease;
  }
  .tp-close:hover{background:rgba(255,255,255,.12);color:var(--white);border-color:rgba(255,255,255,.3);}
  .tp-header{font-family:'Space Grotesk',sans-serif;font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--warm-gray-dim);margin-bottom:10px;padding-right:26px;}
  .tp-credit{font-family:'Space Grotesk',sans-serif;font-size:11.5px;letter-spacing:.02em;color:var(--warm-gray);margin:-4px 0 12px;line-height:1.4;}
  /* internal playback engine (real Audio elements, progress tracking) stays exactly as
     before — it just isn't rendered anymore. The vibe list below drives it instead. */
  .tp-list{display:none;}
  /* ---- visible vibe selector: same cards as the welcome screen, condensed into rows ---- */
  .tp-vibe-list{list-style:none;display:flex;flex-direction:column;gap:4px;}
  .tp-vibe{padding:7px 6px;border-radius:8px;transition:background .2s ease;}
  .tp-vibe:hover{background:rgba(255,255,255,.05);}
  .tp-vibe-top{display:flex;align-items:center;gap:9px;}
  .tp-vibe-play{
    width:26px;height:26px;flex:0 0 26px;border-radius:50%;border:1px solid var(--warm-gray-dim);
    background:none;color:var(--white);display:flex;align-items:center;justify-content:center;
    cursor:pointer;font-size:10px;transition:all .2s ease;
  }
  .tp-vibe.playing .tp-vibe-play{border-color:var(--v-accent,var(--accent));background:var(--v-accent,var(--accent));}
  .tp-vibe-icon{font-size:13px;color:var(--v-accent,var(--warm-gray));flex:0 0 auto;}
  .tp-vibe-label{display:inline-block;flex:1;min-width:0;}
  .tp-vibe-label .vibe-label{font-size:12.5px;font-weight:600;}
  .tp-vibe-vol{display:flex;align-items:center;gap:8px;margin-top:6px;padding-left:35px;}
  .tp-vibe-vol i{font-size:10px;color:var(--warm-gray-dim);flex:0 0 auto;}
  .tp-vibe-vol input[type="range"]{flex:1;height:3px;accent-color:var(--v-accent,var(--accent));}
  .tp-credit{
    margin-top:12px;padding-top:10px;border-top:1px solid #262626;
    font-family:'Space Grotesk',sans-serif;font-size:10px;letter-spacing:.06em;text-transform:uppercase;
    color:var(--warm-gray-dim);text-align:center;
  }

  /* ---- global particle density control ---- */
  .tp-particles{margin-top:12px;padding-top:10px;border-top:1px solid #262626;}
  .tp-particles-row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px;}
  .tp-particles-label{
    font-family:'Space Grotesk',sans-serif;font-size:11px;letter-spacing:.06em;text-transform:uppercase;
    color:var(--warm-gray-dim);display:flex;align-items:center;gap:6px;
  }
  .tp-particles-label i{color:var(--accent);font-size:11px;}
  .tp-particles-toggle{
    font-family:'Space Grotesk',sans-serif;font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
    border:1px solid var(--warm-gray-dim);background:none;color:var(--warm-gray-dim);border-radius:100px;
    padding:3px 11px;cursor:pointer;transition:all .2s ease;
  }
  .tp-particles-toggle.is-on{border-color:var(--accent);background:var(--accent);color:var(--white);}
  .tp-particles-slider-row{display:flex;align-items:center;gap:8px;}
  .tp-particles-slider-row i{font-size:10px;color:var(--warm-gray-dim);flex:0 0 auto;}
  .tp-particles-slider{flex:1;height:3px;accent-color:var(--accent);}
  .tp-particles-slider:disabled{opacity:.35;}
  .tp-particles-pct{font-family:'Space Grotesk',sans-serif;font-size:10px;color:var(--warm-gray-dim);flex:0 0 32px;text-align:right;}

  /* ---- mobile "return to top" button ---- */
  /* Only meaningful on mobile, where sections stack and the page actually scrolls
     vertically — desktop's horizontal viewport never scrolls the page, so this is
     hidden there (dirHints / the FAB's Home item already cover "back to Hero"). Stacked
     above #fab (bottom:20px, always visible) rather than sharing its spot, since this one
     only fades in after scrolling — see .visible below. */
  #backToTop{
    position:fixed;bottom:84px;right:16px;z-index:70;
    width:46px;height:46px;border:none;
    /* Same ring trick as #tpToggle: this fill is the ring color, the real dark fill is
       an inset ::before layer, since a plain `border` can't follow the pick mask. */
    background:#262626;
    color:var(--white);display:flex;align-items:center;justify-content:center;
    cursor:pointer;opacity:0;transform:translateY(10px);pointer-events:none;
    filter:drop-shadow(0 4px 12px rgba(0,0,0,.45));
    transition:opacity .25s ease,transform .25s ease,background .25s ease;
  }
  #backToTop::before{
    content:"";position:absolute;inset:1.5px;background:rgba(12,12,12,.85);
    -webkit-mask-image:var(--pick-svg); mask-image:var(--pick-svg);
    -webkit-mask-size:100% 100%; mask-size:100% 100%;
    -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
    backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
  }
  #backToTop.visible{opacity:1;transform:translateY(0);pointer-events:auto;}
  #backToTop:hover{background:var(--accent);}
  #backToTop i{position:relative;z-index:1;font-size:15px;color:var(--accent);transform:translateY(-3px);}

  /* ---- desktop: single-axis, looping, left/right only ---- */
  @media (min-width:1024px){
    /* Scoped to .bb-front (added to <body> only on the front page, see header.php) rather
       than plain `body` — this stylesheet loads on every page, and an unscoped rule here
       was locking vertical scroll on the Portfolio and Music archive pages too, since
       there's nothing on those pages to undo it. */
    body.bb-front{overflow:hidden;height:100vh;}
    #viewport{height:100vh;width:100vw;padding-top:0;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;}
    #viewport::-webkit-scrollbar{display:none;}
    #track{flex-direction:row;height:100vh;}
    /* Side padding was 96px (leaving big empty margins on wide screens since panel-inner
       used to cap out at 900px well before filling the space) — now 48px, and panel-inner
       itself goes up to 1320px, so the content column (and everything sized as a fraction
       of it, like the split image side below) actually uses the wider viewport instead of
       floating in a fixed 900px island. */
    .panel{width:100vw;height:100vh;min-height:100vh;border-bottom:none;border-right:1px solid #1f1f1f;padding:0 48px;flex:0 0 100vw;}
    .panel-inner{max-width:1320px;}
    .panel h2{font-size:clamp(3.6rem,7.2vw,6.8rem);}
    .hero-panel h1{font-size:clamp(3.6rem,7.8vw,7rem);}
    .panel .lede{font-size:1.2rem;max-width:56ch;}
    .panel .body{font-size:1.2rem;max-width:56ch;}
    .panel .imgspec{height:480px;}
    /* Image side now gets the larger share of the split (was 1.1fr text / .9fr image —
       text used to be bigger than the image itself). Reverse mirrors which physical side
       (left/right) the image lands on, but the image column stays the bigger one in both
       cases — see renderSplit/.split.reverse's order:-1 on .imgspec. */
    .split{display:grid;grid-template-columns:.8fr 1.2fr;gap:56px;align-items:center;width:100%;}
    .split.reverse{grid-template-columns:1.2fr .8fr;}
    .split.reverse .imgspec{order:-1;}
    .tile-grid{grid-template-columns:repeat(3,1fr);}
    .tile-grid .imgspec{height:250px;}
    .portfolio-tile-grid .imgspec{height:240px;}
    .mobile-index{display:none;}
    .explore-arrows{display:flex;}

    #dirHints{
      display:flex;justify-content:space-between;position:fixed;top:50%;left:0;right:0;
      transform:translateY(-50%);z-index:40;pointer-events:none;padding:0 28px;
    }
    #dirHints .arrow{
      width:40px;height:40px;border-radius:50%;border:1px solid rgba(255,255,255,.6);color:var(--white);
      display:flex;align-items:center;justify-content:center;font-size:16px;opacity:.92;
      transition:border-color .3s ease,color .3s ease,background .3s ease,transform .2s ease,opacity .3s ease;
      cursor:pointer;pointer-events:auto;background:rgba(0,0,0,.45);backdrop-filter:blur(6px);
      -webkit-backdrop-filter:blur(6px);box-shadow:0 2px 12px rgba(0,0,0,.4);margin:0;padding:0;
      appearance:none;-webkit-appearance:none;font-family:inherit;
    }
    #dirHints .arrow:hover{opacity:1;background:rgba(0,0,0,.7);border-color:var(--accent);color:var(--accent);transform:scale(1.08);}

    /* Floating action button + expanding jump menu */
    #fab{
      display:flex;align-items:center;justify-content:center;position:fixed;bottom:36px;right:36px;z-index:70;
      width:58px;height:58px;background:var(--accent);color:var(--fab-icon,var(--white));border:none;
      cursor:pointer;filter:drop-shadow(0 8px 20px rgba(0,0,0,.45));
      transition:transform .3s var(--ease),color .25s ease;
    }
    #fab:hover{transform:scale(1.08);}
    #fab i{font-size:19px;transform:translateY(-3px);transition:transform .3s var(--ease);}
    #fab.open i{transform:translateY(-3px) rotate(45deg);}

    #fabMenu{
      display:block;position:fixed;bottom:104px;right:36px;z-index:65;
      background:rgba(12,12,12,.92);border:1px solid #262626;border-radius:12px;
      backdrop-filter:blur(8px);padding:10px;min-width:220px;
      opacity:0;transform:translateY(12px) scale(.96);pointer-events:none;
      transition:opacity .25s ease, transform .25s ease;
    }
    #fabMenu.open{opacity:1;transform:translateY(0) scale(1);pointer-events:auto;}
    #fabMenu button{
      display:flex;align-items:center;gap:10px;width:100%;text-align:left;
      background:none;border:none;color:var(--white);padding:9px 10px;border-radius:6px;
      font-size:13px;cursor:pointer;
    }
    #fabMenu button:hover{background:rgba(0,57,166,.25);color:#fff;}
    #fabMenu button .n{font-family:'Space Grotesk',sans-serif;color:var(--warm-gray-dim);font-size:11px;width:20px;}
    #fabMenu hr{border:none;border-top:1px solid #262626;margin:6px 4px;}
    #fabMenu .home-item{color:var(--accent);font-weight:600;}

    /* trackPlayer's own styles now live at base scope (outside this media query) so it
       also works on mobile — this is just the desktop-only spacing tweak, matching the
       FAB's 36px inset on the opposite corner. #backToTop is desktop-irrelevant since
       the horizontal viewport here never page-scrolls. */
    #trackPlayer{bottom:36px;left:36px;}
    #backToTop{display:none;}
  }
