/* ============ FONTS ============ */
@import url('https://api.fontshare.com/v2/css?f[]=switzer@400,500,600,700&display=swap');
/* Reference site's handwritten labels are baked into images (no real font
   the browser exposes, which is why a font-identifier extension can't read
   it) — using Whisper, a thin flowing script. Whisper only ships a single
   Regular/400 weight, so these labels are set in that weight (no faux-bold)
   and sized up from the old bold-marker Kalam values since a delicate
   script reads smaller at the same pixel size. */
@import url('https://fonts.googleapis.com/css2?family=Whisper&display=swap');
/* Zero Station case only -- mono type stands in for the "system readout"
   labels (block numbers, status tags) from the original presentation deck.
   Scoped to .zs- classes below so it never leaks into the rest of the site. */
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&display=swap');

:root{
  /* was a flat neutral #fafafa; nudged toward the hero card's lilac gradient
     so the page background reads as part of the same palette — but pulled
     back lighter than an earlier, too-saturated pass, since the grayscale
     portrait photo started looking off-white/muddy against a strong tint. */
  --bg:#f9f6fc;
  --grid-line:rgba(0,0,0,0.06);
  --text-black:#141414;
  --text-gray:#828282;
  --text-desc:#545454;
  --green:#12b33f;
  --pill-bg:rgba(255,255,255,0.7);
  --ease-out-expo:cubic-bezier(0.16,1,0.3,1);
  --case-accent:#e8573f;
  /* shared tint for card/pill drop shadows — a dark, muted violet instead of
     flat black, so shadows read as soft and warm rather than harsh/inky.
     Used as rgba(var(--shadow-tint), alpha). */
  --shadow-tint:38,28,58;

  /* ============ ACCENT COLORS ============
     Two accents had been doing the same job (headings, focus rings, active
     states) with no reconciliation: --accent-site is the portfolio shell's
     own accent (nav, footer, About/Process/Experience, any persistent
     site-wide chrome), --accent-teamup is the TeamUp case study's own
     product-brand indigo (scoped to case-teamup.html content only). A
     site-wide element (like the floating back-to-top button) should always
     use --accent-site, even on a case-study page — the shell stays neutral,
     only the case's own content gets branded in the case's color.
     --accent-site was a muted grayish plum (#5c5468) — read as flat and a
     bit lifeless once the hero video introduced a real, saturated lilac
     identity elsewhere on the page; there was even a leftover ".lilac-dark"
     class already pointing at this token from before, so the name was
     always meant to be an actual lilac. Now it is one -- reusing the same
     mid-tone lilac already established in the hero-card gradient, so the
     new site-wide accent and the hero's own palette are the same color
     family instead of two unrelated purples. */
  --accent-site:#6d4f96;
  /* the site's "dark UI" color -- footer, dark CTA pills/buttons, the
     settled project-card surface, the custom cursor -- used to be flat
     #111/#000 everywhere, which is exactly the black-and-white flatness
     that stopped matching the hero's now-lilac identity. Same dark lilac
     as the hero-card's own top stop, so these dark surfaces read as part
     of the same palette instead of generic black. */
  --ink:#2a1a52;
  --accent-teamup:#4648D4;
  /* comma-separated channels of --accent-teamup, for rgba(var(--accent-teamup-rgb), alpha)
     -- same pattern as --shadow-tint below, needed anywhere the indigo is used
     at partial opacity (tinted backgrounds, soft shadows) instead of solid. */
  --accent-teamup-rgb:70,72,212;
  /* case study's lavender tint -- was written as both #E5E0FA and #e5e0fa
     in different spots, same color with no single source of truth. */
  --accent-teamup-tint:#E5E0FA;

  /* ============ RADIUS SCALE ============
     Corner radius had drifted into five near-duplicate values (12/14/16/20/24)
     all doing the same "rounded card" job with no real distinction between
     them. Consolidated to three named steps; --card-radius kept as an alias
     for --radius-md since it's the one most call sites already reference. */
  --radius-sm:12px;
  --radius-md:16px;
  --radius-lg:24px;
  --card-radius:var(--radius-md);

  /* ============ GLASS (BACKDROP-BLUR) SCALE ============
     Three real tiers were already in consistent use, just never named:
     a light touch for floating chrome (nav pill, contact pill), a medium
     blur for the portfolio's own content cards, and a heavier blur for the
     TeamUp case study's cards. Naming them stops future additions from
     picking a random number, and the two components that had drifted onto
     the wrong tier (.back-to-top, the mobile nav dropdown) are fixed to use
     --blur-chrome below instead of --blur-case. */
  --blur-chrome:5px;
  --blur-card:10px;
  --blur-case:20px;

  /* ============ SHADOW SYSTEM ============
     A handful of components had grown their own one-off box-shadow numbers
     over time (0.05 vs 0.06 opacity, 20px vs 24px blur, etc.) for no real
     visual reason — just accumulated drift. Consolidated into a small set of
     elevation levels here so every "card" of a given weight looks identical
     site-wide, and any future tweak only needs to happen in one place. */
  --shadow-xs:0 1px 2px rgba(var(--shadow-tint),0.04);
  --shadow-sm:0 1px 2px rgba(var(--shadow-tint),0.04), 0 8px 20px -8px rgba(var(--shadow-tint),0.08);
  --shadow-sm-hover:0 4px 10px rgba(var(--shadow-tint),0.05), 0 18px 32px -12px rgba(var(--shadow-tint),0.14);
  --shadow-md:0 1px 2px rgba(var(--shadow-tint),0.04), 0 12px 28px -10px rgba(var(--shadow-tint),0.08);
  --shadow-pill:0 1px 2px rgba(var(--shadow-tint),0.03), 0 10px 26px -10px rgba(var(--shadow-tint),0.08);
  --shadow-pill-hover:0 4px 10px rgba(var(--shadow-tint),0.05), 0 16px 32px -12px rgba(var(--shadow-tint),0.12);
  --shadow-photo:0 12px 24px -14px rgba(var(--shadow-tint),0.16);
  --shadow-lg:0 16px 36px -18px rgba(var(--shadow-tint),0.16);
  --shadow-lg-hover:0 22px 48px -18px rgba(var(--shadow-tint),0.22);
  --shadow-btn-dark:0 8px 18px -8px rgba(0,0,0,0.22);
  --shadow-btn-dark-hover:0 12px 24px -8px rgba(0,0,0,0.28);
  --shadow-modal:0 24px 50px -20px rgba(0,0,0,0.2), 0 8px 20px -8px rgba(0,0,0,0.1);
}

*{box-sizing:border-box;}
html,body{margin:0;padding:0;}
body{
  background:var(--bg);
  font-family:'Switzer','Switzer Placeholder',sans-serif;
  color:var(--text-black);
  cursor:none; /* custom cursor replaces native */
  overflow-x:hidden;
  /* IMPORTANT: body itself must never get a transform/animation. Any
     non-"none" transform on an ancestor makes the browser anchor
     position:fixed descendants (the custom cursor, nav pill, contact bar)
     to THAT ancestor's box instead of the real viewport — which is exactly
     why the cursor and nav used to get "stuck" near the top once the page
     scrolled. The page-sweep transition below lives on .page-fade instead,
     which wraps only the normal scrolling content. */
}
a{text-decoration:none;color:inherit;}

/* screen-reader-only: keeps a heading in the accessibility tree for correct
   document structure (one h1 per page, no skipped levels) without adding
   any visible element — used where the visual design has no room for a
   redundant on-screen heading. */
.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0;
}

/* ============ PAGE TRANSITIONS (scroll-away exit -> navigate -> slide-in entrance) ============
   Plain CSS keyframes + a click listener in script.js — no Framer, no SPA
   router. Clicking an internal link swaps in .leaving, which sweeps
   .page-fade up and out (fast, ~0.32s); script.js waits for that to finish
   before actually navigating. The next page's .page-fade then slides up
   into place from just below its resting position while fading in
   (~0.55s), so the exit of one page and the entrance of the next read as
   one continuous upward motion. .page-fade wraps everything EXCEPT the
   fixed-position UI (bg-grid, custom cursor, nav, contact bar) — those
   stay direct children of body so they're never inside a transformed
   ancestor. Works the same in every browser. */
.page-fade{
  opacity:0;
  animation:pageFadeIn .55s var(--ease-out-expo) forwards;
}
.page-fade.leaving{
  animation:pageFadeOut .32s cubic-bezier(.5,0,.75,0) forwards;
  pointer-events:none;
}
@keyframes pageFadeIn{
  from{ opacity:0; transform:translateY(26px); }
  to{ opacity:1; transform:translateY(0); }
}
@keyframes pageFadeOut{
  from{ opacity:1; transform:translateY(0); }
  to{ opacity:0; transform:translateY(-46px); }
}

/* ============ REVEAL TEXT ON SCROLL (block headings) ============
   Matched against kirayanti.space itself: its headline text animates in
   per-word from opacity:0, filter:blur(var(--blur-chrome)), translateY(10px) — a quick,
   blurred rise rather than a slow dramatic one. script.js finds each
   section heading and treats its .l1/.l2 (or the hero's .line-1/.line-2)
   child lines as separate reveal units, staggering that same blur+rise as
   each one crosses into view; headings with no line spans (case-study h2s,
   case-hero h1) reveal as a single unit. Plays once per heading. */
.reveal-line{
  opacity:0;
  filter:blur(var(--blur-chrome));
  transform:translateY(10px);
  transition:opacity .6s var(--ease-out-expo), transform .6s var(--ease-out-expo), filter .6s var(--ease-out-expo);
  will-change:opacity, transform, filter;
}
.reveal-line.reveal-in{
  opacity:1;
  filter:blur(0);
  transform:translateY(0);
}

/* ============ REVEAL ON SCROLL — SECONDARY CONTENT GROUPS ============
   Same idea, lighter treatment: on the reference site, non-headline UI
   (badges, cards, etc.) just fades and rises in — no blur, since blurring
   lots of small repeated elements at once reads as sluggish rather than
   alive. Used for project cards, case-study stat blocks, tag/tool pills,
   and accordion items; script.js staggers each group's children a little
   as the group scrolls into view. */
.reveal-fade{
  opacity:0;
  transform:translateY(16px);
  transition:opacity .5s var(--ease-out-expo), transform .5s var(--ease-out-expo);
}
.reveal-fade.reveal-in{
  opacity:1;
  transform:translateY(0);
}

@media (prefers-reduced-motion: reduce){
  .reveal-line, .reveal-fade{ transition:none; opacity:1; transform:none; filter:none; }
  /* the accordion open/close and job-card story reveal weren't covered
     here before — their open/hover states still apply instantly, just
     without the animated grow/fade. */
  .acc-collapse, .acc-collapse-inner,
  .job-card-detail, .job-card-detail-inner{ transition:none; }
  /* case-teamup gallery crossfade, back-to-top slide-in, and arrow hover --
     keep the state changes, drop the animated motion/fade between them. */
  .gallery-slide{ transition:none; }
  .back-to-top{ transition:none; }
  .gallery-arrow{ transition:none; }
  .nav-links-wrap{ transition:none; }
}

/* subtle decorative background grid, matches faint column lines on kirayanti.space */
/* a11y audit fix -- visually hidden until keyboard-focused, then jumps
   above everything (including the custom cursor at z-index:9999) so a
   keyboard user's very first Tab can skip the repeated nav straight to
   each page's real content instead of tabbing through it every time. */
.skip-link{
  position:fixed; top:-100px; left:16px; z-index:10000;
  background:var(--ink); color:#fff; font-size:14px; font-weight:600;
  padding:12px 20px; border-radius:var(--radius-sm);
  transition:top .2s ease;
}
.skip-link:focus{ top:16px; }

.bg-grid{
  position:fixed; inset:0; z-index:0; pointer-events:none;
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
  background-size: 33.33% 100%, 100% 480px;
}

/* ============ AMBIENT GLOW BLOBS ============
   Soft color bleeding from a section's own corner — placed in the About and
   "How I think" sections only (kept away from the hero, which already has
   its own lilac gradient card). Each host section needs overflow:hidden so
   the blob's off-edge portion clips instead of leaking into neighboring
   sections, and its content wrapper needs its own position+z-index so it
   stacks above the blob rather than under it.
   Deliberately NOT filter:blur() + mask-image: combining those two on the
   same element renders as one huge hard-edged smear in Chromium/WebKit (the
   blur re-blurs the mask's own alpha edge instead of just softening the
   shape) — that's the giant flat-color panel this was producing. A plain
   radial-gradient background fades to fully transparent on its own, with no
   filter/mask interaction at all, so it can't misrender that way. */
.glow-blob{ position:absolute; border-radius:50%; pointer-events:none; z-index:0; }
.glow-blob--lilac{
  width:900px; height:900px; opacity:0.35;
  background:radial-gradient(circle, #B8A6F0 0%, rgba(184,166,240,0) 70%);
}
.glow-blob--indigo{
  width:800px; height:800px; opacity:0.18;
  background:radial-gradient(circle, var(--accent-teamup) 0%, rgba(var(--accent-teamup-rgb),0) 70%);
}

/* ============ CUSTOM CURSOR ============
   A small dot that follows the pointer everywhere. The old site-wide
   hover-into-a-pill-with-text behavior stays retired (it read as noisy over
   the nav and buttons) — but the project cards get a scoped version: a text
   chip that appears next to the dot only while hovering a card (see
   .cursor-label below + the per-card listeners in script.js). */
.cursor{
  position:fixed; top:0; left:0; z-index:9999; pointer-events:none;
  transform:translate(-50%,-50%);
  display:flex; align-items:center; justify-content:center;
  background:var(--ink); border-radius:999px;
  padding:6px;
}
.cursor .dot{ width:6px;height:6px;border-radius:50%;background:#fff; animation:pulse 1.2s ease-in-out infinite; }
@keyframes pulse{ 0%,100%{opacity:.35; transform:scale(.8);} 50%{opacity:1; transform:scale(1);} }
.cursor-label{
  position:absolute; top:50%; left:100%; margin-left:10px;
  transform:translateY(-50%) scale(0.85);
  background:var(--ink); color:#fff; font-size:12px; font-weight:600; white-space:nowrap;
  padding:8px 14px; border-radius:999px;
  opacity:0; transition:opacity .25s ease, transform .25s var(--ease-out-expo);
}
.cursor-label.visible{ opacity:1; transform:translateY(-50%) scale(1); }
@media (hover:none){ .cursor{display:none;} body{cursor:auto;} }

/* ============ NAV ============ */
.nav-wrap{
  position:fixed; top:24px; left:0; right:0; z-index:100;
  display:flex; justify-content:center;
  /* this div spans the full viewport width just to center the nav pill --
     without this, its empty flex gutters on either side of the pill still
     hit-test as clickable at z-index:100, silently eating clicks/hovers on
     anything positioned underneath them (e.g. the hero video's day/night
     toggle, top-right). pointer-events:auto on nav below re-enables it for
     the pill itself and everything inside it. */
  pointer-events:none;
}
nav{
  pointer-events:auto;
  position:relative;
  display:flex; align-items:center; gap:8px;
  /* glass values measured on kirayanti.space's nav: blur(var(--blur-chrome)), 50% white, 32px
     radius — plus a hairline border + soft shadow so it still reads as a
     separate surface against a light page background. */
  background:rgba(255,255,255,0.5);
  backdrop-filter:blur(var(--blur-chrome)); -webkit-backdrop-filter:blur(var(--blur-chrome));
  border:1px solid rgba(255,255,255,0.8);
  border-radius:32px;
  box-shadow:var(--shadow-pill);
  padding:12px;
  transition:padding .5s var(--ease-out-expo), gap .5s var(--ease-out-expo);
}
.nav-profile{
  display:flex; align-items:center; gap:8px;
  padding:4px 14px 4px 4px; border-radius:999px;
  font-weight:600; font-size:13px; white-space:nowrap;
}
/* was a circle-cropped placeholder avatar (dicebear) -- the real mark is a
   flat badge, not a photo, so object-fit:contain (no cropping) and no
   circle mask/gray fallback bg read as a logo instead of an avatar photo. */
.nav-profile img{width:28px;height:28px;object-fit:contain;}
/* the grid-template-columns 0fr -> 1fr trick (same technique as the accordion
   below) — animates a genuinely fluid width instead of guessing a max-width,
   so the reveal doesn't stutter or force link text to wrap mid-transition.
   Default state is EXPANDED (links visible) — the nav only switches to the
   collapsed hover-to-reveal pill once script.js adds .nav-compact, which
   happens after scrolling past the hero. See the "NAV: expanded on load..."
   block in script.js. */
.nav-links-wrap{
  display:grid; grid-template-columns:1fr;
  transition:grid-template-columns .5s var(--ease-out-expo);
}
nav.nav-compact .nav-links-wrap{ grid-template-columns:0fr; }
nav.nav-compact:hover .nav-links-wrap, nav.nav-compact:focus-within .nav-links-wrap{ grid-template-columns:1fr; }
.nav-links{
  display:flex; align-items:center; gap:2px;
  min-width:0; overflow:hidden; white-space:nowrap;
  opacity:1;
  transition:opacity .3s ease;
}
nav.nav-compact .nav-links{ opacity:0; transition-delay:0s; }
nav.nav-compact:hover .nav-links, nav.nav-compact:focus-within .nav-links{ opacity:1; transition-delay:.08s; }
.nav-links a{
  font-size:13px; font-weight:500; padding:8px 12px; border-radius:999px;
  white-space:nowrap;
  transition:background .2s, color .2s;
}
/* :not(.nav-connect) matters here: without it, this selector's extra type
   match (a) out-specifies .nav-connect:hover below and was overriding its
   background with this near-transparent gray on hover — which is why the
   black Connect button used to wash out almost invisible when hovered. */
.nav-links a:not(.nav-connect):hover{ background:rgba(0,0,0,0.06); }
.nav-connect{
  font-size:13px; font-weight:600; background:var(--ink); color:#fff;
  padding:8px 16px; border-radius:999px; margin-left:2px; white-space:nowrap;
  display:inline-flex; align-items:center;
  transition:transform .3s var(--ease-out-expo), background .2s;
}
.nav-connect:hover{ background:var(--ink); transform:translateY(-1px); }

/* ============ LETTER ROLL ON HOVER (Connect button) ============
   Same idea as the Framer community "LetterRollMenu" component: each
   character sits in a one-line-tall mask with an identical duplicate
   stacked directly beneath it; hovering slides the whole stack up by one
   line so the duplicate rolls into view where the original was. Built by
   script.js, which wraps the button's text into this structure on load. */
.letter-roll{ display:inline-flex; }
.lr-mask{ display:inline-block; overflow:hidden; height:1em; line-height:1em; vertical-align:top; }
.lr-inner{ display:flex; flex-direction:column; transition:transform .45s var(--ease-out-expo); }
.lr-copy{ display:block; height:1em; line-height:1em; }
.nav-connect:hover .lr-inner{ transform:translateY(-1em); }

/* ============ MOBILE NAV ============
   .nav-links-wrap only ever revealed via :hover/:focus-within (see the
   grid-template-columns trick above) -- neither fires on a touch tap, so
   once nav-compact kicked in after scrolling, phone/tablet users had no way
   to reach Projects/About/Process/Experience/Connect at all. Below 700px
   this swaps that hover-reveal for a real tap-to-open dropdown (toggled by
   script.js adding/removing .nav-open on #siteNav), and the desktop
   scroll-compact behavior is switched off entirely so it can't fight it. */
.nav-toggle{
  position:relative;
  display:none; width:40px; height:40px; border-radius:50%; flex-shrink:0;
  align-items:center; justify-content:center; border:none; background:transparent;
  color:#111; cursor:pointer;
}
/* a11y audit fix -- invisible hit-area extension so the actual tappable
   area is 44x44 even though the visible circle stays 40x40. */
.nav-toggle::before{ content:''; position:absolute; inset:-2px; }
.nav-toggle svg{ width:20px; height:20px; }
.nav-toggle:focus-visible{ outline:2px solid var(--accent-site); outline-offset:2px; }
@media (max-width:700px){
  .nav-toggle{ display:flex; }
  nav.nav-compact .nav-links-wrap{ grid-template-columns:1fr; }
  nav.nav-compact .nav-links{ opacity:1; }
  .nav-links-wrap{
    position:absolute; top:calc(100% + 10px); right:0; width:200px; display:block;
    opacity:0; visibility:hidden; transform:translateY(-8px); pointer-events:none;
    transition:opacity .25s ease, transform .25s ease, visibility .25s;
  }
  nav.nav-open .nav-links-wrap{ opacity:1; visibility:visible; transform:translateY(0); pointer-events:auto; }
  .nav-links{
    flex-direction:column; align-items:stretch; gap:2px; white-space:normal;
    background:rgba(255,255,255,0.9); backdrop-filter:blur(var(--blur-chrome)); -webkit-backdrop-filter:blur(var(--blur-chrome));
    border:1px solid rgba(255,255,255,0.95); border-radius:var(--radius-md);
    box-shadow:var(--shadow-lg); padding:8px;
  }
  /* min-height + flex centering (not just padding) guarantees the real 44px
     touch target regardless of the font's rendered line-height. */
  .nav-links a{ min-height:44px; display:flex; align-items:center; padding:0 14px; border-radius:var(--radius-sm); text-align:left; }
  .nav-connect{ margin:4px 0 0; justify-content:center; }
}

/* ============ HERO VIDEO (day/night ambient loop) ============
   New first hero, sitting above .hero-card below: a looping video of the
   same "designer's desk" scene, day or night depending on the visitor's
   real local time in Europe/Berlin (with a manual sun/moon override, kept
   in memory only for the current tab -- no persistence needed). Built as
   two stacked <video> elements that crossfade via opacity, never swapping
   `src` on a single element (that causes a load-flash). The clock and
   music-hotspot position values below were measured directly off the
   actual rendered video frames (not guessed) -- see script.js for the
   day/night detection, clock, and hotspot logic. */
.hero-video-card{
  position:relative; z-index:1;
  /* shifts the whole block up so its top 40px sits above the document's
     scroll-0 position (permanently off-screen) instead of being clipped by
     an inner overflow -- on a real 1920x1080 window the video renders near
     1080px tall, taller than the actual browser viewport (browser chrome
     eats some of the screen's own 1080px), so this trades a sliver of the
     top (mostly plain wall/ceiling) for a bit more of the desk area fitting
     in view without scrolling. */
  margin:-40px 0 0;
  border-radius:0;
  overflow:hidden;
}
.hero-video-wrap{
  position:relative;
  width:100%;
  aspect-ratio:1920/1080;
  overflow:hidden;
  background:#0c0715; /* shows briefly only if a frame hasn't painted yet */
}
.hero-video{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  opacity:0;
  transition:opacity 0.8s ease;
}
.hero-video.active{ opacity:1; }
/* fades the video's own bottom edge into .hero-card's own top color
   (#2a1a52, see the HERO block below) so the seam between video and the
   next block disappears instead of cutting hard. */
.hero-video-wrap::after{
  content:'';
  /* kept shorter and pushed toward the very bottom edge on purpose -- taller
     versions of this overlay were darkening the desk/mug area in the middle
     of the frame; staying near-transparent until ~50% down (roughly desk
     level) keeps the actual scene readable and only fades to solid right at
     the seam with the next block. */
  position:absolute; left:0; right:0; bottom:0; height:48%; z-index:4;
  background:linear-gradient(to bottom, rgba(42,26,82,0) 0%, rgba(42,26,82,0.1) 45%, rgba(42,26,82,0.55) 78%, #2a1a52 100%);
  pointer-events:none;
}

.daynight-toggle{
  position:absolute; top:60px; right:20px; z-index:10;
  display:flex; align-items:center; justify-content:center;
  width:40px; height:40px; border-radius:50%; padding:0;
  border:1px solid rgba(255,255,255,0.3);
  background:rgba(20,10,40,0.45);
  backdrop-filter:blur(var(--blur-chrome)); -webkit-backdrop-filter:blur(var(--blur-chrome));
  color:#fff; cursor:pointer;
  transition:background .2s ease, transform .2s ease;
}
.daynight-toggle:hover{ background:rgba(20,10,40,0.65); transform:translateY(-1px); }
/* a11y audit fix -- 44x44 hit area (already position:absolute, so this
   just extends the invisible click/tap region without resizing the
   visible circle). Mobile's smaller 34x34 circle gets a bigger inset. */
.daynight-toggle::before{ content:''; position:absolute; inset:-2px; }
.daynight-toggle svg{ width:18px; height:18px; }
/* default (day) shows the moon -- tapping it is an invitation to switch
   *to* night; .is-night (added by script.js) flips which icon shows. */
.daynight-toggle .icon-sun{ display:none; }
.daynight-toggle.is-night .icon-sun{ display:block; }
.daynight-toggle.is-night .icon-moon{ display:none; }

.desk-clock{
  /* color sampled from the neon triangle wall light in the actual video
     frame (bright pink-lilac), so the clock reads as another practical
     light fixture in the room instead of a random UI accent. Sized up and
     bolded to look like a real wall-mounted digit display, not a caption. */
  position:absolute; top:30%; left:71%; z-index:5;
  font-family:'IBM Plex Mono', monospace;
  font-weight:800;
  font-size:clamp(30px, 4.4vw, 52px);
  letter-spacing:2px;
  color:#faf3ff;
  text-shadow:0 0 6px rgba(255,255,255,0.9), 0 0 20px rgba(230,182,238,0.9), 0 0 36px rgba(230,182,238,0.55);
  pointer-events:none;
}

/* A calm little "now playing" card sized to sit safely inside the PC
   screen's own rectangle -- jellyfish "album art" + title, no motion.
   Earlier passes had it overflowing the screen bounds and pulsing/bouncing
   (equalizer + glow) whenever playing; both are gone now -- state only
   shows via the play/pause icon swap, no animation. */
.music-hotspot{
  position:absolute; left:78%; top:calc(57% - 14px); width:10%; height:36%;
  border:none; padding:0; background:transparent;
  cursor:pointer; z-index:6;
}
.mh-card{
  width:100%; height:100%;
  display:flex; flex-direction:column; align-items:stretch; justify-content:flex-start;
  gap:8px;
  padding:8px;
  border-radius:10px;
  background:linear-gradient(165deg, rgba(109,79,150,0.6), rgba(20,10,40,0.72));
  border:1px solid rgba(228,217,255,0.28);
  backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
  box-shadow:0 10px 26px -10px rgba(0,0,0,0.55);
  transition:background .2s ease;
}
.music-hotspot:hover .mh-card,
.music-hotspot:focus-visible .mh-card{
  background:linear-gradient(165deg, rgba(109,79,150,0.72), rgba(20,10,40,0.8));
}

/* "album art" swatch -- jellyfish illustration standing in for cover art,
   with a small play/pause badge tucked in the corner instead of a big
   icon sitting on top of it. */
.mh-art{
  position:relative;
  width:100%; flex:1 1 auto; min-height:0; border-radius:9px;
  background:linear-gradient(135deg, #b9a0dd, #6d4f96);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,0.2), inset 0 -6px 14px rgba(0,0,0,0.18);
  display:flex; align-items:center; justify-content:center;
  overflow:hidden;
}
.mh-jelly-img{ width:100%; height:100%; object-fit:cover; object-position:50% 30%; display:block; }

.mh-play-badge{
  position:absolute; top:50%; left:50%; transform:translate(-50%, -50%);
  width:34%; aspect-ratio:1;
  border-radius:50%; background:rgba(20,10,40,0.6);
  border:1px solid rgba(255,255,255,0.25);
  display:flex; align-items:center; justify-content:center;
}
.mh-play-badge svg{ width:44%; height:auto; color:#fff; display:block; }
.mh-play-badge .icon-pause{ display:none; }
.music-hotspot.playing .mh-play-badge .icon-play{ display:none; }
.music-hotspot.playing .mh-play-badge .icon-pause{ display:block; }

.mh-title{
  /* flex-shrink:0 -- without this the title was losing a fight for space
     against .mh-art's flex:1 and getting squeezed shorter than its own
     line-height, clipping the descenders off the bottom of the text. */
  flex:0 0 auto;
  font-size:clamp(8px, 0.85vw, 12px); font-weight:600; color:#F3ECFF;
  text-align:center; line-height:1.4;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:100%;
}

.now-playing{
  position:absolute; bottom:16px; right:16px; z-index:6;
  display:flex; align-items:center; gap:6px;
  background:rgba(20,10,40,0.6); color:#E4D9FF;
  font-size:11px; padding:6px 12px; border-radius:20px;
  opacity:0; transition:opacity .3s ease; pointer-events:none;
}
.now-playing.visible{ opacity:1; }
.now-playing .bar{ width:2px; background:#E4D9FF; border-radius:1px; height:4px; animation:barBounce 1s ease-in-out infinite; }
.now-playing .bar:nth-child(2){ animation-delay:.15s; }
.now-playing .bar:nth-child(3){ animation-delay:.3s; }
@keyframes barBounce{ 0%,100%{ height:4px; } 50%{ height:10px; } }

@media (max-width:860px){
  .daynight-toggle{ width:34px; height:34px; top:54px; right:14px; }
  .daynight-toggle::before{ inset:-5px; }
  .daynight-toggle svg{ width:16px; height:16px; }
}

/* ============ HERO ============
   Second hero now, right under the video. Top edge picks up the video's own
   dark fade-out (#2a1a52) so the seam is invisible, peaks at a genuinely
   rich lilac (#b9a0dd) around 24%, then eases down through several soft
   steps into off-white by the bottom -- more, closer-spaced stops than a
   simple 2-color fade so the whole transition reads as gradual, not a hard
   cut. The rich-purple peak is kept narrow and early on purpose: .hero-copy
   text (h1.headline .line-1 in --accent-site, a mid-tone plum) drops under
   4.5:1 contrast against a saturated purple that close to its own color --
   .hero's content sits vertically centered further down, past 36% where the
   gradient's already back to a safe, light tone. No more rounded card
   treatment (retired alongside the video hero's), full width instead. */
.hero-card{
  position:relative; z-index:1;
  margin:0;
  border-radius:0;
  overflow:hidden;
  background:linear-gradient(180deg, #2a1a52 0%, #6d4f96 12%, #b9a0dd 22%, #d3c2e8 32%, #e3d8f1 45%, #eee6f7 60%, #f5f0fa 78%, #faf8fc 100%);
}
.hero{
  position:relative; z-index:1;
  max-width:1200px; margin:0 auto;
  padding:150px 40px 90px;
  display:grid; grid-template-columns:1fr 1fr; gap:40px; align-items:center;
  min-height:80vh;
}
.badge{
  display:inline-flex; align-items:center; gap:8px;
  background:#fff; border-radius:999px; padding:7px 14px 7px 10px;
  font-size:12px; font-weight:600; color:var(--text-black);
  box-shadow:var(--shadow-xs);
  margin-bottom:24px;
}
.badge .dot{width:8px;height:8px;border-radius:50%;background:var(--green); box-shadow:0 0 0 3px rgba(18,179,63,0.15);}
.badge .txt{ display:inline-block; }

h1.headline{
  font-size:68px; font-weight:500; letter-spacing:-1.7px; line-height:1.12;
  margin:0 0 28px;
}
h1.headline .line-1{ color:var(--accent-site); display:block; }
h1.headline .line-2{ color:var(--text-black); font-weight:600; display:block; }

.hero-copy p{ margin:0; font-size:21px; line-height:1.45; letter-spacing:-0.36px; }
.hero-copy .lead{ font-weight:600; color:var(--text-black); margin-bottom:4px; }
.hero-copy .sub{ font-weight:500; color:var(--text-desc); max-width:420px; }

/* fanned stack visual (right side of hero) */
.stack-wrap{ position:relative; height:340px; }
.stack-card{
  position:absolute; top:50%; left:50%; width:280px; height:220px;
  border-radius:var(--radius-sm); overflow:hidden;
  box-shadow:var(--shadow-lg);
  border:1px solid rgba(255,255,255,0.4);
  background-size:cover; background-position:center;
}

/* ============ TICKER ============ */
/* .ticker-wrap has no z-index stacking context issue, but it also has no
   background of its own — so the fixed .bg-grid column lines (position:fixed,
   z-index:0, running the full page height) show straight through the strip,
   reading as stray vertical stripes crossing the ticker text. Giving it an
   opaque page-bg fill covers those lines specifically inside this band; the
   existing mask-image still fades that fill (and the text) out at the two
   edges, so it reads as one soft-edged bar, not a hard-edged box. */
.ticker-wrap{
  position:relative; z-index:1; overflow:hidden; background:var(--bg);
  padding:18px 0; border-top:1px solid rgba(0,0,0,0.06); border-bottom:1px solid rgba(0,0,0,0.06);
  mask-image:linear-gradient(to right, transparent, black 8%, black 92%, transparent);
  -webkit-mask-image:linear-gradient(to right, transparent, black 8%, black 92%, transparent);
}
.ticker-track{
  display:flex; gap:28px; white-space:nowrap; width:max-content;
  animation:scrollTicker 32s linear infinite;
  font-size:13px; font-weight:600; letter-spacing:0.04em; color:rgba(0,0,0,0.32); text-transform:uppercase;
}
/* explicit inline-block + line-height:1 so this glyph can never render taller
   than the surrounding text — the previous ✦ star character fell back to a
   symbol font Switzer doesn't ship, and that fallback's tall glyph box made
   a couple of the separators show up as a thin vertical stripe rather than a
   small mark in line with the words. */
.ticker-track span.sep{ display:inline-block; line-height:1; vertical-align:middle; color:rgba(0,0,0,0.18); }
@keyframes scrollTicker{ from{transform:translateX(0);} to{transform:translateX(-50%);} }

/* ============ WORK / GRID SECTION ============ */
/* same full-width/inner split as .about — this section holds the bottom
   half of the glow blob that continues into .about right below it, so it
   needs the same overflow:hidden host + centered inner column. */
.work{ position:relative; z-index:1; padding:128px 0 160px; overflow:hidden; }
.work-inner{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:0 40px; }
/* heading uses the same white-text + mix-blend-mode:exclusion trick measured on
   kirayanti.space: on a #fafafa background it reads as near-black, but would
   invert on a dark/photo backdrop too. */
.work-title-wrap{ isolation:isolate; margin-bottom:48px; }
/* sized to match the About section's heading below it, so the two read as
   the same two-tone lilac/black heading rhythm used across the page */
.work-kicker{
  font-size:56px; font-weight:500; letter-spacing:-1.68px; line-height:1.05;
  color:var(--text-black); margin:0;
}
.work-title{
  font-size:64px; font-weight:500; letter-spacing:-1.92px; line-height:1;
  color:#fff; mix-blend-mode:exclusion; display:inline-flex; align-items:center; gap:14px;
  margin:0;
}
.work-title .spark{ font-size:0.8em; }
.view-more-row{ display:flex; justify-content:center; margin-top:48px; }
.view-more{
  display:inline-flex; align-items:center; gap:6px; white-space:nowrap;
  font-size:13px; font-weight:600; color:var(--text-desc);
  background:#fff; padding:12px 22px; border-radius:999px;
  box-shadow:var(--shadow-pill);
  transition:color .2s ease, transform .25s var(--ease-out-expo), box-shadow .25s ease;
}
.view-more:hover{ color:var(--text-black); transform:translateY(-2px); box-shadow:var(--shadow-pill-hover); }

/* track spans from hero stack to end of grid — cards are absolutely positioned inside it */
#track{ position:relative; }
.grid-slots{
  display:grid; grid-template-columns:1fr 1fr; gap:24px;
}
@media (max-width:860px){ .grid-slots{ grid-template-columns:1fr; } }
.slot{ visibility:hidden; } /* reserves layout space, real card floats above it */
.slot-img{ width:100%; aspect-ratio:4/3; border-radius:var(--card-radius); }

.proj-card{
  position:absolute; border-radius:var(--card-radius); z-index:1;
  will-change:transform,top,left,width,height;
  opacity:0; transition:opacity .4s ease;
}
.proj-card.ready{ opacity:1; }
/* .proj-card-surface / .wcard-surface hold the actual visible card box
   (background, border, rounded-corner clipping for the thumbnail) — kept
   separate from .proj-card/.wcard itself so the backlight glow (a sibling
   element, see .card-glow below) can sit fully behind every card instead of
   just behind its own — a per-card pseudo-element approach was tried first,
   but its stacking was tied to that one card's own z-index, so its bleed
   spilled ON TOP of whichever neighboring card happened to sit lower in the
   stack. One shared glow element behind the whole grid fixes that. */
.proj-card-surface, .wcard-surface{
  position:absolute; inset:0; z-index:1; border-radius:inherit; overflow:hidden;
  background:var(--ink); border:1.5px solid #fff; box-shadow:var(--shadow-lg);
  transition:box-shadow .3s ease;
  --x:50%; --y:50%;
}
.proj-card.settled:hover .proj-card-surface{ box-shadow:var(--shadow-lg-hover); }
.proj-card .thumb, .wcard .thumb{
  position:absolute; inset:0; background-size:cover; background-position:center;
  transition:transform .5s var(--ease-out-expo);
}
.proj-card.settled:hover .thumb{ transform:scale(1.045); }

/* Glow Bento-style hover (referenced from Framer's "Glow Bento" component,
   rebuilt from a working from-scratch reference). Two layers, same as the
   reference: */

/* 1) ONE shared blurred circle behind the whole grid, following the raw
   cursor position anywhere inside the grid's bounding area — including the
   gaps between cards. A solid tint + heavy blur (no radial-gradient needed,
   the blur itself creates the falloff) positioned via left/top + a
   translate(-50%,-50%) so it's always centered exactly on the cursor.
   z-index is below every card, so it only ever shows through the gaps
   around whichever card(s) it's near, never on top of one. Color matches
   the .glow-blob near the footer's principles section. */
.card-glow{
  position:absolute; z-index:0; pointer-events:none;
  border-radius:50%;
  background:#B8A6F0;
  filter:blur(60px);
  opacity:0; transition:opacity .3s ease;
  transform:translate(-50%,-50%);
}
.card-glow.visible{ opacity:1; }
/* Opacity while visible is actually driven inline (see wireGridGlow in
   script.js) — it fades the glow out as the cursor nears any edge of the
   grid zone (mostly relevant at the top, where the "Featured Projects"
   heading/paragraph sits just above), so the blur dies down before it ever
   gets close enough to wash out that text, rather than being hard-clipped. */

/* 2) a border ring on the individual card the cursor is actually over,
   tracing just the edge nearest the pointer. Lives on -surface itself
   (inset:0, no negative offset) so it never bleeds past its own card no
   matter how it's stacked against a neighbor — only the shared .card-glow
   above is allowed to bleed between cards. --x/--y are the LOCAL cursor
   position within that one card (separate from --gx/--gy on the grid glow),
   updated on mousemove in script.js. */
.proj-card-surface::before, .wcard-surface::before{
  content:''; position:absolute; inset:0; z-index:2; border-radius:inherit;
  padding:1.5px; pointer-events:none;
  background:radial-gradient(500px circle at var(--x) var(--y), rgba(184,166,240,0.7), transparent 40%);
  -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor;
  mask-image:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite:exclude;
  opacity:0; transition:opacity .3s ease;
}
.proj-card:hover .proj-card-surface::before, .wcard:hover .wcard-surface::before{ opacity:1; }

/* title + tags sit directly on the image, top of the card — a scrim keeps
   them legible over whatever's underneath without needing a fixed dark photo.
   Shared with .wcard (work.html's static archive grid uses the exact same
   overlay treatment as the homepage's animated cards). */
.proj-card .card-scrim, .wcard .card-scrim{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  /* dark-violet tint read as too close to black/flat shadow -- switched to
     an actually-lilac tone (accent-site's rgb) so the legibility scrim
     reads as a color choice, not a shadow. Opacity at the top raised from
     0.75 to 0.88 (a11y audit fix) -- 0.75 computed to ~3.7:1 for the white
     title/tags text against a light thumbnail, under the 4.5:1 minimum;
     0.88 clears it with margin (~4.9:1) even worst-case. */
  background:linear-gradient(180deg, rgba(109,79,150,0.88) 0%, rgba(109,79,150,0.7) 30%, rgba(109,79,150,0) 62%);
}
.proj-card .card-top, .wcard .card-top{
  position:absolute; top:0; left:0; right:0; z-index:2;
  display:flex; align-items:flex-start; justify-content:space-between; gap:12px;
  padding:18px 20px;
}
.card-title{
  font-size:20px; font-weight:600; letter-spacing:-0.3px; line-height:1.2;
  color:#fff; margin:0;
}
.card-tags{ display:flex; gap:6px; flex-shrink:0; }
.card-tags span{
  font-size:11px; font-weight:600; letter-spacing:0.01em; color:#fff; white-space:nowrap;
  background:rgba(255,255,255,0.16); border:1px solid rgba(255,255,255,0.28);
  backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
  padding:5px 11px; border-radius:999px;
}
@media (max-width:560px){
  .card-title{ font-size:17px; }
  .card-top{ padding:14px 16px; }
}

/* ============ ABOUT SECTION ============ */
/* .about is full-width (no max-width) so its glow-blob positions/clips
   relative to the actual browser viewport, not the narrower 1200px content
   column — that column now lives in .about-inner instead. Without this
   split, the blob was clipped at the content column's edge, which reads as
   a hard rectangular cutoff floating in the middle of the page rather than
   bleeding off the true edge of the screen. */
.about{ position:relative; z-index:1; padding-bottom:140px; overflow:hidden; }
.about-inner{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:0 40px; }
/* flex (not inline-block) spans avoid a letter-spacing rendering quirk where
   negative letter-spacing on an atomic inline-level box gets distributed
   symmetrically around the text, visually shifting it left of the box's
   true edge. margin-left matches .about-grid's 320px photo column + 64px
   gap, so the heading lines up with the bio paragraphs, not the photo. */
.about-heading{
  position:relative; z-index:1;
  font-size:56px; font-weight:500; letter-spacing:-1.68px; line-height:1.05; margin:0 0 32px 384px;
  display:flex; flex-wrap:wrap; align-items:baseline; gap:0.28em;
}
.about-heading .l1{ color:var(--accent-site); }
.about-heading .l2{ color:#2b2b2b; }

.about-grid{ position:relative; z-index:1; display:grid; grid-template-columns:320px 1fr; gap:64px; align-items:start; }
.about-photo{ width:100%; aspect-ratio:320/360; object-fit:cover; object-position:center top; border-radius:var(--radius-md); filter:grayscale(1); display:block; margin-bottom:20px; }

.about-id-row{ margin-bottom:28px; }
/* baseline-aligned so "Since 2018" sits flush against the bottom of the
   name text, not the shorter role line below it — flex's align-items:baseline
   lines up the two text runs' baselines regardless of their different font sizes. */
.about-id-top{ display:flex; align-items:baseline; gap:20px; }
.about-id h3{ font-size:22px; font-weight:500; letter-spacing:-0.66px; margin:0 0 4px; }
.about-id p{ font-size:14px; font-weight:600; letter-spacing:-0.14px; color:var(--text-desc); margin:0; }
.about-since{
  position:relative; margin-left:auto; padding-left:16px;
  font-size:16px; font-weight:600; color:var(--text-desc); white-space:nowrap;
}
/* absolutely positioned + translateY(-50%) instead of vertical-align:middle —
   vertical-align centers against the x-height of the text, which reads low
   next to the digits in "2018"; this centers on the box's true geometry. */
.about-since .dot{
  position:absolute; left:0; top:50%; transform:translateY(-50%);
  width:8px; height:8px; border-radius:50%; background:#141414;
}
.about-id p.about-location{ font-size:13px; font-weight:500; color:var(--text-desc); margin:2px 0 0; }

.where-label{ font-size:18px; font-weight:500; margin:0 0 24px; }
/* hover the stack to unfold it: collapsed cards peek out from behind one
   another, expanded they spread into a vertical list. everything animates
   via top/transform/opacity on the (always-absolute) cards + a height
   transition on the container, so nothing pops or jumps. */
.exp-stack{ position:relative; height:79px; transition:height .5s var(--ease-out-expo); }
.exp-stack.expanded{ height:212px; }
.exp-card{
  position:absolute; left:0; right:0; height:64px; background:#fff; border-radius:var(--radius-sm);
  box-shadow:var(--shadow-sm);
  padding:0 16px; display:flex; align-items:center; justify-content:space-between;
  transition:top .5s var(--ease-out-expo), transform .5s var(--ease-out-expo), opacity .4s ease;
}
.exp-card .role h4{ font-size:14px; font-weight:600; margin:0 0 6px; }
.exp-card .role p{ font-size:12px; font-weight:600; color:var(--text-desc); margin:0; }
.exp-card .date{ font-size:12px; font-weight:600; color:var(--text-desc); white-space:nowrap; }
.exp-card[data-i="0"]{ top:0; z-index:3; }
.exp-card[data-i="1"]{ top:8px; transform:scale(0.96); opacity:.7; z-index:2; }
.exp-card[data-i="2"]{ top:15px; transform:scale(0.92); opacity:.4; z-index:1; }
.exp-stack.expanded .exp-card[data-i="0"]{ top:0; transform:none; opacity:1; }
.exp-stack.expanded .exp-card[data-i="1"]{ top:74px; transform:none; opacity:1; }
.exp-stack.expanded .exp-card[data-i="2"]{ top:148px; transform:none; opacity:1; }

/* "currently listening" widget — replaces the job-history stack on the
   standalone About page (that stays homepage-only, matching the reference
   site's split between the two). Now a real Spotify track embed, so this
   wrapper just controls width/spacing — Spotify's own iframe already draws
   its rounded dark card, art, and play controls, so no extra card chrome
   (background/padding/shadow) here or it'd look like a card inside a card. */
.listening-card{ width:100%; line-height:0; }
.listening-card iframe{ display:block; }

.about-bio p{ font-size:21px; line-height:1.4; font-weight:400; color:var(--text-black); margin:0 0 22px; }
.about-bio p b{ font-weight:700; }

.about-sign-row{ display:flex; align-items:center; justify-content:space-between; margin-top:36px; flex-wrap:wrap; gap:20px; }
.signature{ font-family:'Whisper','Switzer',cursive; font-size:66px; font-weight:400; color:#141414; transform:rotate(-2deg); line-height:1; }
.learn-btn{
  display:inline-flex; align-items:center; gap:8px; background:var(--ink); color:#fff;
  font-size:13px; font-weight:600; padding:12px 20px; border-radius:999px; white-space:nowrap;
  transition:transform .3s var(--ease-out-expo), background .2s;
}
.learn-btn:hover{ background:#333; transform:translateY(-2px); }

@media (max-width:860px){
  .about-grid{ grid-template-columns:1fr; }
  .about-heading{ font-size:36px; margin-left:0; }
}

/* ============ "BEYOND THE SCREEN" PERSONAL GALLERY ============
   On the reference site this strip is always quietly in motion, not a
   scroll-it-yourself carousel — same idea as the tools ticker up in the
   hero (.ticker-wrap/.ticker-track): an outer frame clips + fades the
   edges, an inner flex row (photos listed twice in the HTML) scrolls
   itself left on a linear infinite loop. Full color on purpose — the
   portrait above is grayscale, this is the "off the clock" contrast. */
.beyond-section{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:60px 0 140px; }
.beyond-kicker{
  font-family:'Whisper','Switzer',cursive; font-size:46px; font-weight:400; line-height:1.2; color:var(--text-black);
  display:block; transform:rotate(-2deg); margin:0 40px 16px;
}
.beyond-label{ font-size:18px; font-weight:500; color:var(--text-black); max-width:520px; margin:0 40px 24px; }
.beyond-gallery-wrap{
  overflow:hidden;
  /* the hover lift on .beyond-photo (-4px) had no room to breathe -- overflow:hidden
     clipped it flush against this wrap's own top edge, so the photo looked like it
     got sliced off instead of lifting. 12px of top padding pushes the whole
     carousel down and gives the lift somewhere to go before it hits the clip line. */
  padding-top:12px;
  mask-image:linear-gradient(to right, transparent, black 6%, black 94%, transparent);
  -webkit-mask-image:linear-gradient(to right, transparent, black 6%, black 94%, transparent);
}
.beyond-gallery{
  display:flex; gap:16px; width:max-content; padding:0 40px;
  animation:scrollBeyond 32s linear infinite;
}
.beyond-gallery:hover{ animation-play-state:paused; }
@keyframes scrollBeyond{ from{ transform:translateX(0); } to{ transform:translateX(-50%); } }
.beyond-photo{
  flex:0 0 auto;
  width:220px; aspect-ratio:3/4; border-radius:var(--radius-md);
  background-size:cover; background-position:center;
  box-shadow:var(--shadow-photo);
  transition:transform .35s var(--ease-out-expo);
}
.beyond-photo:hover{ transform:translateY(-4px); }
@media (max-width:860px){
  .beyond-section{ padding-top:20px; }
  .beyond-photo{ width:160px; }
}
@media (prefers-reduced-motion: reduce){
  .beyond-gallery{ animation:none; overflow-x:auto; }
}

/* ============ STACK / TOOLS SECTION ============ */
.stack-section{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:20px 40px 140px; }
.stack-grid{ display:grid; grid-template-columns:1fr 380px; gap:60px; align-items:start; }
/* the tag-list column runs taller than the heading/pills/tool-grid column,
   so pinning the left side lets the page hold still while the cards scroll
   past it, then release into normal scroll once the tag-list runs out —
   same idea as the Folioxa reference (pure CSS, no scroll-jacking JS needed). */
.stack-left{ position:sticky; top:140px; }
.stack-heading{ font-size:56px; font-weight:500; letter-spacing:-1.68px; line-height:1.05; margin:0 0 28px; max-width:600px; }
.stack-heading .l1{ color:var(--accent-site); display:block; }
.stack-heading .l2{ color:#2b2b2b; display:block; }
.tech-label{
  font-family:'Whisper','Switzer',cursive; font-size:46px; font-weight:400; line-height:1.2; color:var(--text-black);
  display:inline-block; transform:rotate(-2deg); margin:0 0 20px;
}
.tool-grid{ display:grid; grid-template-columns:repeat(7, 1fr); gap:14px; max-width:536px; margin-bottom:36px; }
/* real tool logos — official brand marks (Simple Icons / svgrepo / direct
   asset links), all normalized to a single tile size, shadow, and hover
   pattern. Hovering any tile lifts it slightly and shows the tool's name
   in a small pill tooltip above it, the same pattern used for the cursor label. */
.tool-tile{
  position:relative;
  aspect-ratio:1; width:100%; background:#fff; border-radius:var(--radius-sm);
  border:1px solid rgba(0,0,0,0.07);
  display:flex; align-items:center; justify-content:center;
  box-shadow:var(--shadow-sm);
  color:#141414;
  transition:transform .3s var(--ease-out-expo), box-shadow .3s ease;
}
.tool-tile:hover{
  transform:translateY(-4px);
  box-shadow:var(--shadow-sm-hover);
}
.tool-tile svg{ width:28px; height:28px; transition:transform .3s var(--ease-out-expo); }
.tool-tile img{ width:28px; height:28px; object-fit:contain; transition:transform .3s var(--ease-out-expo); }
/* Canva has no free flat monochrome mark available, so instead of forcing
   its colorful circular badge through a filter (which flattens the white
   wordmark into the background and becomes unreadable), this recreates the
   same "circle + wordmark" shape as a plain black circle with the name set
   in Whisper (the site's script webfont) — solid black/white, no image. */
.canva-badge{
  width:32px; height:32px; border-radius:50%; background:#000;
  display:flex; align-items:center; justify-content:center;
  color:#fff; font-family:'Whisper',cursive; font-size:13px; line-height:1;
  transition:transform .3s var(--ease-out-expo);
}
.tool-tile:hover svg, .tool-tile:hover img, .tool-tile:hover .canva-badge{ transform:scale(1.08); }
.tool-tile .tt-tip{
  position:absolute; bottom:calc(100% + 10px); left:50%; z-index:5;
  transform:translateX(-50%) translateY(4px);
  background:var(--ink); color:#fff; font-size:11px; font-weight:600; white-space:nowrap;
  padding:6px 10px; border-radius:999px;
  opacity:0; pointer-events:none;
  transition:opacity .2s ease, transform .2s var(--ease-out-expo);
}
.tool-tile:hover .tt-tip{ opacity:1; transform:translateX(-50%) translateY(0); }
/* tt-tip is hover-only, which never fires on touch — script.js adds a
   .tt-active class on tap so touch users can see the tool name too, and
   makes the tile focusable so keyboard users get the same via :focus-visible. */
.tool-tile:focus-visible{ outline:2px solid var(--accent-site); outline-offset:2px; }
.tool-tile:focus-visible .tt-tip, .tool-tile.tt-active .tt-tip{ opacity:1; transform:translateX(-50%) translateY(0); }

/* card format: icon badge + title + description, one per row — matches
   the tool-tile shadow/border language used on the left side of this
   section so the two columns read as one family. */
.tag-list{ display:flex; flex-direction:column; gap:16px; }
.tag-item{
  display:flex; flex-direction:column; align-items:flex-start;
  background:#fff; border-radius:var(--radius-md); border:1px solid rgba(0,0,0,0.07);
  padding:26px 26px 24px;
  box-shadow:var(--shadow-sm);
  transition:transform .3s var(--ease-out-expo), box-shadow .3s ease;
}
.tag-item:hover{
  transform:translateY(-4px);
  box-shadow:var(--shadow-sm-hover);
}
.tag-icon{
  width:44px; height:44px; border-radius:var(--radius-sm); flex-shrink:0;
  background:linear-gradient(160deg, #2e2e2e 0%, #060606 100%);
  box-shadow:
    inset 0 1.5px 1.5px rgba(255,255,255,0.16),
    inset 0 -3px 4px rgba(0,0,0,0.5),
    0 8px 16px -8px rgba(var(--shadow-tint),0.45),
    0 2px 5px -1px rgba(var(--shadow-tint),0.24);
  display:flex; align-items:center; justify-content:center; color:#fff; font-size:16px;
  margin-bottom:18px;
  transition:transform .3s var(--ease-out-expo), box-shadow .3s ease;
}
.tag-item:hover .tag-icon{
  transform:translateY(-2px);
  box-shadow:
    inset 0 1.5px 1.5px rgba(255,255,255,0.2),
    inset 0 -3px 4px rgba(0,0,0,0.5),
    0 12px 22px -10px rgba(var(--shadow-tint),0.5),
    0 3px 6px -1px rgba(var(--shadow-tint),0.26);
}
.tag-icon svg{ width:20px; height:20px; display:block; flex-shrink:0; }
.tag-item .label{ font-size:21px; font-weight:600; letter-spacing:-0.42px; color:var(--text-black); margin:0 0 8px; }
.tag-item .desc{ font-size:14px; font-weight:500; line-height:1.55; color:var(--text-desc); margin:0; }

@media (max-width:860px){
  .stack-grid{ grid-template-columns:1fr; }
  .stack-left{ position:static; }
  .stack-heading{ font-size:36px; }
  .tag-list{ gap:14px; }
}
@media (max-width:560px){
  /* 7 equal columns at this width squeezed each tile under 34px square --
     below the 44px touch-target minimum. 4 columns keeps every tile a
     comfortable 60px+ tap target on a phone screen. */
  .tool-grid{ max-width:100%; gap:10px; grid-template-columns:repeat(4, 1fr); }
  .tool-tile svg, .tool-tile img{ width:22px; height:22px; }
  .canva-badge{ width:26px; height:26px; font-size:10px; }
}

.process-lead{ font-size:18px; font-weight:500; line-height:1.6; color:var(--text-desc); max-width:520px; margin:0 0 24px; }
.process-pills{ display:flex; flex-wrap:wrap; gap:10px; margin-bottom:32px; }
.process-pill{
  display:inline-flex; align-items:center; gap:6px; font-size:10px; font-weight:700; letter-spacing:0.05em; text-transform:uppercase;
  color:var(--text-black); background:#fff; border:1px solid rgba(0,0,0,0.08);
  padding:7px 14px; border-radius:999px; box-shadow:var(--shadow-xs);
}
.process-pill svg{ width:13px; height:13px; flex-shrink:0; }

/* ============ PROCESS: TOOL PAIRINGS SHOWCASE ============ */
/* .tools-showcase is now a full-bleed (edge-to-edge) wrapper — just a
   positioning context for the glow-blob, which needs the real viewport
   width to sit flush against actual screen edges instead of being boxed
   into a max-width column and clipped by overflow:hidden there. The
   centered content (heading, lead, cards) lives in .tools-showcase-inner
   underneath it. */
.tools-showcase{ position:relative; z-index:1; }
.tools-showcase-inner{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:40px 40px 100px; }
.tools-showcase-heading{ font-size:36px; font-weight:500; letter-spacing:-0.9px; line-height:1.15; max-width:680px; margin:0 0 12px; }
.tool-pair-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:20px; margin-top:32px; }
.tool-pair-card{
  display:flex; align-items:center; gap:16px;
  border-radius:var(--radius-md); padding:20px; box-shadow:var(--shadow-sm);
  transition:transform .3s var(--ease-out-expo), box-shadow .3s ease;
  /* glassmorphism — see the .job-card comment for why. */
  background:rgba(255,255,255,0.55);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
  border:1px solid rgba(255,255,255,0.7);
}
.tool-pair-card:hover{ transform:translateY(-3px); box-shadow:var(--shadow-sm-hover); }
.tp-icon{
  width:44px; height:44px; border-radius:var(--radius-sm); background:var(--bg); flex-shrink:0;
  display:flex; align-items:center; justify-content:center; color:#141414;
}
.tp-icon svg{ width:22px; height:22px; }
.tp-info h4{ font-size:16px; font-weight:600; letter-spacing:-0.3px; margin:0 0 6px; color:var(--text-black); }
.tp-tag{
  display:inline-block; font-size:10px; font-weight:700; letter-spacing:0.04em; text-transform:uppercase;
  color:var(--text-desc); background:var(--bg); padding:5px 10px; border-radius:999px;
}
@media (max-width:860px){
  .tool-pair-grid{ grid-template-columns:1fr 1fr; }
}
@media (max-width:560px){
  .tool-pair-grid{ grid-template-columns:1fr; }
}

/* ============ PROCESS: REST OF STACK ============ */
.rest-stack-section{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:0 40px 60px; }
.plain-label{ font-size:14px; font-weight:700; color:var(--text-desc); margin:0 0 20px; }
.stack-row-wrap{
  overflow-x:auto; scroll-snap-type:x proximity; scrollbar-width:none;
  /* overflow-x:auto forces overflow-y to a computed 'auto' too (CSS spec),
     which clips tile box-shadows (and the .tt-tip hover pill, which sits
     even further above the tile) tight against this box's padding edge.
     Padding here pushes that clip boundary out so shadows/hover-lift/tip
     all have room to render; the matching negative margin cancels the
     extra space so surrounding layout doesn't shift. Top needs enough
     room for the tip's own height (~24px) plus its 10px offset above
     the tile, not just the shadow bleed. */
  padding:48px 6px 36px;
  margin:-48px -6px -36px;
}
.stack-row-wrap::-webkit-scrollbar{ display:none; }
.stack-row{ display:flex; flex-wrap:wrap; gap:14px; }
.stack-row .tool-tile{
  width:64px; height:64px; background:#fff; border-radius:var(--radius-sm); flex-shrink:0;
  border:1px solid rgba(0,0,0,0.07);
  display:flex; align-items:center; justify-content:center; scroll-snap-align:start;
  box-shadow:var(--shadow-sm);
  color:#141414;
  transition:transform .3s var(--ease-out-expo), box-shadow .3s ease;
}
.stack-row .tool-tile:hover{
  transform:translateY(-4px);
  box-shadow:var(--shadow-sm-hover);
}
.stack-row .tool-tile svg{ width:28px; height:28px; transition:transform .3s var(--ease-out-expo); }
.stack-row .tool-tile img{ width:28px; height:28px; object-fit:contain; transition:transform .3s var(--ease-out-expo); }
.stack-row .tool-tile:hover svg, .stack-row .tool-tile:hover img, .stack-row .tool-tile:hover .canva-badge{ transform:scale(1.08); }

/* ============ PROCESS: SPRINT RHYTHM + PRINCIPLES (two-column) ============ */
/* full-bleed wrapper (see .tools-showcase comment above for why) — .rp-grid
   is the section's only real child, so it doubles as the centered inner
   content box instead of needing a separate wrapper div. */
.rhythm-principles-section{ position:relative; z-index:1; }
.rp-grid{ position:relative; z-index:1; display:grid; grid-template-columns:1fr 1fr; gap:32px; align-items:start; max-width:1200px; margin:0 auto; padding:20px 40px 140px; }
.rp-card{
  border-radius:var(--radius-md); padding:32px; box-shadow:var(--shadow-md);
  /* glassmorphism — see the .job-card comment for why. */
  background:rgba(255,255,255,0.55);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
  border:1px solid rgba(255,255,255,0.7);
}
.rp-card-head h3{ font-size:22px; font-weight:600; letter-spacing:-0.4px; margin:0 0 6px; color:var(--text-black); }
.rp-card-head p{ font-size:14px; font-weight:500; color:var(--text-desc); margin:0; }
.rp-divider{ height:1px; background:rgba(0,0,0,0.08); margin:24px 0 0; }
.rp-row{ display:flex; gap:20px; padding:18px 0; border-bottom:1px solid rgba(0,0,0,0.06); }
.rp-row:last-child{ border-bottom:none; padding-bottom:0; }
.week-row .week-day{ flex:0 0 40px; font-size:13px; font-weight:700; letter-spacing:0.06em; padding-top:2px; }
.week-day.lilac-dark, .p-num.lilac-dark{ color:var(--accent-site); }
.week-day.lilac-light, .p-num.lilac-light{ color:#a89db5; }
.week-row-body h4{ font-size:15px; font-weight:600; letter-spacing:-0.2px; margin:0 0 8px; color:var(--text-black); }
.week-row-body p{ font-size:13px; font-weight:500; line-height:1.6; color:var(--text-desc); margin:0 0 12px; }
.week-tags{ display:flex; flex-wrap:wrap; gap:6px; }
.week-tags span{
  font-size:10px; font-weight:700; letter-spacing:0.03em; text-transform:uppercase;
  color:var(--text-desc); background:transparent; border:1px solid rgba(0,0,0,0.12);
  padding:4px 8px; border-radius:999px;
}
.p-num{ flex:0 0 26px; font-size:13px; font-weight:700; padding-top:2px; }
.principle-row-body h4{ font-size:15px; font-weight:600; letter-spacing:-0.2px; margin:0 0 6px; color:var(--text-black); }
.principle-row-body p{ font-size:13px; font-weight:500; line-height:1.6; color:var(--text-desc); margin:0; }
@media (max-width:860px){
  .rp-grid{ grid-template-columns:1fr; }
}
@media (max-width:560px){
  .rp-row{ flex-direction:column; gap:8px; }
}

/* ============ HOW I THINK ABOUT MY WORK (accordion + LinkedIn card) ============ */
/* same full-width/inner split as .about above, for the same reason.
   overflow:hidden removed — it was clipping the glow-blob's own circular
   shape with a hard rectangular edge right at the section boundary instead
   of letting it fade/bleed naturally into the sections around it. */
.principles-section{ position:relative; z-index:1; padding:20px 0 140px; }
.principles-inner{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:0 40px; }
.principles-heading{ position:relative; z-index:1; font-size:64px; font-weight:500; letter-spacing:-1.92px; line-height:1.05; margin:0 0 48px; }
.principles-heading .l1{ color:var(--text-black); display:block; }
.principles-heading .l2{ color:var(--accent-site); display:block; }

.principles-grid{ position:relative; z-index:1; display:grid; grid-template-columns:1fr 1fr; gap:32px; align-items:start; }
.principles-grid.single{ grid-template-columns:1fr; max-width:820px; margin:0 auto; }

.accordion{ position:relative; z-index:1; display:flex; flex-direction:column; gap:16px; }
.acc-item{
  border-radius:var(--radius-md); box-shadow:var(--shadow-md);
  /* glassmorphism — see the .job-card comment for why. */
  background:rgba(255,255,255,0.55);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
  border:1px solid rgba(255,255,255,0.7);
}
.acc-header{
  display:flex; align-items:center; gap:16px; padding:16px 24px; user-select:none;
}
.acc-num{ font-size:16px; font-weight:500; letter-spacing:-0.32px; color:var(--text-black); }
.acc-title{ flex:1; font-size:16px; font-weight:600; color:var(--text-black); }
.acc-icon{ position:relative; width:16px; height:16px; flex-shrink:0; }
.acc-icon::before, .acc-icon::after{
  content:''; position:absolute; top:50%; left:50%; background:var(--text-black);
  transform:translate(-50%,-50%); transition:transform .3s var(--ease-out-expo), opacity .3s ease;
}
.acc-icon::before{ width:16px; height:2px; }
.acc-icon::after{ width:2px; height:16px; }
.acc-item.open .acc-icon::after{ transform:translate(-50%,-50%) rotate(90deg); opacity:0; }

.acc-collapse{ display:grid; grid-template-rows:0fr; transition:grid-template-rows .4s var(--ease-out-expo); }
.acc-item.open .acc-collapse{ grid-template-rows:1fr; }
.acc-collapse-inner{ overflow:hidden; }
.acc-body{ padding:0 24px 20px 56px; font-size:14px; font-weight:500; line-height:1.6; color:var(--text-black); margin:0; }

/* ============ JOB HISTORY TIMELINE ("Key moments in my career") ============
   Modeled on the Framer "Rivers" template's career-timeline block: a single
   vertical line down the middle, cards alternating left/right off it with a
   colored dot marking each one. Collapsed state only shows the "where /
   when / who" (company + dates pills, role title) — the story text (a
   subtitle line + paragraphs) is hidden and reveals on hover via the same
   grid-template-rows 0fr->1fr technique used by .acc-collapse elsewhere,
   just triggered by :hover instead of a click-toggled .open class. */
/* ============ EXPERIENCE: LEADERSHIP & MENTORING ============
   Its own section, with real top/bottom padding of its own so it reads as a
   complete block rather than a strip squeezed between the hero and "Key
   moments in my career" (the generous bottom padding here is what actually
   separates it from that next section; job-history-section's own top
   padding stays minimal, same rhythm used everywhere else on the site —
   e.g. .rhythm-principles-section's 140px bottom vs. the 20px top on
   whatever follows it). Two content cards, always expanded (no accordion
   toggle) — see .mentor-grid below. */
/* full-bleed wrapper + centered inner (see .tools-showcase comment above
   for why) — .mentor-inner wraps the tech-label/heading/accordion so the
   glow-blob (a direct child of .mentor-section itself) gets the real
   viewport width to sit against actual screen edges, unclipped. */
.mentor-section{ position:relative; z-index:1; }
/* same 1200px column as .page-hero and the homepage's .stack-section ("Good
   design ships and keeps moving") — was a narrower, independently-centered
   900px column, which put its left edge further right than the hero above
   it. Same max-width now, so both share one left edge; margin:0 auto still
   centers the whole column on the page like everywhere else. */
.mentor-inner{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:120px 40px 110px; }
.expand-list-heading{ font-size:26px; font-weight:600; letter-spacing:-0.4px; line-height:1.3; max-width:600px; margin:0 0 86px; color:var(--text-black); }
/* scoped override — real page h1 size/weight/spacing (matches .page-hero h1
   exactly), not the site's usual handwritten-script tech-label treatment
   (kept as-is on index.html/process.html). */
.mentor-section .tech-label{
  font-family:'Switzer',sans-serif; font-size:64px; font-weight:500;
  letter-spacing:-1.92px; line-height:1.05; color:var(--accent-site);
  transform:none; margin:0 0 20px;
}
/* two always-expanded cards side by side instead of a click-to-expand
   accordion — reuses the site's existing icon+title+description card
   component (.tag-item/.tag-icon/.label/.desc, same as the "Design Systems /
   Prototyping / ..." list on the homepage and process.html) rather than
   inventing a new card style. Grid's default align-items:stretch keeps both
   cards the same height regardless of which one has more text. */
.mentor-grid{ display:grid; grid-template-columns:1fr 1fr; gap:20px; margin-top:24px; }
@media (max-width:860px){ .mentor-grid{ grid-template-columns:1fr; } }
/* glassmorphism on these two cards only — same recipe as job-card/acc-item/
   rp-card elsewhere (translucent white + backdrop-blur), scoped so the
   homepage/process.html .tag-item cards stay solid white. */
.mentor-grid .tag-item{
  background:rgba(255,255,255,0.55);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
  border:1px solid rgba(255,255,255,0.7);
}

/* ============ CERTIFICATIONS (experience.html, after Leadership & mentoring) ============
   Same section-wrapper/heading pattern as .mentor-section right above it
   (big accent-colored .tech-label + .expand-list-heading subhead) so the two
   read as one continuous rhythm down the page, not two different components. */
.certs-section{ position:relative; z-index:1; }
.certs-inner{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:0 40px 110px; }
.certs-section .tech-label{
  font-family:'Switzer',sans-serif; font-size:64px; font-weight:500;
  letter-spacing:-1.92px; line-height:1.05; color:var(--accent-site);
  transform:none; margin:0 0 20px;
}
.cert-grid{ display:grid; grid-template-columns:repeat(3, 1fr); gap:20px; margin-top:24px; }
@media (max-width:860px){ .cert-grid{ grid-template-columns:1fr; } }
/* same glassmorphism recipe as .job-card/.tool-pair-card/.mentor-grid .tag-item,
   plus a colored top stripe (::before) like the reference — cycling through
   three site-lilac tones instead of arbitrary brand colors, so it still
   reads as "this site's palette" rather than a random rainbow. overflow:
   hidden clips the stripe to the card's own rounded top corners. */
.cert-card{
  position:relative; display:flex; flex-direction:column; gap:14px; min-height:160px;
  border-radius:var(--radius-md); padding:26px 24px 24px; box-shadow:var(--shadow-sm);
  transition:transform .3s var(--ease-out-expo), box-shadow .3s ease;
  background:rgba(255,255,255,0.55);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
  border:1px solid rgba(255,255,255,0.7);
  overflow:hidden;
}
.cert-card::before{
  content:''; position:absolute; top:0; left:0; right:0; height:5px;
  background:var(--accent-site);
}
.cert-card:nth-child(3n+2)::before{ background:#b9a0dd; }
.cert-card:nth-child(3n)::before{ background:#4a2f73; }
.cert-card:hover{ transform:translateY(-3px); box-shadow:var(--shadow-sm-hover); }

/* all six cards now use real logo images (backgrounds removed, cropped
   tight) instead of the earlier text-wordmark stand-ins — desaturated +
   dimmed so IBM/Coursera/Google/Illinois/SkillUp all read as one consistent
   neutral-gray family instead of five different brand colors. */
.cert-logo-img{
  max-height:24px; max-width:100px; width:auto; height:auto; object-fit:contain;
  display:block; align-self:flex-start;
  filter:grayscale(1); opacity:0.8;
}
.cert-title{ font-size:17px; font-weight:600; letter-spacing:-0.2px; line-height:1.35; color:var(--text-black); margin:0; }
.cert-partner{
  font-size:12px; font-weight:600; color:var(--text-desc); margin:-8px 0 0;
  display:flex; align-items:center; gap:5px;
}
.cert-partner-logo{ height:11px; width:auto; filter:grayscale(1); opacity:0.75; }
.cert-card--progress{ border:1px solid rgba(109,79,150,0.35); }
.cert-status{
  align-self:flex-start; margin-top:auto;
  font-size:10px; font-weight:700; letter-spacing:0.05em; text-transform:uppercase;
  color:#fff; background:var(--accent-site); padding:5px 10px; border-radius:999px;
}

/* full-bleed wrapper + centered inner, same reasoning as .mentor-section
   above — .job-history-inner wraps the heading/timeline. */
.job-history-section{ position:relative; z-index:1; }
.job-history-inner{ position:relative; z-index:1; max-width:900px; margin:0 auto; padding:100px 40px 60px; }
/* one line instead of the usual two-line l1/l2 stack (kept block/stacked
   everywhere else .stack-heading is used, e.g. the homepage) — scoped so it
   only affects this instance. The source whitespace between the two spans
   collapses into a normal inline space once they're no longer display:block. */
.job-history-inner .stack-heading{ margin-bottom:70px; white-space:nowrap; max-width:none; }
.job-history-inner .stack-heading .l1,
.job-history-inner .stack-heading .l2{ display:inline; }
.timeline{ position:relative; z-index:1; }
/* height (not top+bottom) so script.js can shrink it to end exactly at the
   last dot instead of running into the empty space below a shorter last
   card — --tl-line-h is set from JS once it measures where that dot sits;
   the calc() is just a same-as-before fallback before JS runs. Starts at
   top:0 (not a few px down) to match the dots, which now sit centered
   exactly on each card's own top edge, not floating partway down the header. */
.timeline::before{
  content:''; position:absolute; left:50%; top:0; width:2px;
  height:var(--tl-line-h, 100%);
  background:rgba(0,0,0,0.08); transform:translateX(-50%);
}
.timeline-item{ position:relative; width:calc(50% - 48px); margin-bottom:40px; }
.timeline-item:last-child{ margin-bottom:0; }
.timeline-item.timeline-left{ margin-right:auto; }
.timeline-item.timeline-right{ margin-left:auto; }
/* top:-7px (half the dot's own height) centers the dot ON the item's top
   edge — i.e. flush with the top of the card below it — instead of sitting
   part-way down inside the header row. */
.timeline-dot{
  position:absolute; top:-7px; width:14px; height:14px; border-radius:50%;
  background:#fff; border:3px solid var(--accent); z-index:2;
}
.timeline-item.timeline-left .timeline-dot{ right:-55px; }
.timeline-item.timeline-right .timeline-dot{ left:-55px; }

/* handwritten callout pointing at the last (= earliest, "the start") dot —
   translateY(-50%) keeps it vertically centered on the dot regardless of
   whether the caption wraps to one or two lines, so the arrow (centered in
   the same flex row) always lines up with the dot itself, not the card.
   The dot spans +41px to +55px from the item edge (14px wide, centered on
   the +48px line — see .timeline-dot above); left:+56px starts the note
   (and its arrow's tip, at the very start of the svg) about 4-6px past the
   dot's outer edge, instead of landing on top of it. */
.timeline-start-note{
  position:absolute; top:0; transform:translateY(-50%);
  display:flex; align-items:center; gap:16px;
  font-family:'Whisper','Switzer',cursive; font-size:20px; font-weight:400; line-height:1.2;
  color:var(--text-black); white-space:nowrap;
}
.timeline-item.timeline-left .timeline-start-note{ left:calc(100% + 56px); }
.timeline-item.timeline-right .timeline-start-note{ right:calc(100% + 56px); flex-direction:row-reverse; text-align:right; }
/* base arrow points left (toward a dot that sits to the note's left, the
   .timeline-left case); mirrored for .timeline-right, where the dot instead
   sits to the note's right. */
.timeline-start-note svg{ width:34px; height:19px; flex-shrink:0; }
.timeline-item.timeline-right .timeline-start-note svg{ transform:scaleX(-1); }
@media (max-width:900px){ .timeline-start-note{ display:none; } }

/* kept close to monochrome on purpose: the three "story" cards (the ones
   that expand on hover) share the same dark lilac already established for
   headings site-wide, so they read as the main chapters. The four plain
   cards below them (no expand, just where/when/who) are all a neutral gray
   instead, so the accent itself signals "this one's just a line item." */
.timeline-item.c-lilac{ --accent:var(--accent-site); }
.timeline-item.c-gray{ --accent:#9a9a9a; }

.job-card{
  position:relative; border-radius:var(--radius-md); overflow:hidden;
  padding:26px 26px 24px; box-shadow:var(--shadow-sm);
  transition:box-shadow .3s ease;
  /* glassmorphism, matching the nav's frosted look — translucent white +
     backdrop-blur instead of a flat fill. Needs something with actual
     color/contrast behind it to read as glass rather than plain washed-out
     white, hence the glow-blob added to this section's background. */
  background:rgba(255,255,255,0.55);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
  border:1px solid rgba(255,255,255,0.7);
}
.job-card:hover{ box-shadow:var(--shadow-sm-hover); }
/* keyboard/touch users can't trigger :hover at all, so a real focus ring
   here matters — this is the whole card's ONLY way of announcing "this is
   interactive, press to expand" to someone tabbing through the page. */
.job-card:focus-visible{ outline:2px solid var(--accent-site); outline-offset:2px; }
.job-card::before{ content:''; position:absolute; top:0; left:0; right:0; height:4px; background:var(--accent); }
.job-card-tags{ display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; gap:8px; margin-bottom:16px; }
.job-card-tags span{
  font-size:12px; font-weight:600; background:rgba(0,0,0,0.05);
  padding:6px 12px; border-radius:999px;
  /* was var(--text-gray) #828282, ~3.85:1 on this pill at 12px — under the
     4.5:1 AA minimum for normal-size text. --text-desc passes comfortably. */
  color:var(--text-desc);
}
.job-card-role{ font-size:21px; font-weight:600; letter-spacing:-0.4px; color:var(--text-black); margin:0; }
.job-card-note{
  font-size:12px; font-weight:500; font-style:italic; margin:6px 0 0;
  /* same contrast fix as .job-card-tags above. */
  color:var(--text-desc);
}

.job-card-detail{
  display:grid; grid-template-rows:0fr;
  transition:grid-template-rows .55s cubic-bezier(0.65,0,0.35,1);
}
/* .open is toggled by script.js on click/Enter/Space, since :hover alone
   never fires on touch devices — without it, the actual story text (what
   was built, what shipped) was completely unreachable on phone/tablet. */
.job-card:hover .job-card-detail, .job-card.open .job-card-detail{ grid-template-rows:1fr; }
.job-card-detail-inner{
  overflow:hidden; opacity:0; transform:translateY(-4px);
  transition:opacity .3s ease, transform .3s ease;
}
.job-card:hover .job-card-detail-inner, .job-card.open .job-card-detail-inner{
  opacity:1; transform:translateY(0);
  transition:opacity .45s ease .12s, transform .45s var(--ease-out-expo) .12s;
}
.job-card-subtitle{ font-size:14px; font-weight:700; color:var(--text-black); margin:16px 0 8px; }
.job-card-text{ font-size:13px; font-weight:500; line-height:1.6; color:var(--text-desc); margin:0 0 10px; }
.job-card-text:last-child{ margin-bottom:0; }
/* plain cards (no story to reveal) skip the hover lift entirely — otherwise
   they'd tease an expansion that never comes. */
.job-card.plain:hover{ box-shadow:var(--shadow-sm); }

@media (max-width:700px){
  .timeline::before{ left:6px; }
  .timeline-item, .timeline-item.timeline-left, .timeline-item.timeline-right{
    width:calc(100% - 34px); margin-left:34px; margin-right:0;
  }
  .timeline-item.timeline-left .timeline-dot,
  .timeline-item.timeline-right .timeline-dot{ left:-28px; right:auto; }
}

.linkedin-card{
  border-radius:var(--radius-md); padding:32px; box-shadow:var(--shadow-md);
  /* glassmorphism — see the .job-card comment for why. */
  background:rgba(255,255,255,0.55);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
  border:1px solid rgba(255,255,255,0.7);
}
.linkedin-card .avatar{ width:90px; height:90px; border-radius:50%; object-fit:cover; margin-bottom:20px; background:#ddd; }
.linkedin-card h3{ font-size:32px; font-weight:500; letter-spacing:-0.96px; line-height:1.1; margin:0 0 16px; }
.linkedin-card h3 .l1{ color:var(--accent-site); display:block; }
.linkedin-card h3 .l2{ color:var(--text-black); display:block; }
.linkedin-card p{ font-size:14px; font-weight:500; line-height:1.6; color:var(--text-desc); margin:0 0 24px; }
.linkedin-btn-row{ display:flex; flex-wrap:wrap; gap:10px; }
.linkedin-btn{
  /* was --radius-lg (24px, a rounded rectangle) -- every other button of
     this same size/role (.nav-connect, .learn-btn, .cb-btn) is a full pill.
     Matching that so it doesn't read as a different component. */
  display:inline-flex; align-items:center; gap:8px; background:var(--ink); color:#fff;
  font-size:12px; font-weight:600; padding:10px 16px; border-radius:999px;
  transition:transform .25s var(--ease-out-expo), background .2s;
}
.linkedin-btn:hover{ background:#333; transform:translateY(-2px); }
.linkedin-btn.ghost{ background:transparent; color:var(--text-black); box-shadow:inset 0 0 0 1px rgba(var(--shadow-tint),0.15); }
.linkedin-btn.ghost:hover{ background:rgba(0,0,0,0.04); }
.linkedin-btn .btn-icon{
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
  width:20px; height:20px; border-radius:6px;
  font-size:11px; font-weight:700; line-height:1;
  background:rgba(255,255,255,0.18);
}
.linkedin-btn.ghost .btn-icon{ background:rgba(0,0,0,0.08); }

@media (max-width:860px){
  .principles-heading{ font-size:36px; }
  .principles-grid{ grid-template-columns:1fr; }
}

/* ============ FOOTER ============ */
footer{
  position:relative; z-index:1; background:#000; color:#fff;
  padding:140px 40px 0; overflow:hidden;
}
.footer-inner{ max-width:1200px; margin:0 auto; }
.footer-heading{ font-size:72px; font-weight:500; letter-spacing:-2.16px; line-height:1; margin:0 0 56px; }
.footer-heading .l1{ color:#fff; display:block; }
.footer-heading .l1 .word{ display:inline-block; }
/* footer is on a black background — the same lilac lightened so it stays legible there */
.footer-heading .l2{ color:#b3a8c0; display:block; }

.footer-row{ display:flex; flex-wrap:wrap; gap:40px; margin-bottom:48px; }
.footer-col .flabel{ font-size:12px; font-weight:600; letter-spacing:-0.12px; color:var(--text-gray); margin:0 0 10px; }
.footer-col a.femail{ font-size:18px; font-weight:500; color:#fff; }
.footer-col a.femail:hover{ color:var(--text-gray); }
.footer-social{ display:flex; gap:10px; }
.footer-social a{
  /* 44x44 minimum touch target (was 36x36) — the circle itself still reads
     visually compact since the border/label sizing didn't change. */
  width:44px; height:44px; border-radius:50%; border:1px solid rgba(255,255,255,0.25);
  display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#fff;
  transition:background .2s, border-color .2s;
}
.footer-social a:hover{ background:rgba(255,255,255,0.1); border-color:rgba(255,255,255,0.5); }

.footer-divider{ height:1px; background:rgba(255,255,255,0.15); margin-bottom:32px; }
.footer-menu-row{ display:flex; justify-content:space-between; align-items:flex-start; flex-wrap:wrap; gap:20px; padding-bottom:56px; }
.footer-menu .flabel{ font-size:12px; font-weight:600; letter-spacing:-0.12px; color:var(--text-gray); margin:0 0 12px; }
.footer-menu-links{ display:grid; grid-template-columns:repeat(2, auto); gap:8px 32px; }
.footer-menu-links a{ font-size:12px; font-weight:600; letter-spacing:-0.12px; color:#fff; }
.footer-menu-links a:hover{ color:var(--text-gray); }
.footer-copy{ font-size:12px; font-weight:600; color:#b8b8b8; white-space:nowrap; }

.footer-wordmark{
  font-size:clamp(70px, 13vw, 250px); font-weight:700; color:#fff; line-height:0.8;
  letter-spacing:-0.02em; white-space:nowrap; margin:0; user-select:none;
  transform:translateY(18%);
}

/* .footer--v2: reordered footer (menu + copyright moved above the divider,
   nothing after it) — scoped so pages still on the old layout are unaffected */
footer.footer--v2{ padding-bottom:56px; }
footer.footer--v2 .footer-menu-row{ padding-bottom:0; margin-bottom:32px; }
footer.footer--v2 .footer-divider{ margin-bottom:24px; }
footer.footer--v2 .footer-copy{ display:block; text-align:left; }
.footer-right-group{ display:flex; gap:40px; align-items:flex-start; }
.footer-right-group .femail{ display:flex; align-items:center; height:36px; }
@media (max-width:560px){
  .footer-right-group{ gap:24px; }
}

@media (max-width:860px){
  .footer-heading{ font-size:44px; letter-spacing:-1.3px; }
  footer{ padding-top:90px; }
  .footer-menu-row{ padding-bottom:32px; }
  footer.footer--v2{ padding-bottom:32px; }
}

/* ============ FLOATING CONTACT BAR ============ */
/* corner button, deliberately not competing with the centered contact-bar
   pill for the same bottom strip -- sits bottom-right, contact-bar stays
   bottom-center */
.back-to-top{
  position:fixed; right:24px; bottom:24px; z-index:85;
  width:48px; height:48px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  background:rgba(255,255,255,0.75);
  backdrop-filter:blur(var(--blur-chrome)); -webkit-backdrop-filter:blur(var(--blur-chrome));
  border:1px solid rgba(255,255,255,0.95);
  box-shadow:var(--shadow-lg);
  color:var(--accent-site); cursor:pointer;
  opacity:0; pointer-events:none; transform:translateY(12px);
  transition:opacity .3s ease, transform .3s var(--ease-out-expo), background .2s ease, color .2s ease;
}
.back-to-top.visible{ opacity:1; pointer-events:auto; transform:translateY(0); }
.back-to-top:hover{ background:var(--accent-site); color:#fff; }
.back-to-top:focus-visible{ outline:2px solid var(--accent-site); outline-offset:2px; }
.back-to-top svg{ width:20px; height:20px; }
@media (max-width:640px){ .back-to-top{ bottom:96px; right:16px; width:44px; height:44px; } }

.contact-bar{
  position:fixed; left:50%; bottom:24px; z-index:90;
  transform:translate(-50%, 20px); opacity:0; pointer-events:none;
  transition:opacity .4s ease, transform .4s var(--ease-out-expo);
}
.contact-bar.visible{ transform:translate(-50%, 0); opacity:1; pointer-events:auto; }
.contact-pill{
  display:grid; align-items:center; background:rgba(255,255,255,0.5);
  backdrop-filter:blur(var(--blur-chrome)); -webkit-backdrop-filter:blur(var(--blur-chrome));
  border:1px solid rgba(255,255,255,0.8); border-radius:48px;
  box-shadow:var(--shadow-pill);
  padding:12px 12px 12px 24px;
}
/* both layers occupy the same grid cell, so the pill auto-sizes to
   whichever one is currently visible. Crossfade is opacity + a light
   scale, on the same slow custom ease used for the pill's own show/hide,
   so the swap reads as one smooth motion rather than an instant cut. */
.cb-default, .cb-hover{
  grid-area:1/1; display:flex; align-items:center; gap:16px;
  transition:opacity .35s ease, transform .35s cubic-bezier(0.65,0,0.35,1);
}
.cb-hover{ opacity:0; pointer-events:none; gap:8px; transform:scale(0.96); }
.cb-default{ transform:scale(1); }
.contact-pill:hover .cb-default{ opacity:0; pointer-events:none; transform:scale(0.96); }
.contact-pill:hover .cb-hover{
  opacity:1; pointer-events:auto; transform:scale(1);
  transition:opacity .45s ease .1s, transform .45s cubic-bezier(0.65,0,0.35,1) .1s;
}
.cb-text p{ margin:0; white-space:nowrap; }
.cb-text .title{ font-size:14px; font-weight:600; color:var(--text-black); }
.cb-text .sub{ font-size:12px; font-weight:600; color:var(--text-desc); }
.cb-icons{ display:flex; gap:8px; }
.cb-icon{
  position:relative;
  width:40px; height:40px; border-radius:50%; display:flex; align-items:center; justify-content:center;
  transition:transform .3s cubic-bezier(0.33,1,0.68,1);
}
/* a11y audit fix -- 44x44 hit area without growing the visible circle. */
.cb-icon::before{ content:''; position:absolute; inset:-2px; }
.cb-icon:hover{ transform:scale(1.08); }
.cb-icon.dark{ background:var(--ink); color:#fff; }
.cb-icon.light{ background:rgba(250,250,250,0.9); color:#111; }
.cb-icon svg{ width:17px; height:17px; }
.cb-btn{
  display:flex; align-items:center; gap:8px; white-space:nowrap;
  padding:10px 20px; border-radius:999px; font-size:13px; font-weight:600;
  transition:transform .3s cubic-bezier(0.33,1,0.68,1);
}
.cb-btn:hover{ transform:translateY(-2px); }
.cb-btn.dark{ background:var(--ink); color:#fff; }
.cb-btn.light{ background:rgba(250,250,250,0.9); color:#111; }
.cb-btn svg{ width:15px; height:15px; flex-shrink:0; }

/* ============ CONTACT MODAL ============
   Triggered from the nav "Connect" link and the floating contact bar's
   dark "Contact" control (both intercepted in script.js). Centered
   overlay, no backend — submitting builds a mailto: with the form fields
   filled in, since the whole site is static. */
.contact-modal-overlay{
  position:fixed; inset:0; z-index:300; display:flex; align-items:center; justify-content:center;
  padding:24px; background:rgba(20,18,22,0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px);
  opacity:0; pointer-events:none; transition:opacity .3s ease;
}
.contact-modal-overlay.open{ opacity:1; pointer-events:auto; }
.contact-modal{
  position:relative; width:100%; max-width:540px; max-height:88vh; overflow-y:auto;
  background:#fff; border-radius:28px; padding:40px;
  box-shadow:var(--shadow-modal);
  transform:translateY(18px) scale(0.97); transition:transform .35s var(--ease-out-expo);
}
.contact-modal-overlay.open .contact-modal{ transform:translateY(0) scale(1); }
.contact-modal-close{
  /* 44x44 minimum touch target (was 36x36) — offset tightened so the
     circle's outer edge lands in the same spot as before. */
  position:absolute; top:12px; right:12px; width:44px; height:44px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; background:transparent; border:none;
  color:var(--text-black); cursor:pointer; transition:background .2s ease; padding:0;
}
.contact-modal-close:hover{ background:rgba(0,0,0,0.06); }
.contact-modal-close svg{ width:18px; height:18px; }
.contact-modal-title{ font-size:30px; font-weight:600; letter-spacing:-0.6px; color:var(--text-black); margin:0 0 8px; padding-right:32px; }
.contact-modal-sub{ font-size:14px; font-weight:500; color:var(--text-desc); line-height:1.5; margin:0 0 28px; }
.contact-modal-profile{ display:flex; align-items:center; gap:12px; margin-bottom:28px; }
.contact-modal-profile img{ width:44px; height:44px; border-radius:50%; object-fit:cover; flex-shrink:0; }
.cmp-name{ font-size:15px; font-weight:700; color:var(--text-black); margin:0 0 2px; }
.cmp-email{ font-size:13px; font-weight:600; color:var(--text-desc); }
.cmp-email:hover{ color:var(--text-black); }
.contact-modal-form{ display:flex; flex-direction:column; gap:20px; }
.cmf-row{ display:grid; grid-template-columns:1fr 1fr; gap:16px; }
.cmf-field label{ display:block; font-size:13px; font-weight:600; color:var(--text-black); margin:0 0 8px; }
.cmf-field input, .cmf-field textarea{
  width:100%; font:inherit; color:var(--text-black);
  background:#fff; border:1px solid rgba(0,0,0,0.13); border-radius:var(--radius-sm);
  padding:12px 14px; transition:border-color .2s ease;
  /* was 14px — below the 16px iOS Safari needs to avoid auto-zooming the
     page when an input is focused. */
  font-size:16px;
}
.cmf-field input::placeholder, .cmf-field textarea::placeholder{ color:var(--text-desc); }
/* was outline:none with only a subtle border-color change — too low-contrast
   to reliably read as "this is focused" for keyboard users. Keeps the
   border tweak but restores a real, visible ring on top of it. */
.cmf-field input:focus, .cmf-field textarea:focus{
  outline:2px solid var(--accent-site); outline-offset:2px; border-color:rgba(0,0,0,0.4);
}
.cmf-field textarea{ resize:vertical; min-height:120px; }
.cmf-submit{
  width:100%; border:none; border-radius:999px; background:var(--ink); color:#fff;
  font-size:15px; font-weight:700; padding:16px; cursor:pointer;
  box-shadow:var(--shadow-btn-dark);
  transition:transform .25s var(--ease-out-expo), box-shadow .25s ease;
}
.cmf-submit:hover{ transform:translateY(-2px); box-shadow:var(--shadow-btn-dark-hover); }
.contact-modal-note{
  font-family:'Whisper','Switzer',cursive; font-size:26px; font-weight:400; color:var(--text-black);
  text-align:center; transform:rotate(-2deg); margin:22px 0 0;
}
@media (max-width:560px){
  .contact-modal{ padding:28px 24px; border-radius:var(--radius-lg); }
  .contact-modal-title{ font-size:25px; }
  .cmf-row{ grid-template-columns:1fr; gap:20px; }
}

@media (max-width:860px){
  .hero{ grid-template-columns:1fr; padding-top:130px; }
  .stack-wrap{ height:260px; }
  h1.headline{ font-size:38px; }
  .work-title{ font-size:40px; }
  .work-kicker{ font-size:34px; }
}

/* ============ "FIRST SECTION ON A STANDALONE PAGE" TOP SPACING ============
   .about / .stack-section / .principles-section normally sit below other
   sections, so they have no top padding of their own. When one of them is
   the very first thing on a page (about.html, process.html, experience.html)
   it needs room to clear the fixed nav pill. */
.about.standalone,
.stack-section.standalone,
.principles-section.standalone{ padding-top:170px; }

/* ============ SHARED SECTION KICKER (standalone pages + case studies) ============ */
.okicker{ font-size:12px; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; color:var(--text-desc); margin:0 0 12px; }
.okicker.light{ color:rgba(255,255,255,0.55); }

/* ============ STANDALONE PAGE HERO (Work / About / Process / Experience) ============ */
.page-hero{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:170px 40px 60px; }
/* experience.html only — matches the homepage hero's overall size: same
   150px top padding, same 90px bottom padding, and the same min-height:80vh
   the homepage .hero uses, so this block takes up the same amount of
   vertical space. Content is centered in that space the same way the
   homepage hero centers its two-column grid. */
.exp-hero{
  padding-top:150px; padding-bottom:90px; min-height:80vh;
  display:flex; flex-direction:column; justify-content:center;
}
.page-hero h1{ font-size:64px; font-weight:500; letter-spacing:-1.92px; line-height:1.05; margin:0 0 20px; }
.page-hero h1 .l1{ color:var(--accent-site); display:block; }
.page-hero h1 .l2{ color:#2b2b2b; display:block; }
/* .oneline: scoped override for work.html's "Featured Projects ✦" heading —
   the other standalone pages still want their l1/l2 stacked on two lines,
   so this only applies where the h1 is explicitly marked. Flex (not
   inline-block) sidesteps the same negative-letter-spacing rendering quirk
   fixed elsewhere on the site: flex items get blockified regardless of
   their own display value, so .l1/.l2 don't need their own override here. */
.page-hero h1.oneline{ display:flex; flex-wrap:wrap; align-items:baseline; gap:0.3em; }
.page-hero p{ font-size:18px; font-weight:500; line-height:1.6; color:var(--text-desc); max-width:520px; margin:0; }
/* .wide: experience.html's paragraph is three sentences (much longer than
   the one-liner used on every other standalone page), so at the shared
   520px width it wrapped early and cramped, well short of where the h1's
   own "to shaping how things work" line ends above it. Widened to match
   that line's length instead of shortening the copy or touching the other
   pages' hero paragraphs. */
.page-hero p.wide{ max-width:760px; }
@media (max-width:860px){ .page-hero h1{ font-size:36px; } }

/* work.html only — centered hero with a handwritten script kicker above the
   heading, matching the reference layout. Scoped to .work-hero so about.html/
   process.html/experience.html's left-aligned .page-hero is untouched. */
.work-hero{ text-align:center; }
.work-hero .work-kicker-script{
  font-family:'Whisper','Switzer',cursive; font-size:28px; font-weight:400;
  line-height:1.2; color:var(--text-black); display:block; margin:0 0 8px;
}
.work-hero h1.oneline{ justify-content:center; }
.work-hero p{ margin:0 auto; }
/* "Featured Projects" made fully black here -- .page-hero h1 .l1's default
   accent-site purple only applies to "Featured" otherwise; this overrides
   just that first word on work.html without touching about/process/
   experience, whose page-hero headings still use the purple accent l1. */
.work-hero h1.oneline .l1{ color:#2b2b2b; }

/* ============ STATIC WORK GRID (work.html — settled grid, no scroll morph) ============
   Same overlay card treatment as the homepage's animated project cards
   (title top-left, year/type tags top-right, sitting right on the cover
   image) — just laid out in a plain static CSS grid instead of the
   fan->grid scroll morph, since this page doesn't have a hero stack to
   morph from. See .card-scrim/.card-top/.card-title/.card-tags above. */
.work-static-grid{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:20px 40px 160px; display:grid; grid-template-columns:1fr 1fr; gap:24px; }
@media (max-width:860px){ .work-static-grid{ grid-template-columns:1fr; } }
.wcard{
  position:relative; z-index:1; display:block; aspect-ratio:4/3; border-radius:var(--card-radius);
}
.wcard:hover .wcard-surface{ box-shadow:var(--shadow-lg-hover); }
.wcard:hover .thumb{ transform:scale(1.045); }

/* "Coming soon" card state (no title/tags yet, case study not ready) —
   thumbnail dimmed so it visibly reads as unavailable, plain centered pill
   instead of the top-corner title+tags, and no click-through since there's
   no real case study to land on yet. */
.proj-card.soon, .wcard.soon{ cursor:default; }
.proj-card.soon .thumb, .wcard.soon .thumb{ filter:grayscale(0.4) brightness(0.85); }
.card-soon{
  position:absolute; top:50%; left:50%; z-index:2; transform:translate(-50%,-50%);
  display:inline-flex; align-items:center;
  font-size:13px; font-weight:700; letter-spacing:0.04em; text-transform:uppercase; color:#fff;
  background:rgba(255,255,255,0.16); border:1px solid rgba(255,255,255,0.3);
  backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
  padding:10px 20px; border-radius:999px; white-space:nowrap;
}

/* ============ CASE STUDY PAGES ============ */
.case-hero{
  position:relative; z-index:1; min-height:640px; display:flex; align-items:flex-end;
  background-size:cover; background-position:center; padding:170px 40px 56px;
}
.case-hero-inner{ max-width:1200px; margin:0 auto; width:100%; }
.case-hero h1{ color:#fff; font-size:56px; font-weight:500; letter-spacing:-1.5px; line-height:1.08; margin:0 0 20px; max-width:820px; }
.case-hero-sub{ color:rgba(255,255,255,0.82); font-size:16px; font-weight:500; max-width:560px; margin:0 0 24px; line-height:1.5; }
.case-meta-row{ display:flex; gap:36px; margin-bottom:20px; }
.case-meta-row .clabel{ display:block; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:0.06em; color:rgba(255,255,255,0.5); margin-bottom:4px; }
.case-meta-row .cval{ font-size:14px; font-weight:600; color:#fff; }
.case-tags{ display:flex; gap:10px; flex-wrap:wrap; }
.case-tag{ font-size:11px; font-weight:700; letter-spacing:0.04em; text-transform:uppercase; color:#fff; background:rgba(255,255,255,0.14); border:1px solid rgba(255,255,255,0.25); padding:8px 14px; border-radius:999px; }

.case-hero.light-hero h1{ color:#0B1C30; }
.case-hero.light-hero .case-hero-sub{ color:rgba(11,28,48,0.72); }
.case-hero.light-hero .case-meta-row .clabel{ color:rgba(11,28,48,0.5); }
.case-hero.light-hero .case-meta-row .cval{ color:#0B1C30; }
.case-hero.light-hero .case-tag{ color:var(--accent-teamup); background:rgba(255,255,255,0.65); border:1px solid rgba(var(--accent-teamup-rgb),0.2); }

.case-overview{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:100px 40px 48px; display:grid; grid-template-columns:260px 1fr; gap:60px; }
.case-overview-labels .olabel-row{ display:flex; justify-content:space-between; gap:16px; padding:12px 0; border-top:1px solid rgba(0,0,0,0.08); font-size:13px; font-weight:600; }
.case-overview-labels .olabel-row:last-child{ border-bottom:1px solid rgba(0,0,0,0.08); }
.case-overview-labels .olabel{ color:var(--text-desc); }
.case-overview-labels .oval{ color:var(--text-black); }
.case-overview-copy h2{ font-size:32px; font-weight:500; letter-spacing:-0.8px; margin:0 0 16px; }
.case-overview-copy p{ font-size:16px; font-weight:500; line-height:1.6; color:var(--text-desc); margin:0 0 18px; max-width:640px; }
.case-overview-copy p:last-child{ margin-bottom:0; }
@media (max-width:860px){ .case-overview{ grid-template-columns:1fr; gap:24px; } }

.case-shot-wrap{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:0 40px 100px; }
.case-shot{ width:100%; aspect-ratio:16/10; border-radius:var(--radius-lg); background-size:contain; background-repeat:no-repeat; background-position:center; background-color:#eeeeff; box-shadow:var(--shadow-lg); }
.case-shot.alt{ aspect-ratio:16/9; }
/* real <img>, no forced aspect-ratio -- takes whatever height the image's
   own proportions give it, so there's never a letterboxed gap on the sides
   no matter what size it's exported at */
.case-shot-img{ width:100%; height:auto; display:block; border-radius:var(--radius-lg); box-shadow:var(--shadow-lg); background:#eeeeff; }

/* Design System proof: two screens, static, side by side -- no hover/click
   interaction, just laid flat so both are readable at once. */
.case-ds-pair{ max-width:1200px; margin:0 auto; padding:0 40px 100px; display:grid; grid-template-columns:1fr 1fr; gap:24px; }
.ds-pane img{ width:100%; height:auto; display:block; border-radius:var(--radius-lg); box-shadow:var(--shadow-lg); }
@media (max-width:700px){ .case-ds-pair{ grid-template-columns:1fr; } }

/* Process: a horizontal roadmap rather than another card grid -- the thin
   top rule running across all five steps is what reads as "one timeline",
   distinct from the glassmorphism card patterns used elsewhere in the case. */
.case-process{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:0 40px 120px; }
.case-process h2{ font-size:32px; font-weight:500; letter-spacing:-0.8px; max-width:640px; margin:0 0 16px; }
.process-track{ display:grid; grid-template-columns:repeat(5,1fr); gap:24px; margin-top:44px; }
.process-step{ position:relative; padding-top:22px; }
.process-step::before{ content:''; position:absolute; top:0; left:0; right:0; height:3px; background:rgba(var(--accent-teamup-rgb),0.16); border-radius:2px; }
.process-step-head{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:12px; }
.process-num{ font-size:13px; font-weight:700; color:var(--accent-teamup); letter-spacing:0.06em; }
.process-week{ font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:var(--accent-teamup); background:rgba(var(--accent-teamup-rgb),0.1); padding:3px 9px; border-radius:999px; }
.process-step h4{ font-size:16px; font-weight:600; margin:0 0 8px; }
.process-step p{ font-size:13px; font-weight:500; line-height:1.6; color:var(--text-desc); margin:0; }
@media (max-width:900px){ .process-track{ grid-template-columns:1fr 1fr; } }
@media (max-width:560px){ .process-track{ grid-template-columns:1fr; } }

/* Interfaces -- real screens grouped by role, each role as its own gallery
   (any group of 3+ screens becomes a carousel instead of eating vertical
   space as a stacked grid). Full-bleed white -> lilac -> white backdrop so
   the gallery cards read as sitting on a distinct surface instead of
   blending into the plain page background. */
.case-interfaces{ position:relative; z-index:1; margin:0; padding:80px 0; background:linear-gradient(180deg, #ffffff 0%, var(--accent-teamup-tint) 50%, #ffffff 100%); }
.case-interfaces-inner{ max-width:1200px; margin:0 auto; padding:0 40px; }
.iface-role-block{ margin:64px 0 0; }
.iface-role-block:first-of-type{ margin-top:48px; }
.iface-role-head{ display:flex; align-items:baseline; gap:28px; flex-wrap:wrap; margin-bottom:24px; }
.iface-role-head h3{ font-size:22px; font-weight:600; color:var(--accent-teamup); margin:0; white-space:nowrap; }
.iface-role-head p{ font-size:15px; font-weight:500; line-height:1.6; color:var(--text-desc); max-width:560px; margin:0; }
@media (max-width:800px){
  .iface-role-head{ flex-direction:column; gap:6px; }
}

/* text + screenshot share one lavender panel instead of sitting separately
   on the plain page background -- reads as a single block, not two.
   full-bleed: outer section spans the whole viewport width, inner wrapper
   keeps the content readable at 1200px (same pattern as .case-results) */
/* soft handoff from the page bg into the lavender panel -- fades in over
   the text (kicker/h2/lead) so the color is fully solid #E5E0FA by the
   time the screenshot card starts, instead of a hard color-block edge */
.case-platform-block{ position:relative; z-index:1; margin:0 0 100px; padding:160px 40px 80px; background:linear-gradient(to bottom, var(--bg) 0, var(--accent-teamup-tint) 340px); }
.case-platform-inner{ max-width:1200px; margin:0 auto; }
.case-platform-block h2{ font-size:32px; font-weight:500; letter-spacing:-0.8px; max-width:640px; margin:0 0 16px; }
.case-platform-shot{ margin-top:40px; }
.case-platform-shot img{ width:100%; height:auto; display:block; border-radius:var(--radius-lg); pointer-events:none; }

/* rotating gallery used for the case's opening shot -- proves "3 roles, 3
   worlds" before a single word of the Problem section is read */
/* Default aspect-ratio matches the common export size (1440x1024) -- this
   fits the LM and Admin galleries exactly since 1024 is what most of their
   slides actually are. Where a gallery's slides vary a lot (dashboards:
   1223/1080/1008), the HTML sets an inline aspect-ratio matching the
   MEDIAN/majority slide height rather than the tallest outlier: sizing to
   the tallest turned every shorter slide into one large empty block (worse
   than the gap it was meant to fix); sizing to the median means only the
   one outlier slide gets a gap, and it's a modest, symmetric one (contain
   centers it) instead of a single lopsided void under one slide. */
.case-shot-gallery{ position:relative; width:100%; aspect-ratio:1440/1024; border-radius:var(--radius-lg); overflow:hidden; box-shadow:var(--shadow-lg); background:#eeeeff; }
.gallery-slide{ position:absolute; inset:0; background-size:contain; background-repeat:no-repeat; background-position:center; background-color:#eeeeff; opacity:0; transition:opacity .6s ease; }
.gallery-slide.active{ opacity:1; }
/* a11y audit correction: gap widened from 8px to 24px so each dot's 32px
   invisible hit area (below) doesn't overlap its neighbor's (dot width 8px
   + 24px gap = 32px center-to-center, matching the hit area exactly).
   Stopped short of the full 44px target because at THIS item count/spacing
   that would force an even wider gap than looks right for a small dot
   row -- .gallery-arrow prev/next (same gallery, same action, already
   44x44) is the genuinely equivalent full-size alternative WCAG 2.5.5
   allows for. */
.gallery-dots{ position:absolute; bottom:20px; left:50%; transform:translateX(-50%); display:flex; gap:24px; z-index:2; }
.gdot{ position:relative; width:8px; height:8px; border-radius:50%; background:rgba(var(--accent-teamup-rgb),0.3); border:none; padding:0; cursor:pointer; transition:background .2s, width .2s; }
.gdot::before{ content:''; position:absolute; top:50%; left:50%; width:32px; height:32px; transform:translate(-50%,-50%); }
.gdot.active{ background:var(--accent-teamup); width:22px; border-radius:4px; }
.gdot:focus-visible{ outline:2px solid var(--accent-teamup); outline-offset:3px; }
.gallery-arrow{
  position:absolute; top:50%; transform:translateY(-50%); z-index:2;
  width:44px; height:44px; border-radius:50%; display:flex; align-items:center; justify-content:center;
  background:rgba(255,255,255,0.75); backdrop-filter:blur(var(--blur-case)); -webkit-backdrop-filter:blur(var(--blur-case));
  border:1px solid rgba(255,255,255,0.95); box-shadow:0px 8px 24px rgba(var(--accent-teamup-rgb),0.16);
  color:var(--accent-teamup); cursor:pointer; opacity:0; transition:opacity .2s ease, background .2s ease;
}
.gallery-arrow svg{ width:20px; height:20px; }
.gallery-arrow.prev{ left:16px; }
.gallery-arrow.next{ right:16px; }
.case-shot-gallery:hover .gallery-arrow{ opacity:1; }
.gallery-arrow:hover{ background:var(--accent-teamup); color:#fff; }
.gallery-arrow:focus-visible{ opacity:1; outline:2px solid var(--accent-teamup); outline-offset:2px; }
@media (hover:none){ .gallery-arrow{ opacity:1; } }

.case-contribution{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:0 40px 120px; }
.case-contribution h2{ font-size:32px; font-weight:500; letter-spacing:-0.8px; max-width:640px; margin:0 0 16px; }
/* live glass cards instead of a static persona image -- staggered like a
   little cascade, sitting on a soft indigo glow over the lavender gradient */
.persona-cards{
  position:relative; display:flex; align-items:flex-start; justify-content:space-between;
  gap:24px; margin:40px 0 8px; padding:90px 40px;
  border-radius:var(--radius-lg);
  background:radial-gradient(ellipse at 50% 45%, rgba(var(--accent-teamup-rgb),0.16), rgba(238,238,255,0) 70%), linear-gradient(160deg,#eeeeff,var(--accent-teamup-tint));
}
.persona-card{
  flex:1; background:rgba(255,255,255,0.75);
  backdrop-filter:blur(var(--blur-case)); -webkit-backdrop-filter:blur(var(--blur-case));
  border:1px solid rgba(255,255,255,0.95); border-radius:var(--radius-md); padding:28px;
  box-shadow:0px 8px 32px rgba(var(--accent-teamup-rgb),0.16);
}
.persona-card h4{ color:var(--accent-teamup); font-size:16px; font-weight:700; text-align:center; margin:0 0 12px; }
.persona-card p{ font-size:14px; font-weight:500; line-height:1.6; color:var(--text-desc); text-align:center; margin:0; }
.persona-card:nth-child(1){ margin-top:110px; }
.persona-card:nth-child(2){ margin-top:55px; }
.persona-card:nth-child(3){ margin-top:0; }
@media (max-width:860px){
  .persona-cards{ flex-direction:column; padding:32px; }
  .persona-card{ margin-top:0 !important; }
}
.case-contribution-list{ display:grid; grid-template-columns:1fr 1fr; gap:20px; margin-top:32px; }
.contrib-item{
  display:flex; flex-direction:column; gap:10px;
  background:rgba(255,255,255,0.75);
  backdrop-filter:blur(var(--blur-case)); -webkit-backdrop-filter:blur(var(--blur-case));
  border:1px solid rgba(255,255,255,0.95);
  border-radius:var(--radius-md); padding:28px;
  box-shadow:0px 8px 32px rgba(var(--accent-teamup-rgb),0.12);
}
.contrib-num{ font-size:13px; font-weight:700; color:var(--accent-teamup); letter-spacing:0.08em; }
.contrib-copy h4{ font-size:17px; font-weight:600; margin:0 0 8px; }
.contrib-copy p{ font-size:14px; font-weight:500; line-height:1.6; color:var(--text-desc); margin:0; }
@media (max-width:700px){ .case-contribution-list{ grid-template-columns:1fr; } }

.case-problem{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:0 40px 120px; }
.case-problem h2{ font-size:32px; font-weight:500; letter-spacing:-0.8px; max-width:640px; margin:0 0 16px; }
.case-lead{ font-size:16px; font-weight:500; line-height:1.6; color:var(--text-desc); max-width:640px; margin:0 0 48px; }
.case-lead.light{ color:rgba(255,255,255,0.75); }
.case-grid-2x2{ display:grid; grid-template-columns:1fr 1fr; gap:24px; }
/* glassmorphism tiles, matching Team.UP's own card token: translucent white,
   blur, hairline border, indigo-tinted soft shadow -- not the generic
   flat-text list the template shipped with */
.case-point{
  background:rgba(255,255,255,0.75);
  backdrop-filter:blur(var(--blur-case));
  -webkit-backdrop-filter:blur(var(--blur-case));
  border:1px solid rgba(255,255,255,0.95);
  border-radius:var(--radius-md);
  padding:28px;
  box-shadow:0px 8px 32px rgba(var(--accent-teamup-rgb),0.12);
}
.case-point .cicon{
  display:flex; align-items:center; justify-content:center;
  width:44px; height:44px; border-radius:var(--radius-sm);
  background:rgba(var(--accent-teamup-rgb),0.1); color:var(--accent-teamup);
  margin-bottom:16px;
}
.case-point .cicon svg{ width:22px; height:22px; }
.case-point h4{ font-size:16px; font-weight:600; margin:0 0 8px; }
.case-point p{ font-size:14px; font-weight:500; line-height:1.6; color:var(--text-desc); margin:0; }
@media (max-width:860px){ .case-grid-2x2{ grid-template-columns:1fr; } }

.case-goals{ position:relative; z-index:1; background-size:cover; background-position:center; padding:120px 40px; }
.case-goals h2, .case-goals > p, .case-goals > .case-goal-cols{ max-width:1200px; margin-left:auto; margin-right:auto; }
.case-goals h2{ color:#fff; font-size:32px; font-weight:500; letter-spacing:-0.8px; max-width:640px; margin:0 auto 16px; }
.case-goal-cols{ display:grid; grid-template-columns:repeat(3,1fr); gap:32px; margin-top:16px; }
.case-goal-cols p{ color:rgba(255,255,255,0.85); font-size:14px; font-weight:500; line-height:1.6; margin:0 0 16px; }
@media (max-width:700px){ .case-goal-cols{ grid-template-columns:1fr; gap:20px; } }
.case-pill{ display:inline-block; font-size:10px; font-weight:700; letter-spacing:0.05em; text-transform:uppercase; color:var(--case-accent); background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.18); padding:6px 14px; border-radius:999px; }

.case-platform{ position:relative; z-index:1; max-width:1200px; margin:0 auto; padding:0 40px 60px; }
.case-platform h2{ font-size:32px; font-weight:500; letter-spacing:-0.8px; max-width:640px; margin:0 0 16px; }

.case-results{ position:relative; z-index:1; margin:0 0 140px; padding:80px 40px; background:linear-gradient(160deg,#eeeeff,#f5eeff); }
.case-results-inner{ max-width:1200px; margin:0 auto; }
.case-results h2{ font-size:32px; font-weight:500; letter-spacing:-0.8px; max-width:640px; margin:0 0 40px; }
/* equal-height cards (grid default align-items:stretch) with the number
   locked to a fixed-height row via align-items:flex-end on the number
   line, so "70+", "1mo", "5" and "4" all sit on the same baseline
   regardless of digit count -- and a hairline rule marks the number/label
   split so the block reads as one aligned row, not four loose cards. */
.case-stats{ display:grid; grid-template-columns:repeat(4,1fr); gap:20px; }
.case-stat{
  display:flex; flex-direction:column;
  background:rgba(255,255,255,0.75);
  backdrop-filter:blur(var(--blur-case)); -webkit-backdrop-filter:blur(var(--blur-case));
  border:1px solid rgba(255,255,255,0.95);
  border-radius:var(--radius-md); padding:28px 24px;
  box-shadow:0px 8px 32px rgba(var(--accent-teamup-rgb),0.12);
}
.cnum{
  display:flex; align-items:baseline; height:44px;
  font-size:40px; font-weight:700; letter-spacing:-1.2px; line-height:1;
  color:var(--accent-teamup); font-variant-numeric:tabular-nums;
  border-bottom:1px solid rgba(var(--accent-teamup-rgb),0.14);
  margin-bottom:14px; padding-bottom:14px;
}
/* solid color, not a lowered-opacity tint -- 0.65 alpha over a translucent
   glass card dropped this under the 4.5:1 AA text-contrast minimum. Visual
   "secondary" weight now comes from size only, not from being harder to read. */
.cunit{ font-size:17px; font-weight:600; margin-left:4px; color:var(--accent-teamup); }
.clabel2{ font-size:13px; font-weight:500; line-height:1.5; color:var(--text-desc); }
@media (max-width:860px){ .case-stats{ grid-template-columns:1fr 1fr; gap:24px; } }
@media (max-width:560px){ .case-stats{ grid-template-columns:1fr; } }
@media (max-width:500px){ .case-results{ padding:48px 24px; } }

/* ============ ZERO STATION CASE -- bespoke elements ============
   The presentation deck this case is built from has its own visual
   language: blue numbered blocks, callout boxes explaining a decision,
   and a side-by-side comparison against generic gaming-club sites. These
   are scoped under .zs- so they only ever apply on this one case page. */
.zs-accent{ --accent-zs:#3B82F6; --accent-zs-rgb:59,130,246; }

.zs-block-head{ display:flex; align-items:center; gap:16px; margin:0 0 14px; }
.zs-num{
  display:flex; align-items:center; justify-content:center; flex:none;
  width:38px; height:38px; border-radius:var(--radius-sm);
  background:var(--accent-zs);
  /* white-on-#3B82F6 only reaches 3.68:1 (fails AA 4.5:1 for normal text) --
     dark navy text on the same blue reaches 5.36:1, so the fix is the text
     color, not a different (off-brand) badge blue */
  color:#040B18;
  font-family:'IBM Plex Mono',monospace; font-weight:600; font-size:14px;
  letter-spacing:-0.02em;
}
.zs-block-head h2{ margin:0; }
.zs-block-head + .case-lead{ margin-top:0; }

.zs-callout{
  display:flex; gap:14px; align-items:flex-start;
  background:rgba(var(--accent-zs-rgb),0.07);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
  border:1px solid rgba(var(--accent-zs-rgb),0.22);
  border-left:3px solid var(--accent-zs);
  padding:18px 22px; border-radius:var(--radius-sm);
  margin:28px 0 0;
}
.zs-callout .zs-callout-icon{
  flex:none; width:22px; height:22px; margin-top:1px; color:var(--accent-zs);
}
.zs-callout-label{
  font-family:'IBM Plex Mono',monospace; font-size:10px; font-weight:700;
  letter-spacing:0.08em; text-transform:uppercase; color:var(--accent-zs);
  display:block; margin-bottom:6px;
}
.zs-callout p{ margin:0; font-size:14px; line-height:1.6; color:var(--text-desc); }
/* The shared .case-goals template centers each child individually (h2 /
   direct >p / >.case-goal-cols) against the section's own full-bleed padded
   box -- fine when every child shares the same max-width, but the callout
   div and the astronaut image aren't in that selector list, and h2's own
   narrower 640px box centers *again* inside the already-centered column,
   drifting away from the kicker/lead. Zero Station needs the background to
   still bleed full-width, so instead of patching each child, everything
   that needs to line up sits inside one real 1200px column (.zs-goals-inner)
   -- the single source of truth for the left/right edges in this section. */
.zs-goals-inner{ position:relative; max-width:1200px; margin:0 auto; }
.case-goals .zs-callout{
  background:rgba(var(--accent-zs-rgb),0.16); border-color:rgba(var(--accent-zs-rgb),0.35);
  margin:28px 0 0; padding:22px 26px;
}
.case-goals .zs-callout p{ color:rgba(255,255,255,0.78); }
/* h2 is still matched by the shared .case-goals h2{ margin:0 auto 16px }
   rule, which would center its 640px box inside .zs-goals-inner -- override
   so it stays flush with the kicker/lead/cards above and below it. */
.zs-dark .case-goals h2{ margin:0 0 16px; }
/* tighten the lead->cards gap: case-lead already carries margin-bottom:48px,
   so the extra 16px top margin on the cards on top of that read as an
   uneven double-gap next to the tighter 28px card->callout spacing. */
.zs-dark .case-goal-cols{ margin-top:0; }

/* three callouts side by side, under the UI-kit screenshot -- each one is
   the designer's own rationale for one system (type / color / shape) */
.zs-callout-grid{
  max-width:1200px; margin:32px auto 0; padding:0 40px 100px;
  display:grid; grid-template-columns:repeat(3,1fr); gap:20px;
}
.zs-callout-grid .zs-callout{ margin:0; flex-direction:column; gap:8px; }
.zs-callout-grid .zs-callout-icon{ margin-top:0; }
@media (max-width:860px){ .zs-callout-grid{ grid-template-columns:1fr; } }

/* comparison block -- generic industry pattern vs. this project, laid out
   like a two-column spec sheet rather than a marketing "vs" table */
.zs-compare{
  display:grid; grid-template-columns:1fr 1fr; margin-top:36px;
  border-radius:var(--radius-md); overflow:hidden;
  border:1px solid rgba(1,6,13,0.08);
}
.zs-compare-col{ padding:28px 30px; background:#fff; }
.zs-compare-col.zs-col-them{ background:rgba(1,6,13,0.03); }
.zs-compare-col.zs-col-us{ background:rgba(var(--accent-zs-rgb),0.05); }
.zs-compare-head{
  display:flex; align-items:center; gap:8px; margin:0 0 18px;
  font-family:'IBM Plex Mono',monospace; font-size:11px; font-weight:700;
  /* was --text-gray (#828282, ~3.58:1 on this white column — fails 4.5:1 at
     this size). --text-desc is already used correctly elsewhere on this
     same page/table (~7:1) so this just matches that. */
  letter-spacing:0.06em; text-transform:uppercase; color:var(--text-desc);
}
.zs-compare-col.zs-col-us .zs-compare-head{ color:var(--accent-zs); }
.zs-compare-row{
  display:flex; gap:12px; padding:12px 0; font-size:14px; line-height:1.5;
  border-top:1px solid rgba(1,6,13,0.06); color:var(--text-desc);
}
.zs-compare-row:first-of-type{ border-top:none; padding-top:0; }
.zs-compare-tag{
  flex:none; font-family:'IBM Plex Mono',monospace; font-size:10px; font-weight:700;
  /* same contrast fix as .zs-compare-head above. */
  letter-spacing:0.04em; text-transform:uppercase; color:var(--text-desc);
  width:88px;
}
.zs-compare-col.zs-col-us .zs-compare-tag{ color:var(--accent-zs); }
@media (max-width:700px){
  .zs-compare{ grid-template-columns:1fr; }
  .zs-compare-col.zs-col-them{ border-bottom:1px solid rgba(1,6,13,0.08); }
}
/* below ~480px the fixed 88px tag column + 60px of column padding left the
   description text only ~215px to wrap in -- stack tag above text instead
   of beside it, and trim the column padding, so it reads normally on a
   small phone instead of looking cramped */
@media (max-width:480px){
  .zs-compare-col{ padding:22px 18px; }
  .zs-compare-row{ flex-direction:column; gap:4px; }
  .zs-compare-tag{ width:auto; }
}

/* results section on this case runs blue, not the shared indigo token */
.case-results.zs-results .cnum,
.case-results.zs-results .cunit{ color:var(--accent-zs); }
.case-results.zs-results .case-stat{ box-shadow:0px 8px 32px rgba(var(--accent-zs-rgb),0.12); }
.case-results.zs-results .cnum{ border-bottom:1px solid rgba(var(--accent-zs-rgb),0.14); }

/* these two icon/pill components default to the shared TeamUp indigo --
   re-tint to this case's own blue regardless of light/dark mode */
.zs-accent .case-point .cicon{ background:rgba(var(--accent-zs-rgb),0.12); color:var(--accent-zs); }
.zs-accent .case-pill{ color:var(--accent-zs); }

/* ---- full dark theme for the Zero Station case body ----
   The product itself is dark navy; sitting it inside the site's default
   light case template (built for TeamUp/Orbit/Pulse) made every screenshot
   read as a dark rectangle dropped on a white page. This repaints every
   section between the hero and the results band so the whole case runs on
   one background, the way the actual product does. Scoped to .zs-dark so
   nothing else on the site is touched -- nav and footer stay the shared
   site chrome. */
.zs-dark{ background:#040B18; position:relative; }
/* ambient blue bloom, fixed so it doesn't repeat/tile as the page scrolls --
   sections already carry position:relative + z-index:1 (or get it added
   just below), so this sits behind all real content */
.zs-dark::before{
  content:''; position:fixed; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(900px circle at 10% 0%, rgba(59,130,246,0.16), transparent 55%),
    radial-gradient(700px circle at 95% 28%, rgba(59,130,246,0.12), transparent 55%),
    radial-gradient(750px circle at 12% 92%, rgba(59,130,246,0.10), transparent 55%);
}
.zs-dark .case-ds-pair,
.zs-dark .zs-callout-grid,
.zs-dark .ticker-wrap,
.zs-dark footer{ position:relative; z-index:1; }
.zs-dark .ticker-wrap{ background:#040B18; border-top-color:rgba(255,255,255,0.12); border-bottom-color:rgba(255,255,255,0.12); }
.zs-dark .ticker-track{ color:rgba(255,255,255,0.55); }
.zs-dark .ticker-track span.sep{ color:rgba(255,255,255,0.28); }
.zs-dark .case-overview-labels .olabel-row{ border-top-color:rgba(255,255,255,0.12); }
.zs-dark .case-overview-labels .olabel-row:last-child{ border-bottom-color:rgba(255,255,255,0.12); }
.zs-dark .olabel{ color:rgba(255,255,255,0.5); }
.zs-dark .oval{ color:#fff; }
.zs-dark .okicker{ color:rgba(255,255,255,0.5); }
.zs-dark .case-overview-copy h2,
.zs-dark .case-platform h2,
.zs-dark .case-problem h2,
.zs-dark .case-results h2{ color:#fff; }
.zs-dark .case-overview-copy p{ color:rgba(255,255,255,0.68); }
.zs-dark .case-lead{ color:rgba(255,255,255,0.65); }
/* glassmorphism, darker pass: the first version's 9% white fill + a heavy
   black drop-shadow (0 20px 40px) read as a muddy halo once it sat on the
   page's near-black background -- a black shadow only reads "dirty" when
   there's nothing lighter to cast it against. Darker fill, border-only
   edge definition, no external shadow. */
.zs-dark .case-point{
  background:rgba(255,255,255,0.035);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
  border:1px solid rgba(255,255,255,0.1);
  box-shadow:inset 0 1px 0 rgba(255,255,255,0.05);
}
.zs-dark .case-point h4{ color:#fff; }
.zs-dark .case-point p{ color:rgba(255,255,255,0.6); }
/* glow kept reading as a smudge no matter how much it was toned down --
   dropped it and just darkened the chip itself (the UI kit's own "surface"
   navy, #000E25) so the icon sits on a solid, defined square instead */
.zs-dark .case-point .cicon{ background:rgba(0,14,37,0.9); box-shadow:none; }
.zs-dark .case-shot-img,
.zs-dark .ds-pane img{ background:#000E25; }
/* flat black (rgba(0,0,0,.5)) mixing with the near-black navy page
   background read muddy for the same reason .case-point's old shadow did --
   swapped for the site's own lilac shadow tint (--shadow-tint) so it reads
   as a soft, colored glow instead of a grey/brown smudge. */
.zs-dark .case-shot-img{ box-shadow:0 24px 60px rgba(var(--shadow-tint),0.5); }
.zs-dark .ds-pane img{ box-shadow:0 24px 60px rgba(var(--shadow-tint),0.5); }
.zs-dark .case-results{ background:transparent; }
.zs-dark .case-stat{
  background:linear-gradient(160deg, rgba(255,255,255,0.09), rgba(255,255,255,0.02));
  border:1px solid rgba(255,255,255,0.14);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
}
.zs-dark .clabel2{ color:rgba(255,255,255,0.6); }
.zs-dark .case-pill{ background:rgba(255,255,255,0.08); border-color:rgba(255,255,255,0.16); }
/* goal-column text was floating bare on the grid background with no
   container -- give each one an actual glass card like the rest of the page */
.zs-dark .case-goal-cols > div{
  background:rgba(255,255,255,0.05);
  backdrop-filter:blur(var(--blur-card)); -webkit-backdrop-filter:blur(var(--blur-card));
  border:1px solid rgba(255,255,255,0.12);
  border-radius:var(--radius-md);
  padding:22px 20px;
}

/* decorative astronaut cutouts -- sit in the empty gutter OUTSIDE the
   1200px content column, never inside the text/card grid itself. The
   host sections cap at max-width:1200px, so "right:-Npx" / "left:-Npx"
   here is measured from THAT box's edge, pushing the figure out into the
   page's outer margin rather than overlapping any text. Only shown once
   the viewport is wide enough to hold the full figure in that margin
   (>=1680px); body already has overflow-x:hidden so nothing here can
   cause a horizontal scrollbar on narrower screens. */
.zs-astro{
  position:absolute; top:50%; transform:translateY(-50%);
  width:200px; height:auto; z-index:1; pointer-events:none;
  filter:drop-shadow(0 0 46px rgba(59,130,246,0.4));
  display:none;
}
@media (min-width:1680px){ .zs-astro{ display:block; } }
/* was right/left:-80px -- since the image itself is 200px wide, an 80px
   offset only pushes the *anchor edge* 80px out, leaving the other 120px
   of the image hanging over the content column and covering the first
   card's text. -220px clears the full image width plus a 20px gap. */
.zs-astro-right{ right:-220px; }
.zs-astro-left{ left:-220px; }
.zs-dark .zs-callout p{ color:rgba(255,255,255,0.65); }
.zs-dark .zs-compare-col{ background:rgba(255,255,255,0.03); }
.zs-dark .zs-compare-col.zs-col-them{ background:rgba(255,255,255,0.02); }
.zs-dark .zs-compare-col.zs-col-us{ background:rgba(var(--accent-zs-rgb),0.08); }
.zs-dark .zs-compare{ border-color:rgba(255,255,255,0.1); }
.zs-dark .zs-compare-row{ border-top-color:rgba(255,255,255,0.08); color:rgba(255,255,255,0.6); }
/* 0.4 alpha measured at 3.77:1 against the dark base -- fails AA (4.5:1
   minimum for normal-size text). 0.5 matches the site's existing
   de-emphasized-label opacity (okicker/olabel) and clears it at 5.32:1 */
.zs-dark .zs-compare-head{ color:rgba(255,255,255,0.5); }
@media (max-width:700px){
  .zs-dark .zs-compare-col.zs-col-them{ border-bottom-color:rgba(255,255,255,0.1); }
}