/* =========================================================
   Jasmine Bontha — Portfolio design system
   Cosmic motif. See CLAUDE.md for the rationale.
   ========================================================= */

/* Typed as <angle> so browsers that support @property (most current ones)
   can smoothly interpolate --tilt-x/--tilt-y when they change — used by
   the magnetic card-tilt effect (initTiltCards() in js/main.js). Falls
   back gracefully to an instant (untyped) change in older browsers;
   nothing breaks, the tilt just isn't eased on those. */
@property --tilt-x {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}
@property --tilt-y {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

/* Native cross-document View Transitions — a quiet wormhole feel between
   full page loads (index.html <-> case-studies/*.html, and case study <->
   case study via the prev/next links) instead of the browser's default
   hard cut: the old page eases down and brightens slightly as if it's
   receding into a point of light, then the new page eases in from that
   same soft glow and settles. A restrained version of "shrink into
   light / emerge from light" rather than a showy zoom — a small signature
   beat, not a spectacle. Pure CSS, no router/framework/JS (rule 3: no
   dependency creep) — browsers without support (Firefox, at the time of
   writing) just navigate normally with no transition, so this is
   enhancement-only and never blocks navigation. Scoped sitewide via this
   shared stylesheet rather than only on the links into case studies, so
   every full-page navigation on the site feels consistent rather than
   some jumping and others transitioning. */
@view-transition {
  navigation: auto;
}

::view-transition-old(root) {
  animation: wormhole-suck-in .32s ease-in forwards;
}

::view-transition-new(root) {
  animation: wormhole-burst-out .4s cubic-bezier(.2, .7, .3, 1) forwards;
}

@keyframes wormhole-suck-in {
  0%   { opacity: 1; scale: 1;    rotate: 0deg;    filter: blur(0px) brightness(1); }
  100% { opacity: 0; scale: .94;  rotate: -1.5deg; filter: blur(3px) brightness(1.12); }
}

@keyframes wormhole-burst-out {
  0%   { opacity: 0; scale: .95; rotate: 1deg; filter: blur(3px) brightness(1.12); }
  100% { opacity: 1; scale: 1;   rotate: 0deg; filter: blur(0px) brightness(1); }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}

/* Self-hosted rather than linked from fonts.googleapis.com: privacy-focused
   browsers and extensions (Brave's shields, Safari's cross-site tracking
   prevention, many ad blockers) block Google Fonts requests by default,
   which silently fell back to a generic serif/cursive system font — the
   nav, logo, and every heading on the live GitHub Pages deploy rendered in
   the wrong typeface for anyone with those protections on, even though
   the page itself loaded fine. Self-hosting removes the third-party
   request entirely, so the type is never at the mercy of the visitor's
   browser privacy settings. Latin-subset .woff2 only (this site has no
   non-Latin copy) — same files Google itself would have served.
   Regenerate from https://fonts.google.com if new weights are needed. */
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/baloo2-v23-latin-var.woff2') format('woff2-variations'), url('../fonts/baloo2-v23-latin-var.woff2') format('woff2');
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/inter-v20-latin-var.woff2') format('woff2-variations'), url('../fonts/inter-v20-latin-var.woff2') format('woff2');
}

* { margin: 0; padding: 0; box-sizing: border-box; }

/* =========================================================
   Art direction: Bee and PuppyCat mood
   The rainbow palette was repainted from vivid neon toward soft, dusty
   pastel hues — dusty rose, peach, muted gold, pale mint, sky blue,
   lavender — for a cozier, more whimsical feel, "a magical mailbox
   floating in space" rather than a technical/corporate one. This was
   first tried as a FULL light-mode flip (cream background, dark text)
   and Jasmine didn't like that — the background is back to the
   original dark cosmic void (#07092a). What stuck from the pastel pass:
   the accent palette itself, Baloo 2 as the display font, and softened/
   recolored versions of the decorative extras added alongside it
   (drifting clouds, occasional shooting stars, the floating mail
   envelope, warm plum-tinted shadows instead of flat black). Every
   rainbow token below KEPT ITS ORIGINAL NAME (--c-red, --d-blue, etc.)
   even though its hue changed — every rule elsewhere in this file that
   already reads var(--c-red) etc. picked up the new palette automatically
   with zero changes to that rule. Keep extending the palette this same
   way (new value, same slot) rather than introducing parallel new tokens.
   The two-tier structure is back to its original meaning now that the
   background is dark again: --c-* (soft pastel) is the vivid-on-dark
   decorative tier (glows, thin borders, dots, planet fills); --d-* (deep)
   is for AA-safe text on the light nav/paper surfaces, and for white text
   sitting on top of a button/badge fill — --c-* is too light for that.
   ========================================================= */
/* CURRENT PALETTE (supersedes the history above): shifted to match
   Jasmine's own Carrd site (sqeakerz.carrd.co), a saturated cobalt-navy
   sky, white/periwinkle sparkles, a vivid rainbow, and violet buttons.
   Same token names, new values; see CLAUDE.md "Art direction" for the
   contrast-checked table. */
:root {
  /* space + surfaces — dark cosmic base (Jasmine's call after seeing the
     full light-cream version: keep the pastel accent palette, but back on
     a dark night-sky background). Tinted deep indigo-violet rather than
     near-black, per a moodboard reference Jasmine shared (a glowing
     cyan/teal night scene) — a lighter, more saturated "moonlit" dark
     rather than a flat void. */
  --bg: #161966;
  --bg-soft: #1f247b;
  --panel: rgba(255, 255, 255, .05);
  --panel-border: rgba(255, 255, 255, .1);
  --paper: #f4faf8;
  --ink: #1d2162;
  /* The nav's own surface — a smidge darker than --bg, not the light
     "paper" surface used for planet-card tooltips and Free Draw's paper
     texture (those stay light/cream on purpose; only the nav itself
     moved to dark). */
  --nav-bg: #0e1045;

  /* text on dark */
  --text: #ffffff;
  --text-dim: #dde0fb;
  --text-mute: #b0b6e5;

  /* soft pastel tier — decorative use (glows, thin borders, dots),
     never as a fill under white text (see note above). Red and teal
     nudged toward coral-salmon and a more vivid glowing cyan respectively
     — closer to a moodboard reference Jasmine shared (coral-pink stars,
     glowing teal clouds, deep indigo night). Saturation raised a notch
     sitewide (both tiers, ~+16% HSL saturation here / ~+8% on the deep
     tier below) at Jasmine's request for a bit more punch than the very
     dusty original pastel pass — still a soft palette, not a return to
     neon. The deep tier's boost was capped smaller and re-checked against
     white and #f4faf8 (the paper/nav surface) so every AA text-contrast
     pairing documented in CLAUDE.md still holds; --d-green has the
     tightest margin (~4.6:1) — don't push its saturation any further
     without re-checking contrast. */
  --c-red: #f17d89;
  --c-orange: #eea663;
  --c-yellow: #efd96e;
  --c-green: #90e369;
  --c-blue: #6cbeef;
  --c-purple: #b79ff4;
  --c-pink: #dc7bf0;
  /* Added from Jasmine's cosmic-creature moodboard reference — a mint/teal
     that the original 6-hue rainbow didn't have. Accent-only, like pink:
     no --d-teal counterpart, since it isn't used as nav/text-on-white. */
  --c-teal: #60dbed;

  /* deep tier — saturated enough for AA-safe text-on-cream AND for white
     text sitting on top of it as a button/badge fill */
  --d-red: #b13556;
  --d-orange: #98521c;
  --d-gold: #6e580c;
  --d-green: #28713e;
  --d-blue: #3058c3;
  --d-purple: #7253d0;

  --gradient-rainbow: linear-gradient(90deg, var(--c-red), var(--c-orange), var(--c-yellow), var(--c-green), var(--c-blue), var(--c-purple));

  --radius-lg: 30px;
  --radius-md: 20px;
  --radius-sm: 12px;
  --nav-h: 76px;

  /* A third, deliberately plainer type voice — system monospace, no extra
     font file — reserved for "meta" marks (eyebrows, running heads,
     section numbers, dates). Pairing a technical/editorial mono against
     the handwritten-feeling display face and the humanist body face is
     what gives the type system a magazine-like, considered contrast
     instead of reading as one generic sans-everywhere template. */
  --font-mono: ui-monospace, 'SF Mono', 'Menlo', 'Cascadia Code', 'Courier New', monospace;
}

html { scroll-behavior: smooth; }

body {
  font-family: 'Inter', sans-serif;
  background: var(--bg);
  color: var(--text);
  overflow-x: hidden;
}

/* Reference mood: a dark starry sky above, warm magenta/gold/purple
   nebula clouds glowing up from below — recreated with the site's own
   CSS-only techniques (gradients + blur), not an embedded image, per the
   "no raster assets" rule; this is a from-scratch match to that mood, not
   a copy of the reference artwork itself. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  background:
    radial-gradient(circle at 22% 14%, rgba(96, 219, 237, .12), transparent 30%),
    radial-gradient(circle at 78% 18%, rgba(183, 159, 244, .1), transparent 32%),
    radial-gradient(circle at 18% 98%, rgba(241, 125, 137, .12), transparent 42%),
    radial-gradient(circle at 50% 102%, rgba(96, 219, 237, .13), transparent 46%),
    radial-gradient(circle at 82% 98%, rgba(183, 159, 244, .12), transparent 42%);
  z-index: -2;
  pointer-events: none;
}

/* Aurora — tall, soft-edged color curtains, heavily blurred and gently
   drifting, glowing UP from the bottom of the viewport (not draping down
   from the top). screen blend mode (correct again now the background is
   dark — it was briefly switched to multiply during a light-background
   version of this palette that didn't stick) so the pastel colors glow
   and layer over each other and the starfield instead of sitting as flat
   tinted panels. Opacities were lowered ~40% in the accessibility pass:
   at full strength the bottom of the viewport got light enough that
   muted text scrolling over it fell below 4.5:1. Re-check contrast
   before brightening any of these again. */
body::after {
  content: '';
  position: fixed;
  inset: -15% -20%;
  background:
    radial-gradient(ellipse 34% 125% at 12% 118%, rgba(96, 219, 237, .2), transparent 60%),
    radial-gradient(ellipse 30% 115% at 38% 124%, rgba(241, 125, 137, .16), transparent 58%),
    radial-gradient(ellipse 32% 125% at 65% 118%, rgba(183, 159, 244, .17), transparent 60%),
    radial-gradient(ellipse 28% 110% at 90% 124%, rgba(96, 219, 237, .14), transparent 56%);
  filter: blur(55px);
  mix-blend-mode: screen;
  z-index: -2;
  pointer-events: none;
  animation: aurora-drift 22s ease-in-out infinite alternate;
}

@keyframes aurora-drift {
  0%   { transform: translateX(-2%) scaleY(1); }
  50%  { transform: translateX(1.5%) scaleY(1.06); }
  100% { transform: translateX(-1%) scaleY(0.97); }
}

.stars {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
}

/* A faint, always-on film-grain texture over the whole page — procedural
   (SVG feTurbulence, not a raster asset), so nothing here is a flat,
   untextured gradient the way a template background usually is. Sits
   above everything (z-index above content) but visually reads as
   underneath, since it's this subtle and additive-blended — the point is
   a hand-printed/paper tactility, not a visible effect on its own. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 250;
  pointer-events: none;
  opacity: .05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

@media (prefers-reduced-motion: reduce), (max-width: 500px) {
  .grain { display: none; }
}

.stars .star {
  position: absolute;
  /* Warm gold by default — set per-star in JS (initStarfield()) to one of
     a few soft pastel hues for variety, rather than plain white, to match
     the site's pastel Bee-and-PuppyCat palette. */
  background: var(--star-color, #e2e7fc);
  border-radius: 50%;
  animation: star-twinkle var(--dur, 3.4s) ease-in-out infinite;
  animation-delay: var(--delay, 0s);
}

@keyframes star-twinkle {
  0%, 100% { opacity: calc(var(--max-op, .5) * .2); transform: scale(.75); }
  50% { opacity: var(--max-op, .5); transform: scale(1.1); }
}

/* Fixed layer for the occasional shooting stars, pinned to the viewport
   like .stars rather than scrolling with the page. Used to also hold the
   background planets/moons (removed at Jasmine's request); kept as its
   own fixed wrapper since the shooting stars still need one. */
.shooting-stars {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
}

/* A short bright streak with a fading tail, at rest at 0 width off in a
   corner — the keyframes hold it invisible for the vast majority of each
   long cycle, then flash it across the sky once. Two instances with very
   different durations/delays so they don't fall into a visible rhythm. */
.shooting-star {
  position: absolute;
  top: 8%;
  left: 60%;
  width: 90px;
  height: 2px;
  border-radius: 2px;
  /* Bright head at the left end, fading tail at the right (pivot) end —
     with transform-origin:right and the flight path heading down-left,
     the left end is the leading edge. Bright-at-left/fade-at-right is
     what makes the head lead the direction of travel instead of trailing
     behind it. */
  background: linear-gradient(90deg, #f4faf8, #e2e7fc 40%, transparent);
  opacity: 0;
  transform-origin: right center;
  rotate: -22deg;
  animation: shooting-star-fly 17s ease-in infinite;
}

.shooting-star-a { top: 10%; left: 55%; animation-duration: 19s; }
.shooting-star-b { top: 30%; left: 20%; width: 70px; animation-duration: 26s; animation-delay: -9s; }

@keyframes shooting-star-fly {
  0%, 88% { opacity: 0; translate: 0 0; }
  90% { opacity: 1; translate: -6vw 4vh; }
  96% { opacity: 1; translate: -22vw 14vh; }
  97%, 100% { opacity: 0; translate: -22vw 14vh; }
}

@media (max-width: 760px) {
  .shooting-stars { display: none; }
}

/* A real weight scale instead of one flat 700 everywhere — h1 heaviest,
   stepping down through h2/h3, so hierarchy reads from weight as well as
   size. .hand (footer's closing line) matches h2's weight, since it's
   used at that level of emphasis. */
h1, .hand { font-family: 'Baloo 2', cursive; font-weight: 800; line-height: 1.05; }
h2 { font-family: 'Baloo 2', cursive; font-weight: 700; line-height: 1.05; }
h3 { font-family: 'Baloo 2', cursive; font-weight: 600; line-height: 1.15; }

.eyebrow {
  color: var(--text-mute);
  letter-spacing: 2px;
  font-size: .78rem;
  font-weight: 500;
  font-family: var(--font-mono);
  text-transform: uppercase;
  margin-bottom: 18px;
}

.gradient {
  background: var(--gradient-rainbow);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* Jasmine's full name in the hero: her own hand-drawn "Jasmine" wordmark
   (the same images/logo-jasmine.png as the nav logo) with "Bontha" set in
   type underneath. The source art's letters ("asmine") sit on a baseline
   ~55% of the way down its canvas; everything below that is empty except
   the J's long tail sweeping down on the left (measured off the PNG's
   alpha channel). The surname is tucked into that empty lower-right
   pocket, under "asmine" and right of the J's tail, so the two words read
   as one lockup instead of two stacked lines. The margins are fractions
   of the wordmark's own 2.2em height, converted into the surname's
   smaller em: re-measure the art before changing either size. */
.hero-name-mark {
  display: block;
  height: 2.2em;
  width: auto;
  /* The J's top stroke reaches the very top of the canvas, so no
     negative margin here or it collides with the eyebrow above. */
  margin-top: .1em;
}

.hero-surname {
  display: block;
  font-size: .56em;
  line-height: 1;
  color: var(--text);
  letter-spacing: .01em;
  /* -(1 - .58) * 2.2em of the h1, in this span's .56em units */
  margin-top: -1.62em;
  /* ~30% of the wordmark's width (2.2em * 700/343), clearing the J's tail */
  margin-left: 2.4em;
}

/* Single-hue accents for small emphasis words — an alternative to reaching
   for the full rainbow gradient every time a word needs a highlight. */
.accent-pink { color: var(--c-pink); }
.accent-yellow { color: var(--c-yellow); }
.accent-green { color: var(--c-green); }
.accent-teal { color: var(--c-teal); }

a { color: inherit; }

.container { max-width: 1280px; margin: 0 auto; padding: 0 8%; }

/* =========================================================
   Scroll reveal — a quiet fade/rise as section content enters the
   viewport, so the page reads as a guided sequence rather than
   everything just sitting there at once. initScrollReveal() in
   js/main.js applies .is-hidden (never CSS-default) right before
   observing, so a visitor without JS simply sees every element in its
   normal, fully visible resting state — this is enhancement only, never
   the only path to content being visible.
   ========================================================= */
.reveal { transition: opacity .8s cubic-bezier(.16,.8,.3,1), transform .8s cubic-bezier(.16,.8,.3,1); }
.reveal.is-hidden { opacity: 0; transform: translateY(30px); }
.reveal.is-visible { opacity: 1; transform: translateY(0); }

/* Fade-only variant for grids of cards/planets already carrying their own
   hover transform — avoids two different-duration transitions competing
   for the same `transform` property. */
.reveal-fade { transition: opacity .9s ease; }
.reveal-fade.is-hidden { opacity: 0; }
.reveal-fade.is-visible { opacity: 1; }

/* Slides in from the right + fades, for a one-off hero-ish moment like
   the About avatar (which already sits on the right side of its grid
   column, so entering from that side reads as arriving into place rather
   than crossing the whole layout) — safe to use transform here since
   that element carries no hover transform of its own to compete with
   (see the note on .reveal-fade above). */
.reveal-right { transition: opacity .8s cubic-bezier(.16,.8,.3,1), transform .8s cubic-bezier(.16,.8,.3,1); }
.reveal-right.is-hidden { opacity: 0; transform: translateX(70px); }
.reveal-right.is-visible { opacity: 1; transform: translateX(0); }

/* Reduced motion: every animation and transition on the site (pseudo-
   elements included) jumps straight to its end state. A near-zero
   duration instead of `animation: none` so anything that animates in
   from opacity 0 (hero entrance, scroll reveals) still lands visible. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    transition-delay: 0s !important;
  }
}

/* =========================================================
   Calm mode (html.calm) — toggled from the nav (initCalmMode() in
   js/main.js), on by default when the OS asks for reduced motion.
   Goes further than reduced motion alone: stops all motion, removes or
   dims the busy background layers (shooting stars, grain, glows, comet
   trail, twinkling stars), brings back the normal mouse pointer, and
   lifts secondary text closer to white for more contrast.
   ========================================================= */
html.calm {
  scroll-behavior: auto;
  --text-dim: #f1f2fe;
  --text-mute: #d6d9f6;
}
html.calm *, html.calm *::before, html.calm *::after {
  animation-duration: .001ms !important;
  animation-delay: 0s !important;
  animation-iteration-count: 1 !important;
  transition-duration: .001ms !important;
  transition-delay: 0s !important;
}
html.calm .shooting-stars,
html.calm .grain,
html.calm .hero-glow,
html.calm .comet-trail,
html.calm .comet-star,
html.calm .cursor,
html.calm .cursor-burst,
html.calm .doodle-burst,
html.calm .rabbit-hop { display: none !important; }
html.calm .stars { opacity: .4; }
html.calm body::before,
html.calm body::after { opacity: .35; }
html.calm .bg-squiggle { opacity: .4; }
html.calm::view-transition-old(root),
html.calm::view-transition-new(root) { animation: none; }
@media (hover: hover) and (pointer: fine) {
  html.calm * { cursor: auto !important; }
  html.calm :is(a[href], button, [role="button"], summary, label[for]) { cursor: pointer !important; }
  html.calm :is(input, textarea) { cursor: text !important; }
}

/* =========================================================
   Custom cursor — a spark/star by default, growing into a spinning
   "CLICK ME!" badge over anything clickable. Built and positioned in
   initCustomCursor() in js/main.js; this just styles/hides it. Scoped to
   real mouse pointers
   (hover:hover + pointer:fine) so touch devices keep their native tap
   behavior untouched — a fake cursor has no meaning without a mouse to
   drive it. `* { cursor: none }` needs !important here because several
   elements already set an explicit `cursor: pointer` (buttons, .btn,
   .planet, etc.) that would otherwise win over a plain inherited value.
   ========================================================= */

@media (hover: hover) and (pointer: fine) {
  * { cursor: none !important; }

  .cursor {
    position: fixed;
    top: 0; left: 0;
    width: 28px; height: 28px;
    transform: translate(-50%, -50%);
    /* Above .theater-modal (2000) too — otherwise the fake cursor
       vanishes behind the Playground/Animations theater overlay while
       it's open, since a fixed-position element with a lower z-index
       renders underneath one with a higher z-index regardless of DOM
       order. */
    z-index: 2100;
    pointer-events: none;
    transition: opacity .15s ease, width .2s ease, height .2s ease;
    /* Blue glow outline around the default star — dropped entirely over
       the "click me" badge below, so the glow doesn't compete with the
       badge's own outline/text at that larger size. */
    filter:
      drop-shadow(0 0 4px rgba(108, 190, 239, .9))
      drop-shadow(0 0 10px rgba(108, 190, 239, .6));
  }

  /* Grows into the "click me" badge's size over anything clickable — the
     star is small and precise, but "CLICK ME!" running around a circle
     needs real room to be legible. */
  .cursor.is-pointer {
    width: 108px;
    height: 108px;
    filter: none;
  }

  /* Wraps the two icon svgs — holds the click-flash scale animation on
     its own transform, separate from .cursor's (mousemove-driven
     position) and .cursor-icon--badge's (continuous spin), so the three
     never fight over the same property. */
  .cursor-flash {
    position: relative;
    width: 100%; height: 100%;
  }

  .cursor-flash.is-flashing { animation: cursor-flash .4s ease-out; }

  @keyframes cursor-flash {
    0% { transform: scale(1); filter: brightness(1); }
    25% { transform: scale(1.8); filter: brightness(2.2); }
    100% { transform: scale(1); filter: brightness(1); }
  }

  .cursor-icon {
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    color: var(--text);
    transition: opacity .15s ease;
  }

  .cursor-icon--star { opacity: 1; }

  .cursor-icon--badge {
    opacity: 0;
    /* Yellow, not blue — needs to read clearly over the pink/blue CTA
       button gradient it's most often hovering, and yellow contrasts
       cleanly against both ends of that gradient. */
    color: var(--c-yellow);
    animation: cursor-spin 6s linear infinite;
    animation-play-state: paused;
  }

  .cursor-icon--badge circle { fill: none; stroke: currentColor; stroke-width: 1.5; opacity: .5; }
  /* The center point — solid fill, full opacity, overriding the ring's
     stroke-only/half-opacity rule above (same selector specificity, so
     this needs to come after it to win). */
  .cursor-icon--badge .cursor-badge-dot { fill: currentColor; stroke: none; opacity: 1; }
  .cursor-icon--badge text {
    font-family: 'Baloo 2', cursive;
    font-size: 16px;
    letter-spacing: 1.5px;
    fill: currentColor;
  }

  .cursor.is-pointer .cursor-icon--star { opacity: 0; }
  .cursor.is-pointer .cursor-icon--badge {
    opacity: 1;
    animation-play-state: running;
  }

  @keyframes cursor-spin {
    to { transform: rotate(360deg); }
  }

  /* Burst particles — small sparks flung outward from the click point,
     one JS-created element per spark, self-removing on animationend. */
  .cursor-burst {
    position: fixed;
    top: 0; left: 0;
    width: 10px; height: 10px;
    margin: -5px 0 0 -5px;
    pointer-events: none;
    z-index: 2099;
    animation: cursor-burst-fly .5s ease-out forwards;
  }

  .cursor-burst svg { width: 100%; height: 100%; display: block; }

  @keyframes cursor-burst-fly {
    0% { transform: translate(0, 0) scale(1); opacity: 1; }
    100% { transform: translate(var(--dx), var(--dy)) scale(.25); opacity: 0; }
  }
}

/* =========================================================
   Skip link — WCAG 2.4.1 (Bypass Blocks). The nav repeats on every page
   and every page load, so without this a keyboard/screen-reader user has
   to tab through all 5 nav links before reaching real content every
   single time. Off-screen until focused, then jumps above even the fixed
   nav (z-index 1000 vs the nav's 200) so it's never hidden behind it.
   Solid fill + border, matching the site's own "cards are bordered, not
   glass" rule, so it reads clearly against whatever's behind it. */
.skip-link {
  position: absolute;
  top: -100px;
  left: 16px;
  z-index: 1000;
  background: var(--nav-bg);
  color: var(--text);
  border: 1.5px solid var(--c-blue);
  border-radius: var(--radius-sm);
  padding: 12px 20px;
  font-family: 'Baloo 2', cursive;
  font-weight: 700;
  text-decoration: none;
  transition: top .2s ease;
}

/* Text for screen readers only, e.g. "(opens in a new tab)" after a link
   whose visible label should stay short. */
.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;
}

.skip-link:focus {
  top: 16px;
}

/* =========================================================
   Nav
   ========================================================= */

.site-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 200;
  background: var(--nav-bg);
  color: var(--text);
  box-shadow: 0 8px 30px -12px rgba(0, 0, 0, .35);
}

/* A puffy, scalloped fringe hanging below the nav — same color as the
   nav itself, so the two read as one shape — instead of a hard flat
   edge, per "make it look like clouds." Sits below the rainbow
   .nav-underline strip. Built from a repeating row of circles (not an
   SVG/raster asset) so it's cheap and infinitely tileable at any width. */
.site-nav::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -13px;
  height: 26px;
  background-image: radial-gradient(circle at 13px 0, var(--nav-bg) 13px, transparent 13.5px);
  background-size: 26px 26px;
  background-repeat: repeat-x;
  background-position: 0 0;
  pointer-events: none;
}

/* One row, left to right in page order: logo, the section links, Resume,
   then Calm mode. The bar is a single grid with one column per item, and
   the link list joins it as a subgrid, so justify-content spreads the
   leftover width into identical gaps between every item: logo-to-link
   and link-to-link spacing come out the same. (Browsers without subgrid
   fall back to the list spacing its own links evenly.) The logo is
   also first in the markup, so screen readers and keyboard users meet
   the items in the same order they're drawn. */
.nav-inner {
  position: relative;
  height: var(--nav-h);
  display: grid;
  grid-template-columns: auto repeat(5, auto) auto;
  justify-content: space-between;
  align-items: center;
  /* Less padding on the left than the rest of the page so the logo sits
     close to the edge, at Jasmine's request. */
  padding: 0 6% 0 3.5%;
}

.logo {
  grid-column: 1;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
}

.nav-links {
  grid-column: 2 / 7;
  display: grid;
  grid-template-columns: repeat(5, auto);
  justify-content: space-between;
  align-items: center;
  list-style: none;
  font-family: 'Baloo 2', cursive;
  font-weight: 700;
  font-size: 1.1rem;
}
@supports (grid-template-columns: subgrid) {
  .nav-links { grid-template-columns: subgrid; }
}

/* Jasmine's own hand-drawn "JB" monogram with a star
   (images/logo-jb.png, real art; the hero still uses the separate
   "Jasmine" wordmark). Sized by height so the art keeps its proportions. */
.logo-mark {
  display: block;
  height: 46px;
  width: auto;
}

.nav-links a {
  text-decoration: none;
  padding: 4px 2px;
  border-bottom: 2px solid transparent;
  transition: border-color .25s, opacity .25s;
}

.nav-links a:hover { opacity: .7; }

.nav-links a.is-active { border-color: currentColor; }

.nav-links a[data-page="projects"] { color: var(--c-orange); }
.nav-links a[data-page="about"] { color: var(--c-red); }
.nav-links a[data-page="playground"] { color: var(--c-green); }
.nav-links a[data-page="contact"] { color: var(--c-blue); }
.nav-links a[data-page="resume"] { color: var(--c-purple); }

/* Calm mode switch (see initCalmMode() in js/main.js): the last item in
   the nav row. */
.calm-toggle {
  grid-column: 7;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px 6px 9px;
  border: 1.5px solid var(--c-purple);
  border-radius: 50px;
  background: transparent;
  color: var(--text);
  font: 600 .8rem/1 'Inter', sans-serif;
  cursor: pointer;
}
.calm-toggle svg { width: 16px; height: 16px; flex: none; }
.calm-toggle[aria-pressed="true"] { background: var(--d-purple); border-color: var(--d-purple); color: #fff; }
.calm-toggle[aria-pressed="true"] svg path { fill: currentColor; }
/* Narrower desktops don't have room for the text label: show the icon alone (the label stays in
   the accessibility tree via .sr-only, so the button keeps its name). */
@media (max-width: 1100px) {
  .calm-toggle { padding: 7px; }
  .calm-toggle .calm-label {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
}

.nav-underline {
  position: relative;
  height: 4px;
  width: 100%;
  background: var(--gradient-rainbow);
}

/* A slim gold hairline trimming the nav just above its signature rainbow
   strip — a small "royal" accent, requested but then deliberately kept to
   just this and the matching footer trim below after a broader version
   (on every card and section heading too) read as too much. */
.nav-underline::before {
  content: '';
  position: absolute;
  top: -2px;
  left: 0;
  right: 0;
  height: 1.5px;
  background: linear-gradient(90deg, transparent, var(--c-yellow) 15%, var(--d-gold) 50%, var(--c-yellow) 85%, transparent);
  opacity: .8;
  pointer-events: none;
}

.nav-toggle {
  display: none;
  background: none;
  border: none;
  font-size: 1.6rem;
  line-height: 1;
  padding: 6px 8px;
  color: var(--text);
  cursor: pointer;
}

@media (max-width: 760px) {
  .nav-inner { display: flex; padding: 0 6% 0 4%; gap: 10px; }
  .logo { margin-right: auto; }
  .logo-mark { height: 40px; }
  .nav-toggle { display: block; }
  .nav-links {
    position: fixed;
    top: calc(var(--nav-h) + 4px);
    left: 0; right: 0;
    background: var(--nav-bg);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 10px 8% 22px;
    transform: translateY(-120%);
    /* Hidden, not just moved off-screen, so a closed menu's links can't
       be reached with Tab. Visibility flips after the slide finishes. */
    visibility: hidden;
    transition: transform .3s ease, visibility 0s linear .3s;
  }
  .nav-links.is-open { transform: translateY(0); visibility: visible; transition: transform .3s ease; }
  .nav-links li { padding: 10px 0; border-top: 1px solid rgba(255, 255, 255, .1); }
  .nav-links li:first-child { border-top: none; }
}

/* =========================================================
   Comet trail (home page only) — a single straight vertical ribbon
   running down the right edge of the page in the site's full rainbow
   gradient, behind content, at one constant width for its whole length.
   No sway, no obstacle-dodging — it runs straight through/behind
   whatever's in front of it, relying on z-index (behind main content) to
   stay out of the way of readability. A star rides it at the current
   scroll position, in front of content, pulsing its own glow rather than
   growing/shrinking with the ribbon. See initCometTrail() in js/main.js.
   ========================================================= */

.comet-trail {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  z-index: 0;
  pointer-events: none;
}

.comet-trail svg { display: block; overflow: visible; }

.comet-trail .trail-seg {
  fill: url(#trailGradient);
  opacity: .92;
  /* Only the stretch of ribbon the star has already flown over is
     visible — the rest stays hidden until the star reaches it, so the
     trail reads as something the comet leaves behind rather than a
     path already laid out ahead of it. Height kept in sync with scroll
     in initCometTrail()'s update(). */
  clip-path: url(#trailReveal);
}

.comet-star {
  position: fixed;
  top: 0; left: 0;
  width: 64px; height: 64px;
  z-index: 150;
  pointer-events: auto;
  cursor: pointer;
  background: none;
  border: none;
}

.comet-star .star-glyph {
  display: block;
  width: 100%; height: 100%;
  /* Glow pulses continuously, at rest, not just while scrolling
     (star-glow-pulse below) — the star shape itself never scales, on a
     scroll burst or otherwise, only its drop-shadow glow does. */
  animation: star-glow-pulse 2.6s ease-in-out infinite alternate;
}

/* A stronger glow burst each time scrolling starts (triggered in
   main.js) — still filter-only, just brighter/wider than the resting
   pulse above while it plays. */
.comet-star.is-pulsing .star-glyph {
  animation: scroll-pulse .5s ease-out;
}

@keyframes star-glow-pulse {
  from { filter: drop-shadow(0 0 10px rgba(154, 169, 244, .85)) drop-shadow(0 0 22px rgba(220, 123, 240, .5)); }
  to   { filter: drop-shadow(0 0 18px rgba(154, 169, 244, 1)) drop-shadow(0 0 34px rgba(220, 123, 240, .7)); }
}

@keyframes scroll-pulse {
  0% { filter: drop-shadow(0 0 10px rgba(154, 169, 244, .85)) drop-shadow(0 0 22px rgba(220, 123, 240, .5)); }
  35% { filter: drop-shadow(0 0 20px rgba(154, 169, 244, 1)) drop-shadow(0 0 40px rgba(220, 123, 240, .85)); }
  100% { filter: drop-shadow(0 0 10px rgba(154, 169, 244, .85)) drop-shadow(0 0 22px rgba(220, 123, 240, .5)); }
}

.comet-star svg {
  width: 100%; height: 100%;
}

@media (max-width: 760px) {
  .comet-star { width: 42px; height: 42px; }
}

/* =========================================================
   Buttons / cards / spark mark
   ========================================================= */

.btn {
  display: inline-block;
  padding: 16px 32px;
  border: none;
  border-radius: 50px;
  background: var(--d-purple);
  color: white;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  box-shadow: 0 6px 20px rgba(183, 159, 244, .22);
  transition: transform .3s cubic-bezier(.2,.8,.3,1.3), box-shadow .3s ease;
}

.btn:hover { transform: translateY(-3px); box-shadow: 0 10px 22px rgba(183, 159, 244, .34); }
.btn:active { transform: translateY(-1px) scale(1.01); }
.btn:focus-visible, a:focus-visible, button:focus-visible { outline: 3px solid var(--c-yellow); outline-offset: 3px; }

.card {
  /* A solid panel with no outline: the darker fill alone separates it
     from the page, rather than a border or a soft glow. */
  position: relative;
  /* Solid, a step darker than the page background (starting from the
     nav's own darker indigo, --nav-bg, and deepening slightly toward the
     bottom corner) rather than a translucent white wash, so each tile
     reads as a distinct, grounded panel. Derived from existing tokens,
     no new color. */
  background: linear-gradient(160deg, var(--nav-bg), color-mix(in srgb, var(--nav-bg) 78%, black));
  border: none;
  border-radius: var(--radius-lg);
  padding: 40px;
  box-shadow: 0 14px 28px -20px rgba(7, 8, 36,.6);
  transition: transform .3s cubic-bezier(.2,.7,.3,1), box-shadow .3s ease;
  /* Magnetic tilt toward the cursor (initTiltCards() in js/main.js sets
     these two custom properties on mousemove) — 0deg at rest, so a card
     that's never hovered renders identically to before this existed. */
  --tilt-x: 0deg;
  --tilt-y: 0deg;
}

/* Hover motion only on cards that actually go somewhere or do something
   (a link or button). Static content cards (case-study Overview/Research/
   feature cards, the About fallback list) stay still, since a card that
   reacts to the mouse promises a click, and clicking it would do nothing. */
a.card:hover,
button.card:hover {
  transform: perspective(800px) rotateX(var(--tilt-y)) rotateY(var(--tilt-x)) translateY(-6px);
  box-shadow: 0 18px 32px -18px rgba(7, 8, 36,.6);
}
.card h3 { font-family: 'Baloo 2', cursive; margin-bottom: 12px; font-size: 1.4rem; }
.card p { color: var(--text-dim); }
.tbd { color: var(--text-mute); font-style: italic; }

/* Rainbow accent-dot — a small colored "planet" badge reused on About's
   doodle fallback grid, cycling through the palette so each card reads
   as its own little world instead of a plain identical tile. No hover
   glow: these cards are static, so they don't react to the mouse. */
.doodles-fallback .card {
  position: relative;
  overflow: hidden;
}

.doodles-fallback .card::before {
  content: '';
  position: absolute;
  top: 18px; right: 18px;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--dot, var(--c-blue));
  transition: transform .3s ease, box-shadow .3s ease;
}

.doodles-fallback .card:nth-child(1) { --dot: var(--c-orange); }
.doodles-fallback .card:nth-child(2) { --dot: var(--c-yellow); }
.doodles-fallback .card:nth-child(3) { --dot: var(--c-green); }

/* =========================================================
   Page section scaffolding
   ========================================================= */

main { padding-top: var(--nav-h); position: relative; z-index: 1; }

.page-hero {
  padding: 80px 8% 60px;
  min-height: 46vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: relative;
  overflow: hidden;
}

/* Section titles used to always be a page's own <h1>; now that About,
   Projects, Playground, and Contact are anchor sections on one page, only
   the hero's "Hi! I'm Jasmine" is the document's h1 and these are demoted
   to h2 — same display size either way. */
.page-hero h1, .page-hero h2 { font-size: clamp(3rem, 6.4vw, 5rem); letter-spacing: -.01em; }

section { padding: 60px 8% 100px; position: relative; }

/* Deliberately uneven rhythm from section to section — a flat repeated
   padding value on every block is exactly the "template stacked cards"
   feeling to avoid; an editorial page never gives every block the same
   air around it. */
section[aria-label="Project galaxy"] { padding: 10px 8% 170px; }
section[aria-label="Experience content"] { padding: 30px 8% 56px; }
/* Top padding bumped from an original 4px — that tiny value only worked
   because the now-removed .playground-tabs (a 46px-tall pill row with its
   own 8px/36px top/bottom margin) supplied the real gap under the
   Playground heading above (which sets its own padding-bottom:0). With
   the tabs gone, this section's own padding has to carry that gap. */
section[aria-label="Playground content"] { padding: 48px 8% 70px; }
.contact-body { padding: 34px 8% 150px; }

/* One-page navigation: clicking a nav link jumps to #about/#projects/
   #playground/#contact. Offset the landing spot by the fixed nav's height
   so the section heading doesn't end up hidden underneath it. */
#about, #experience, #projects, #playground, #contact { scroll-margin-top: var(--nav-h); }

footer {
  position: relative;
  z-index: 1;
  padding: 56px 8% 170px;
  text-align: center;
  border-top: 1px solid var(--panel-border);
  overflow: hidden;
}

/* Same slim gold trim as the nav, at the top edge of the footer — bookends
   the page with the same "royal" accent it opened with. */
footer::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1.5px;
  z-index: 2;
  background: linear-gradient(90deg, transparent, var(--c-yellow) 15%, var(--d-gold) 50%, var(--c-yellow) 85%, transparent);
  opacity: .8;
  pointer-events: none;
}

/* The journey's end — after a whole page of deep space, a warm horizon
   glow rising behind a city skyline at the very bottom of the site.
   Anchored to the bottom of the footer (which now has extra bottom
   padding to make room for it) rather than the viewport, so it only ever
   appears once, right where the page actually ends. */
.horizon-glow {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent 55%,
    rgba(238, 166, 99, .18) 70%,
    rgba(220, 123, 240, .28) 85%,
    rgba(239, 217, 110, .3) 100%
  );
}

.city-skyline {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 130px;
  z-index: 0;
  pointer-events: none;
  opacity: .92;
}

@media (max-width: 700px) {
  footer { padding-bottom: 120px; }
  .city-skyline { height: 90px; }
}

/* Minimal footer: one row of links and two small notes. The contact
   details and form live in #contact above it on the home page, so the
   footer no longer repeats a big "get in touch" block. */
footer .footer-links,
footer .footer-note { position: relative; z-index: 1; }
footer .footer-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px 22px;
  margin-bottom: 22px;
}
footer .footer-links a {
  position: relative;
  text-decoration: none;
  color: var(--text-dim);
  padding-bottom: 4px;
}
footer .footer-links a::after {
  content: '';
  position: absolute;
  left: 50%; right: 50%; bottom: 0;
  height: 2px;
  background: var(--gradient-rainbow);
  transition: left .25s ease, right .25s ease;
}
footer .footer-links a:hover,
footer .footer-links a:focus-visible { color: var(--c-pink); }
footer .footer-links a:hover::after,
footer .footer-links a:focus-visible::after { left: 0; right: 0; }
footer .footer-note { color: var(--text-dim); font-size: .85rem; line-height: 1.6; max-width: 72ch; margin: 0 auto 6px; }

@media (max-width: 760px) {
  .container, section, .nav-inner, .page-hero { padding-left: 6%; padding-right: 6%; }
}

/* =========================================================
   Home — hero ribbon + mascot
   ========================================================= */

.hero {
  min-height: calc(100vh - var(--nav-h));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 40px 8% 0;
  position: relative;
}

/* Soft blurred nebula blob sitting behind the hero text — fills the left/
   background area with color now that the comet ribbon is gone, reusing
   the rainbow palette + blur technique already used for body::before/
   ::after rather than introducing a new motif. z-index 0, so it stays
   behind .hero-text/.mascot-wrap (both z-index 1). */
.hero-glow {
  position: absolute;
  top: 12%;
  left: -8%;
  width: clamp(280px, 46vw, 560px);
  aspect-ratio: 1;
  border-radius: 50%;
  background:
    radial-gradient(circle at 35% 35%, rgba(183, 159, 244, .22), transparent 62%),
    radial-gradient(circle at 68% 55%, rgba(96, 219, 237, .14), transparent 64%),
    radial-gradient(circle at 48% 78%, rgba(220, 123, 240, .13), transparent 62%);
  filter: blur(70px);
  z-index: 0;
  pointer-events: none;
  animation: hero-glow-pulse 9s ease-in-out infinite;
}

@keyframes hero-glow-pulse {
  0%, 100% { opacity: .85; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.07); }
}

.hero-text { max-width: 620px; position: relative; z-index: 1; }
@media (min-width: 901px) {
  /* Nudges the text a touch left of its flex-default position, closer to
     the hero's left inset — a "smidge," not a layout restructure; the
     mascot lane (flex-grow) is unaffected since this is a relative-
     position offset, not a flex/margin change. Scoped above the 900px
     breakpoint where .hero-text and .mascot-wrap sit side by side; below
     it they stack, where this offset would just crowd the left edge. */
  .hero-text { left: -24px; }
}
.hero-text h1 { font-size: clamp(3.2rem, 7.5vw, 6rem); margin-bottom: 36px; letter-spacing: -.01em; }
.hero-lede { font-family: 'Baloo 2', cursive; font-weight: 600; color: var(--text); margin-bottom: 12px; font-size: clamp(1.3rem, 2.2vw, 1.6rem); line-height: 1.35; max-width: 520px; }
.hero-meta { color: var(--text-dim); margin-bottom: 34px; font-size: 1rem; line-height: 1.6; max-width: 520px; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 14px; }

/* Resume highlights in the hero, so the 10-second read already includes
   real work experience, not just a title. Quiet solid pills (not buttons:
   no hover, no border) that wrap cleanly at any width. */
.hero-proof {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: -16px 0 30px;
  padding: 0;
  font-size: .85rem;
  color: var(--text-dim);
  max-width: 560px;
}
.hero-proof li {
  padding: 5px 12px;
  border-radius: 20px;
  background: color-mix(in srgb, white 8%, var(--bg));
}
.hero-proof strong { color: var(--text); font-weight: 600; }

/* Secondary button: same shape as .btn, outlined instead of filled, so
   the primary action still reads first. */
.btn--ghost {
  background: transparent;
  color: var(--text);
  box-shadow: inset 0 0 0 1.5px var(--c-purple);
}
.btn--ghost:hover { box-shadow: inset 0 0 0 1.5px var(--c-purple), 0 10px 22px rgba(183, 159, 244, .2); }

/* Hero content is above the fold and visible on first paint, so it gets a
   one-time entrance on load rather than the IntersectionObserver-driven
   .reveal system used further down the page — a short staggered
   rise-and-fade (eyebrow, then heading, then subhead, then CTA) instead of
   everything appearing at once. */
.hero-text .eyebrow,
.hero-text h1,
.hero-lede,
.hero-meta,
.hero-proof,
.hero-actions {
  opacity: 0;
  animation: hero-enter .8s cubic-bezier(.16,.8,.3,1) forwards;
}
.hero-text .eyebrow { animation-delay: .05s; }
.hero-text h1 { animation-delay: .16s; }
.hero-lede { animation-delay: .32s; }
.hero-meta, .hero-proof { animation-delay: .4s; }
.hero-actions { animation-delay: .48s; }

@keyframes hero-enter {
  from { opacity: 0; transform: translateY(22px); }
  to { opacity: 1; transform: translateY(0); }
}

.mascot-wrap {
  position: relative;
  z-index: 1;
  flex: 1 1 240px;
  max-width: 800px;
  min-width: 180px;
  /* Insets the lane from .hero's right padding edge — without this the
     flex-grow lane always runs flush to that edge (space-between anchors
     the last item's far edge to the container's), so this is what actually
     pulls the mascot in from the edge rather than just capping its size. */
  margin-right: 8px;
  opacity: 0;
  animation: hero-enter-scale 1s .22s cubic-bezier(.16,.8,.3,1) forwards;
}

@keyframes hero-enter-scale {
  from { opacity: 0; transform: scale(.92) translateY(14px); }
  to { opacity: 1; transform: scale(1) translateY(0); }
}

/* Transform lives on the button, not .mascot itself — .mascot already
   drives its own transform via the float animation below, and a second,
   directly-set transform on the same element would just be overridden
   every frame by the running keyframe animation. */
.mascot-btn {
  display: block;
  width: 100%;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform .2s ease;
}

.mascot-btn:hover { transform: scale(1.04); }
.mascot-btn:active { transform: scale(.97); }

.mascot {
  width: 100%;
  height: auto;
  animation: float 7s ease-in-out infinite;
  /* A soft chromatic-aberration fringe — pink/teal offset glows tracing the
     mascot's silhouette — as a restrained nod to the glitch/RGB-split line
     art in Jasmine's cosmic-creature moodboard reference, without going as
     far as literally re-rendering the illustration in that style. */
  filter:
    drop-shadow(-2px 0 1px rgba(220, 123, 240, .35))
    drop-shadow(2px 0 1px rgba(144, 227, 105, .35));
}

@keyframes float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-22px); }
}

/* Same big-squiggle background treatment reused as a shared class so
   About/Playground/Contact/footer can each get their own — behind the
   section's content (which paints on top simply by coming later in the
   markup), never intercepting clicks. */
.bg-squiggle {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}

@media (max-width: 900px) {
  .hero { padding-top: 20px; }
  .mascot-wrap { flex: 0 1 auto; max-width: none; width: 78vw; margin: 0 auto; }
}

/* =========================================================
   About
   ========================================================= */

.about-grid {
  display: grid;
  grid-template-columns: 1.1fr .9fr;
  gap: 60px;
  align-items: center;
}

.about-copy p { color: var(--text-dim); font-size: 1.05rem; line-height: 1.8; margin-bottom: 18px; max-width: 46ch; }

/* Small info block under the bio: a label/value list with a hairline
   rule on the left, the About-sized cousin of .case-quickfacts. */
.about-facts {
  margin: 8px 0 30px;
  padding-left: 18px;
  border-left: 2px solid var(--c-purple);
  display: grid;
  gap: 12px;
  max-width: 46ch;
}
.about-facts dt {
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-mute);
  margin-bottom: 2px;
}
.about-facts dd { margin: 0; color: var(--text); line-height: 1.5; }

.about-copy .about-lead {
  font-family: 'Baloo 2', cursive;
  font-weight: 600;
  font-size: clamp(1.35rem, 2.2vw, 1.7rem);
  line-height: 1.35;
  color: var(--text);
  margin: 6px 0 16px;
}

/* Work history from the resume, as a plain row of entries (dates, place,
   role, one or two lines of what she did), so the portfolio carries the
   experience before anyone opens the PDF. A thin rainbow-token rule on top
   of each entry ties it to the palette without boxing it into a card. */
.experience { margin-top: 72px; position: relative; z-index: 1; }
.experience-title { font-size: clamp(1.6rem, 3vw, 2.2rem); margin-bottom: 26px; }
.experience-list {
  list-style: none;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 32px 28px;
}
.experience-list li { border-top: 2px solid var(--exp-accent, var(--c-purple)); padding-top: 16px; }
.experience-list li:nth-child(1) { --exp-accent: var(--c-red); }
.experience-list li:nth-child(2) { --exp-accent: var(--c-yellow); }
.experience-list li:nth-child(3) { --exp-accent: var(--c-green); }
.experience-list li:nth-child(4) { --exp-accent: var(--c-blue); }
.experience-when {
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-mute);
  margin-bottom: 6px;
}
.experience-list h4 { font-family: 'Baloo 2', cursive; font-weight: 600; font-size: 1.3rem; line-height: 1.2; }
.experience-role { color: var(--text); font-size: .92rem; margin: 2px 0 10px; }
.experience-list p:not(.experience-when):not(.experience-role) { color: var(--text-dim); font-size: .92rem; line-height: 1.6; }

.avatar-frame {
  position: relative;
  width: min(78vw, 400px);
  aspect-ratio: 1 / 1;
  /* Off-center in its column — a perfectly centered circle is exactly the
     "generated template" look this page is trying not to have. Static on
     purpose (no tilt, no hover motion): this version of the art is a
     transparent PNG (photo + hand-illustrated ring, no background square
     to frame), so it's displayed at natural size with no crop/rotation/
     hover effect, the same treatment as the hero mascot art. */
  margin: 0 auto 0 8%;
}

.avatar-photo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

@media (max-width: 900px) {
  .about-grid { grid-template-columns: 1fr; text-align: left; }
}

/* Floats the doodles around the About section's own content instead of
   giving them a separate empty block below it — an absolute layer spanning
   #about (a .page-hero, already position:relative), sitting above the
   bio/avatar so the doodles stay reachable, but pointer-events:none at the
   container level so the empty space between them doesn't block clicks on
   the real content (e.g. the "Say hello" button) underneath. */
.about-doodles {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

.doodle {
  position: absolute;
  pointer-events: auto;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  animation: float 7.5s ease-in-out infinite;
  filter: drop-shadow(0 6px 16px rgba(7, 8, 36, .4));
  transition: transform .3s;
}

.doodle img { display: block; width: 100%; height: auto; }

.doodle:hover, .doodle:focus-visible {
  transform: scale(1.14) rotate(-4deg);
  animation-play-state: paused;
}

.doodle-1 { width: 62px; top: 4%;  left: 6%;  animation-duration: 6.4s; }
.doodle-2 { width: 50px; top: 66%; left: 1%; animation-duration: 5.4s; animation-delay: -1.6s; }
.doodle-3 { width: 68px; top: 2%;  left: 82%; animation-duration: 6.8s; animation-delay: -3.2s; }

.doodles-fallback {
  display: none;
  margin-top: 40px;
  /* One column: three facts in a 2-column grid would leave an orphan. */
  grid-template-columns: 1fr;
  max-width: 520px;
  gap: 20px;
}

.doodles-fallback .card { padding: 24px; display: flex; align-items: center; gap: 16px; }
.doodles-fallback .card img { width: 44px; height: 44px; flex: 0 0 auto; }
.doodles-fallback .card h3 { font-size: 1.1rem; margin-bottom: 4px; }
.doodles-fallback .card p { font-size: .92rem; }

@media (max-width: 700px) {
  .about-doodles { display: none; }
  .doodles-fallback { display: grid; }
}

/* =========================================================
   About's sketchbook tooltips (.planet-card) — the name is a holdover
   from when Projects also used this component for its planet tooltips;
   Projects moved to the always-visible constellation cards below, but
   .doodle's hover/focus tooltips in the About section still use this.
   ========================================================= */

.planet-card {
  position: absolute;
  width: 240px;
  background: var(--paper);
  color: var(--ink);
  border-radius: var(--radius-sm);
  padding: 20px 22px;
  box-shadow: 0 30px 70px rgba(7, 8, 36, .5), 0 6px 18px rgba(7, 8, 36, .3);
  opacity: 0;
  transform: translateY(8px) rotate(-2deg) scale(.96);
  pointer-events: none;
  transition: opacity .25s ease, transform .3s cubic-bezier(.2,.7,.3,1.1);
  z-index: 5;
}

/* h3, not h4 — "About" (h2) has no h3 between it and these tooltips in
   the DOM, so h4 here was a skipped heading level (WCAG 1.3.1). Matches
   the level already used by .doodles-fallback's equivalent content. */
.planet-card h3 { font-family: 'Baloo 2', cursive; margin-bottom: 6px; }
.planet-card p { font-size: .92rem; color: #2a3085; }
/* .tbd's sitewide color (--text-mute) is tuned for the dark page
   background (6.5:1 there) — on .planet-card's light --paper surface it
   drops to ~2.5:1 and fails AA. This override keeps the same "muted
   placeholder" feel at an equivalent 6.5:1 against --paper instead. */
.planet-card .tbd { color: #535997; }
.planet-card code, .doodles-fallback code {
  font-family: monospace;
  font-size: .85em;
  background: rgba(10, 11, 45, .08);
  padding: 1px 5px;
  border-radius: 4px;
}

/* "View case study" link reused inside planet tooltips and project cards
   site-wide — a small animated arrow nudge on hover instead of a full
   button, so it reads as a natural extension of the card rather than a
   competing CTA. */
.view-case {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 10px;
  font-family: 'Baloo 2', cursive;
  font-weight: 400;
  font-size: .92rem;
  text-decoration: none;
  color: var(--d-blue);
}

.planet-card .view-case { color: var(--d-blue); }

.view-case .arrow { transition: transform .2s ease; }
.view-case:hover .arrow, .view-case:focus-visible .arrow { transform: translateX(4px); }

.doodle:hover + .planet-card,
.doodle:focus-visible + .planet-card,
.planet-card:hover,
.planet-card:focus-within {
  opacity: 1;
  transform: translateY(0) rotate(-1deg) scale(1);
  pointer-events: auto;
}

/* =========================================================
   Playground
   ========================================================= */

/* A simple responsive grid: every tile visible at once, no sideways
   scrolling to discover what's there (it used to be a horizontal
   scroll-snap deck with a "scroll for more" hint, replaced so nothing
   is hidden off-screen). */
.playground-deck {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 28px;
}

.sticker-card {
  position: relative;
  background: color-mix(in srgb, white 7%, var(--bg)); /* solid, see .project-spread */
  border: 1.5px solid rgba(255, 255, 255, .18);
  border-radius: var(--radius-md);
  padding: 30px 24px;
  text-align: center;
  box-shadow: 0 14px 26px -18px rgba(7, 8, 36, .6);
  transition: transform .3s cubic-bezier(.2,.7,.3,1.1), box-shadow .3s ease, border-color .3s ease;
  /* Clips .sticker-thumb's bled top corners to the card's own radius. */
  overflow: hidden;
  /* Each tile is a plain <a> to its own playground/*.html placeholder
     page (same "plain link, sitewide view-transition just works" pattern
     as .project-visual) — reset the anchor defaults so it still reads as
     a card, not a link. */
  display: block;
  text-decoration: none;
  color: inherit;
}

/* Full-bleed thumbnail at the top of each tile — pulled out to the card's
   own edges via negative margin (matching .sticker-card's own padding, so
   it bleeds flush) rather than sitting inset like a regular image. Each
   src is a small placeholder line-art SVG in images/ (same "real file,
   swap the file" pattern as the Projects orbit thumbnails), one per tile. */
.sticker-thumb {
  display: block;
  width: calc(100% + 48px);
  aspect-ratio: 16 / 9;
  object-fit: cover;
  margin: -30px -24px 18px;
}

.sticker-view {
  display: block;
  margin-top: 14px;
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: .5px;
  text-transform: uppercase;
  color: var(--c-pink);
}

/* Simplified to a plain lift — no magnetic cursor-tilt/scale — at
   Jasmine's request, further paring back the tile hover after the static
   alternating rotation was already removed. .card elsewhere keeps its
   own magnetic tilt; this is scoped to .sticker-card only. */
.sticker-card:hover {
  transform: translateY(-7px);
  border-color: var(--c-pink);
  box-shadow: 0 18px 30px -16px rgba(7, 8, 36, .55);
}

/* playground/animations.html's real gallery of animation clips — a grid
   of .card tiles, each a full-bleed video/GIF. Reuses .card for the
   bordered/translucent panel look and hover lift rather than
   introducing a new card treatment. Each tile is a <button> (not a
   <figure> — there's no caption to anchor a figure to any more, and the
   whole tile is a click target that opens theater mode, see below), so
   this also resets the native button chrome .card's own rules don't
   already cover, the same "reset the interactive element's own
   defaults so it still reads as a card" pattern .sticker-card uses for
   its anchor. */
.animation-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 32px;
}

.animation-tile {
  display: block;
  width: 100%;
  padding: 0;
  overflow: hidden;
  font: inherit;
  cursor: pointer;
}

.animation-tile video,
.animation-tile img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: #000;
}

/* Illustrations reuse the animation gallery and viewer. The pieces are
   finished compositions in mixed shapes (portrait paintings, wide
   panels, a poster, a logo), so they aren't cropped into matching tiles.
   Instead they sit in justified rows: each tile's --ar (width / height,
   set inline) is its flex-grow and base width, so every piece in a row
   ends up the same height and reading order stays left to right. The
   max-width caps any tile at about 440px tall, so a row that ends up
   holding a single piece doesn't blow it up to full width. */
.illustration-gallery { display: flex; flex-wrap: wrap; gap: 24px; }
.illustration-tile { flex: var(--ar) 1 calc(var(--ar) * 200px); max-width: calc(var(--ar) * 440px); }
.illustration-tile img { aspect-ratio: auto; height: auto; }
.illustration-page .theater-stage img { width: auto; max-width: 100%; }

/* Theater mode — clicking any .animation-tile opens its clip full-size
   in this one shared overlay (populated by initAnimationTheater() in
   js/main.js) instead of playing inline in the small tile. The site's
   first true modal; kept on-system (plum-tinted shadow, --radius-md,
   dark/pastel palette) rather than a new visual language for it. */
.theater-modal {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 60px 24px;
  background: rgba(7, 9, 42, .88);
}

.theater-modal[hidden] { display: none; }

.theater-stage { width: 100%; max-width: 1100px; }

.theater-stage video,
.theater-stage img {
  display: block;
  width: 100%;
  max-height: 80vh;
  margin: 0 auto;
  border-radius: var(--radius-md);
  box-shadow: 0 30px 80px -20px rgba(0, 0, 0, .6);
}

.theater-close {
  position: fixed;
  top: 24px;
  right: 24px;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, .3);
  background: rgba(255, 255, 255, .08);
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background .2s ease, border-color .2s ease, transform .2s ease;
}

.theater-close svg { width: 18px; height: 18px; }

.theater-close:hover {
  background: rgba(255, 255, 255, .18);
  border-color: var(--c-pink);
  transform: scale(1.06);
}

@media (max-width: 560px) {
  .theater-modal { padding: 80px 16px 24px; }
  .theater-close { top: 16px; right: 16px; }
}

/* A hand-drawn squiggle under the Playground heading, in the site's
   rainbow gradient, that draws itself in on load — this page is the one
   explicitly licensed to feel rougher and more sketchbook (CLAUDE.md),
   so its hero gets a flourish the other page-heroes don't. */
.squiggle-underline {
  display: block;
  width: min(260px, 60vw);
  height: 20px;
  margin-top: 6px;
}

.squiggle-underline path {
  stroke-dasharray: 320;
  stroke-dashoffset: 320;
  animation: draw-squiggle 1.1s .3s ease forwards;
}

@keyframes draw-squiggle {
  to { stroke-dashoffset: 0; }
}

/* The hidden "type rabbit" easter egg — see initRabbitEgg()/hopRabbit()
   in js/main.js. A silhouette bounds across the screen left to right in a
   few arcs, like a real hop, then fades out and removes itself. */
.rabbit-hop {
  position: fixed;
  top: 50%; left: -10%;
  width: 70px; height: 70px;
  z-index: 400;
  pointer-events: none;
  animation: rabbit-hop-across 1.8s cubic-bezier(.3,.7,.4,1) forwards;
}
.rabbit-hop-glyph {
  display: block;
  width: 100%; height: 100%;
  filter: drop-shadow(0 0 10px rgba(183, 159, 244, .6));
}

@keyframes rabbit-hop-across {
  0%   { translate: 0vw 0; scale: .7; opacity: 0; }
  8%   { opacity: 1; }
  25%  { translate: 27vw -60px; scale: 1; }
  50%  { translate: 54vw 0; scale: .95; }
  75%  { translate: 81vw -60px; scale: 1; }
  92%  { opacity: 1; }
  100% { translate: 110vw 0; scale: .7; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .rabbit-hop { display: none; }
}

/* =========================================================
   Projects — a stacked list of "project spreads" (a bordered info card
   paired with a photo), replacing Project Orbit's carousel at Jasmine's
   explicit request, styled after a reference she shared
   (wallofportfolios.in's "selected works" list: a page-like card next to
   a photo, repeated down the page). Cards/photos originally alternated a
   slight scattered-page tilt per spread; Jasmine asked for that removed
   in favor of a level, straight layout, and for the vertical gap between
   spreads tightened so more of the list is visible at once without
   scrolling through one project at a time — don't reintroduce the tilt
   or widen the gap back toward the original 80px without checking with
   her first. Adapted to this site's own dark/pastel system rather than
   copied literally (rule 4) — the
   reference's cream paper + hot-pink pills became this site's existing
   translucent `.card`-style panel + the same per-project accent/
   accent-deep color pair the old orbit rings and CTAs already used
   (`--accent`/`--accent-deep` custom properties set inline per
   `.project-spread`), not a new off-system palette. Plain `<a>` links
   throughout, so the sitewide cross-document View Transition (top of
   this file) still applies with no extra JS.
   ========================================================= */

/* 2x2 grid, not a stacked single-column list — Jasmine asked for the
   cards sized down after the unified split-card redesign read as too
   big at full row width; a 2-column grid halves each card's width
   (height follows from that, since .project-spread has no fixed
   height) without shrinking any single value by feel. */
.projects-list {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 28px;
  padding: 36px 8% 24px;
}

/* One unified bordered/rounded card per project (the "shape" from
   Jasmine's mkilgoredesign.com reference — a split color text-panel +
   image sharing one card outline) rather than the text card and photo
   as two separate floating siblings. The border/background/radius/
   shadow that used to live on .project-card alone now live here, on the
   shared container, and .project-card/.project-visual are just its two
   flex halves — .project-visual's own border-radius/shadow/max-width are
   gone since the parent's overflow:hidden clips both halves to one
   outline. Still the stacked single-column list, not the reference's
   2-column grid (Jasmine asked to borrow just the card shape). */
.project-spread {
  display: flex;
  align-items: stretch;
  /* Solid, not translucent: a see-through panel let the aurora glow
     behind it wash the text out below AA as the page scrolled. Mixed
     from existing tokens so it looks the same as the old 7% white wash
     over plain --bg. */
  background: color-mix(in srgb, white 7%, var(--bg));
  border: 1.5px solid var(--panel-border);
  border-radius: 26px;
  overflow: hidden;
  box-shadow: 0 14px 26px -18px rgba(7, 8, 36, .6);
}

.project-card {
  flex: 1.15 1 320px;
  padding: 32px;
  font-size: .92rem;
  /* A soft accent tint over the shared dark card bg, not the reference's
     solid saturated panel fill — the soft/pastel tier is too light for
     white text to sit on directly (see the color table above), so this
     stays a low-opacity wash over the existing dark card background
     instead of an opaque fill. color-mix() reads the same --accent
     already set inline per spread, no new color token. */
  background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 15%, transparent), transparent 75%);
}

.project-card h3 { font-size: 1.5rem; color: var(--accent); margin-bottom: 10px; }
.project-card > p { color: var(--text-dim); line-height: 1.6; margin-bottom: 16px; }

.project-visual {
  flex: 1 1 260px;
  display: block;
  min-height: 220px;
  position: relative;
}

/* Absolutely positioned so the photo fills whatever height the text sets,
   instead of a tall screenshot stretching the whole card. */
.project-visual img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform .4s ease;
}

/* The card itself doesn't react to hover, since clicking its text does
   nothing. Only the parts that are links do: the photo zooms when you
   hover the photo, and the button has its own hover. */
.project-visual:hover img,
.project-visual:focus-visible img { transform: scale(1.05); }
/* The spread clips to its rounded outline (overflow: hidden), which would
   cut off a normal outside focus ring, so draw this one inset. */
.project-visual:focus-visible { outline-offset: -5px; }

/* Flagship projects (DreamScape, EcoLink) span both columns with a bigger
   image and title, so the strongest work is what a recruiter sees first
   and largest. The two team projects stay in the 2-up row below. */
.project-spread--featured { grid-column: 1 / -1; min-height: 400px; }
.project-spread--featured .project-card { flex: 1 1 360px; padding: 44px; font-size: 1rem; }
.project-spread--featured .project-visual { flex: 1.35 1 420px; }
.project-spread--featured h3 { font-size: clamp(1.9rem, 3vw, 2.5rem); }
.project-spread--featured .project-card > p { max-width: 52ch; }

.project-card .project-kicker {
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-mute);
  margin-bottom: 8px;
}

/* The same four labeled fields on every card (role, team, timeline,
   research), so projects compare at a glance. A hairline-topped 2x2
   grid; the wider featured cards run it as one row of four. */
.project-meta {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px 20px;
  margin: 4px 0 22px;
  padding-top: 16px;
  border-top: 1px solid var(--panel-border);
}
.project-spread--featured .project-meta { grid-template-columns: repeat(4, minmax(0, 1fr)); max-width: 640px; }
.project-meta dt {
  font-family: var(--font-mono);
  font-size: .66rem;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-mute);
  margin-bottom: 3px;
}
.project-meta dd { margin: 0; color: var(--text); font-size: .9rem; line-height: 1.35; }

@media (max-width: 760px) {
  .projects-list { grid-template-columns: 1fr; gap: 32px; }
  .project-spread--featured .project-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .project-spread--featured { min-height: 0; }
  .project-spread--featured .project-card { padding: 32px; }
  .project-spread { flex-direction: column; }
  /* No row to stretch against once stacked — .project-visual needs its
     own sizing instead of matching the text panel's height. */
  .project-visual { aspect-ratio: 16 / 10; min-height: 180px; }
}

/* =========================================================
   Contact
   ========================================================= */

/* The site's closing line: a positioning statement instead of a generic
   "Let's talk", sized down from the section-title scale so a full
   sentence fits in two or three lines. */
.page-hero .contact-statement {
  font-size: clamp(2.2rem, 4.6vw, 3.8rem);
  max-width: 17ch;
  line-height: 1.08;
}
.contact-pitch {
  color: var(--text-dim);
  font-size: 1.1rem;
  line-height: 1.7;
  max-width: 56ch;
  margin-top: 22px;
  position: relative;
}

.contact-grid {
  display: grid;
  /* Asymmetric, not an even 1fr/1fr split — the form has more content and
     earns the wider column. */
  grid-template-columns: .82fr 1.18fr;
  gap: 60px;
  align-items: start;
}

.contact-direct a {
  position: relative;
  display: block;
  font-family: 'Baloo 2', cursive;
  font-size: 1.4rem;
  color: var(--text);
  text-decoration: none;
  margin-bottom: 14px;
  padding-left: 20px;
  border-bottom: 1px dashed transparent;
}

.contact-direct a::before {
  content: '';
  position: absolute;
  left: 0; top: 50%;
  width: 8px; height: 8px;
  border-radius: 50%;
  transform: translateY(-50%) scale(.6);
  opacity: .5;
  transition: transform .25s ease, opacity .25s ease;
}

.contact-direct a:nth-child(1)::before { background: var(--c-pink); }
.contact-direct a:nth-child(2)::before { background: var(--c-blue); }
.contact-direct a:nth-child(3)::before { background: var(--c-purple); }

.contact-direct a:hover,
.contact-direct a:focus-visible { border-color: var(--c-pink); color: var(--c-pink); }
.contact-direct a:hover::before,
.contact-direct a:focus-visible::before { transform: translateY(-50%) scale(1); opacity: 1; }

form.contact-form { display: flex; flex-direction: column; gap: 18px; }

.contact-form label { display: block; font-size: .85rem; letter-spacing: 1px; color: var(--text-mute); margin-bottom: 6px; text-transform: uppercase; }

.contact-form input,
.contact-form textarea {
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--text-mute); /* 3:1+ so the field outline is visible (WCAG 1.4.11) */
  border-radius: var(--radius-sm);
  padding: 14px 16px;
  color: var(--text);
  font-family: 'Inter', sans-serif;
  font-size: 1rem;
  transition: outline-color .2s ease, transform .2s ease, background .2s ease;
}

.contact-form input:focus,
.contact-form textarea:focus {
  outline: 2px solid var(--c-blue);
  outline-offset: 2px;
  background: rgba(255, 255, 255, .08);
  transform: translateY(-1px);
}

/* Each field gets its own accent color on focus — small, but it turns a
   generic form into one more on-brand rainbow touch instead of a flat
   default input everywhere. */
.contact-form div:nth-of-type(1) input:focus { outline-color: var(--c-orange); }
.contact-form div:nth-of-type(2) input:focus { outline-color: var(--c-green); }
.contact-form div:nth-of-type(3) textarea:focus { outline-color: var(--c-purple); }

.contact-form-status { min-height: 1.4em; font-size: .92rem; color: var(--text-dim); }
.contact-form-status.is-success { color: var(--c-green); }
.contact-form-status.is-error { color: var(--c-red); }

/* A few quiet pulsing sparks near the Contact hero heading — the page
   otherwise has no visual motif of its own; this borrows the site's
   spark mark rather than inventing a new one. */
.mini-spark {
  position: absolute;
  width: 20px; height: 20px;
  opacity: .85;
  animation: mini-pulse 3.4s ease-in-out infinite;
  z-index: 0;
  pointer-events: none;
}

.spark-a { top: 6%; left: 1%; }
.spark-b { top: 62%; left: 7%; width: 14px; height: 14px; animation-delay: .6s; }
.spark-c { top: 22%; left: 15%; width: 11px; height: 11px; animation-delay: 1.2s; }

@keyframes mini-pulse {
  0%, 100% { transform: scale(.85); opacity: .45; }
  50% { transform: scale(1.15); opacity: 1; }
}

@media (max-width: 700px) {
  .mini-spark { display: none; }
}

/* A couple of extra twinkle-stars scattered around the mascot — more of
   the loose star scatter from Jasmine's cosmic-creature moodboard
   reference, with the same soft chromatic-fringe glow as the mascot. */
.hero-spark {
  position: absolute;
  opacity: .8;
  animation: mini-pulse 2.6s ease-in-out infinite;
  pointer-events: none;
  filter:
    drop-shadow(-1.5px 0 1px rgba(220, 123, 240, .4))
    drop-shadow(1.5px 0 1px rgba(144, 227, 105, .4));
}

.hero-spark-a { top: -6%; right: 4%; width: 22px; height: 22px; }
.hero-spark-b { bottom: 10%; left: -8%; width: 14px; height: 14px; animation-delay: .9s; }

@media (max-width: 900px) {
  .hero-spark { display: none; }
}

@media (max-width: 800px) {
  .contact-grid { grid-template-columns: 1fr; }
}

/* =========================================================
   Case studies — a wide "presentation deck" layout (redesigned per a
   reference Jasmine shared, laurenlangdesign.com/supplierone) rather
   than a narrow centered blog column: a 1320px-wide stage, large
   numbered chapter headers, alternating full-width/split/text sections,
   and generous spacing so the page reads like a UX design review instead
   of an article. Deliberately KEPT on the site's own dark cosmic
   background + pastel accent palette rather than that reference's white
   one — a full light-mode version of the whole site was tried earlier
   and explicitly rejected ("I don't like it"), so this widens/re-paces
   the layout without touching that decision. Each page still sets
   --case-accent (one of the rainbow tokens) on <body> to theme its
   back-link, chapter numerals, quote marks, and list bullets.

   None of the 3 case studies have real screenshots, wireframes, journey
   maps, device mockups, user quotes, or metrics yet — only placeholder
   art — so every visual slot here (.case-mockup, .case-visual-box) is an
   honestly-labeled placeholder image (swap-the-file pattern, same as
   elsewhere) and every quote/metric slot is marked [TBD] rather than
   invented (rule 6). */

.case-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  color: var(--case-accent, var(--c-blue));
  font-size: .85rem;
  letter-spacing: 2px;
  text-transform: uppercase;
  margin-bottom: 22px;
}

.case-back .arrow { transition: transform .2s ease; }
.case-back:hover .arrow, .case-back:focus-visible .arrow { transform: translateX(-4px); }

.case-hero h1 { font-size: clamp(2.8rem, 6vw, 4.6rem); margin-bottom: 10px; }
.case-hook { color: var(--text-dim); font-size: 1.2rem; max-width: 62ch; margin-top: 14px; }

/* Large featured image right in the hero, and the large edge-to-edge
   mockup in Final Solution — the two "immersive" full-bleed moments the
   brief calls for. Same box reused both places. */
.case-mockup {
  max-width: 1320px;
  margin: 40px auto 0;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1.5px solid var(--panel-border);
  background: var(--panel);
}

.case-mockup img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Quick facts row under the hero — Role/Timeline/Team/Tools as a wide,
   minimal strip of columns rather than a boxed card, so it reads as part
   of the hero rather than a separate panel. */
.case-quickfacts {
  max-width: 1320px;
  margin: 36px auto 0;
  padding: 0 8%;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  border-top: 1px solid var(--panel-border);
  border-bottom: 1px solid var(--panel-border);
}

.case-quickfacts div {
  padding: 18px 24px 18px 0;
  border-left: 1px solid var(--panel-border);
  padding-left: 24px;
}

.case-quickfacts div:first-child { border-left: none; padding-left: 0; }

.case-quickfacts dt {
  font-size: .7rem;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-mute);
  margin-bottom: 6px;
}

.case-quickfacts dd { margin: 0; color: var(--text); font-size: .98rem; line-height: 1.4; }

/* On wide screens: four short facts across the top, then the longer
   "My contributions" / "Feedback" entries on a second row, wider so their
   sentences don't wrap into a narrow column. */
/* Below that, the long sentence-length entries take a full row instead
   of squeezing into a narrow half-width column. */
@media (max-width: 899px) {
  .case-quickfacts .qf-wide,
  .case-quickfacts .qf-full { grid-column: 1 / -1; border-left: none; padding-left: 0; border-top: 1px solid var(--panel-border); }
}
@media (min-width: 900px) {
  .case-quickfacts { grid-template-columns: repeat(4, 1fr); }
  .case-quickfacts .qf-wide { grid-column: span 2; }
  .case-quickfacts .qf-full { grid-column: 1 / -1; }
  .case-quickfacts .qf-wide,
  .case-quickfacts .qf-full { border-top: 1px solid var(--panel-border); }
  .case-quickfacts .qf-row-start { border-left: none; padding-left: 0; }
}

/* One "chapter" per major beat (Overview, Research, Process, Design
   Iterations, Final Solution, Results, Reflection) — a wide 1320px stage
   with generous vertical padding, alternating a faint tint band on every
   other chapter as the "background changes between sections" cue from
   the brief, done as a soft wash rather than a hard block so the page
   still reads as one continuous surface. */
.case-chapter {
  max-width: 1320px;
  margin: 0 auto;
  padding: 90px 8%;
  position: relative;
}

.case-chapter--tint {
  background: linear-gradient(180deg, transparent, rgba(255, 255, 255, .035) 12%, rgba(255, 255, 255, .035) 88%, transparent);
}

/* A large numbered heading that acts as a chapter break, with a thin
   rule trailing off after it — not a small in-flow h2 like the rest of
   the site's prose headings. */
.case-chapter-header {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-bottom: 48px;
}

.case-chapter-header .num {
  font-family: var(--font-mono);
  font-size: 1rem;
  letter-spacing: 2px;
  color: var(--case-accent, var(--c-blue));
  opacity: .85;
  flex-shrink: 0;
}

.case-chapter-header h2 {
  font-size: clamp(2rem, 4vw, 3.2rem);
}

.case-chapter-header::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--panel-border);
  min-width: 40px;
}

.case-chapter p {
  color: var(--text-dim);
  font-size: 1.05rem;
  line-height: 1.8;
  max-width: 72ch;
  margin-bottom: 16px;
}

/* Problem / Goal / Impact (Overview) and any other 3-up card row — reuses
   the site's existing solid-bordered .card rather than a new panel style. */
.case-overview-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
  margin-top: 8px;
}

.case-overview-grid .card { height: 100%; }

/* Research findings as callout cards instead of a wall of paragraphs. */
.case-callout-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 28px;
  margin: 8px 0 50px;
}

/* A large standalone statement — reused for both a mid-chapter pull-out
   line (an existing sentence pulled out of the surrounding prose, never
   new copy) and for [TBD] user-quote slots, since no real quotes exist
   yet for any of these 3 projects. */
.case-quote {
  max-width: 880px;
  margin: 50px auto;
  padding-left: 44px;
  position: relative;
  font-family: 'Baloo 2', cursive;
  font-weight: 500;
  font-size: clamp(1.35rem, 2.4vw, 1.9rem);
  line-height: 1.5;
  color: var(--text);
}

.case-quote::before {
  content: '\201C';
  position: absolute;
  left: -4px;
  top: -26px;
  font-size: 4.2rem;
  line-height: 1;
  color: var(--case-accent, var(--c-blue));
  opacity: .4;
}

.case-quote cite {
  display: block;
  margin-top: 14px;
  font-style: normal;
  font-family: var(--font-mono);
  font-size: .78rem;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-mute);
}

/* Image-left/text-right, alternating to text-left/image-right via
   --reverse — the Process chapter's "journey maps, flows, decision
   frameworks" alongside their explanation, instead of stacking image
   above text every time. */
.case-split-visual {
  display: grid;
  grid-template-columns: 1fr;
  gap: 36px;
  align-items: center;
  margin-bottom: 64px;
}

@media (min-width: 900px) {
  .case-split-visual { grid-template-columns: 1fr 1fr; gap: 64px; }
  .case-split-visual--reverse { direction: rtl; }
  .case-split-visual--reverse > * { direction: ltr; }
}

.case-split-visual:last-child { margin-bottom: 0; }

/* The general-purpose placeholder box — 4:3, used for process diagrams,
   design-iteration gallery tiles, and research artifacts. Each <img>
   points at a small placeholder SVG in images/ (a dashed frame + the
   site's spark glyph, in that case study's accent color) that Jasmine
   can overwrite directly — no HTML/CSS edits needed to drop in a real
   screenshot/diagram, the same swap-the-file pattern as the About page's
   doodle slots. object-fit: cover crops any real image neatly regardless
   of its actual aspect ratio. */
.case-visual-box {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1.5px solid var(--panel-border);
  background: var(--panel);
}

.case-visual-box img { display: block; width: 100%; height: 100%; object-fit: cover; }

.case-chapter figcaption {
  margin-top: 10px;
  font-size: .85rem;
  font-style: italic;
  color: var(--text-dim);
}

/* Design Iterations — a gallery of sketch/wireframe placeholder tiles
   with captions beneath, rather than one single image. */
/* A full sketch sheet shown whole at its own aspect ratio, rather than
   cropped into a 4:3 .case-visual-box (a center crop would cut screens off). */
.case-sketch { max-width: 760px; margin: 0 auto 48px; }
.case-sketch img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  border: 1.5px solid var(--panel-border);
}

.case-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 32px;
}

/* Final Solution's feature highlights, in a split card row under the big
   edge-to-edge mockup. */
.case-feature-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 28px;
  margin-top: 50px;
}

.case-list { list-style: none; margin: 0 0 16px; }
.case-list li {
  position: relative;
  padding-left: 24px;
  color: var(--text-dim);
  line-height: 1.7;
  margin-bottom: 10px;
}
.case-list li::before {
  content: '';
  position: absolute;
  left: 4px; top: 9px;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--case-accent, var(--c-blue));
}

/* Numbered feature/step list — same rhythm as .case-list but with a filled
   accent numeral instead of a dot, for "core features" style rundowns. */
.case-list--numbered { counter-reset: case-step; }
.case-list--numbered li { padding-left: 36px; }
.case-list--numbered li::before { content: none; }
.case-list--numbered li::after {
  counter-increment: case-step;
  content: counter(case-step);
  position: absolute;
  left: 0; top: 0;
  width: 24px; height: 24px;
  border-radius: 50%;
  border: 2px solid var(--case-accent, var(--c-blue));
  background: transparent;
  color: var(--text);
  font-family: 'Baloo 2', cursive;
  font-size: .78rem;
  line-height: 1;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
}

.case-links { display: flex; flex-wrap: wrap; gap: 14px 26px; margin-top: 10px; align-items: center; }
.case-links a:not(.btn) {
  font-family: 'Baloo 2', cursive;
  font-size: 1rem;
  text-decoration: none;
  color: var(--case-accent, var(--c-blue));
  border-bottom: 1px dashed transparent;
}
.case-links a:not(.btn):hover,
.case-links a:not(.btn):focus-visible { border-color: currentColor; }

.case-reflection {
  font-family: 'Baloo 2', cursive;
  font-weight: 400;
  font-size: 1.4rem;
  line-height: 1.7;
  color: var(--text);
  max-width: 68ch;
}

/* Insights: what we learned (the card) and what it changed (the "So"
   line), so research visibly turns into decisions. Static cards, no
   hover (they aren't links). */
.case-insight-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 28px;
  margin: 24px 0 28px;
}
.case-insight { display: flex; flex-direction: column; }
.case-insight h3 { font-size: 1.35rem; margin-bottom: 12px; }
.case-chapter .case-insight-num {
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--case-accent, var(--c-blue));
  margin-bottom: 10px;
}
.case-chapter .case-insight p { font-size: 1rem; line-height: 1.65; }
.case-chapter .case-insight-so {
  margin: auto 0 0;
  padding: 14px 0 0 14px;
  border-left: 2px solid var(--case-accent, var(--c-blue));
  color: var(--text);
}
.case-insight-so span {
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-mute);
  margin-right: 4px;
}

/* Outcome sub-section ("What I'd test next"). */
.case-subhead { font-size: clamp(1.4rem, 2.4vw, 1.8rem); margin: 56px 0 18px; }
/* Prev/next case-study nav, echoing the reference format's footer links. */
.case-prevnext {
  max-width: 1320px;
  margin: 6px auto 0;
  padding: 34px 8% 55px;
  border-top: 1px solid var(--panel-border);
  display: flex;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
}

.case-prevnext a {
  display: flex;
  flex-direction: column;
  max-width: 260px;
  text-decoration: none;
  color: var(--text);
}

.case-prevnext a:last-child { text-align: right; align-items: flex-end; }

.case-prevnext .dir-label {
  font-size: .72rem;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-mute);
  margin-bottom: 6px;
}

.case-prevnext .dir-title {
  font-family: 'Baloo 2', cursive;
  font-size: 1.05rem;
}

.case-prevnext a:hover .dir-title,
.case-prevnext a:focus-visible .dir-title { text-decoration: underline; }

@media (max-width: 760px) {
  .case-chapter { padding: 60px 6% 60px; }
  .case-quickfacts { padding-left: 6%; padding-right: 6%; }
  .case-prevnext { padding-left: 6%; padding-right: 6%; }
  .case-overview-grid { grid-template-columns: 1fr; }
}

@media (max-width: 500px) {
  .case-prevnext a:last-child { text-align: left; align-items: flex-start; }
}
