    /* ─── Reset & Variables ─────────────────────────────── */
    *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

    :root {
      --bg:           #02131c;
      --bg2:          #031824;
      --cyan:         #00f0e0;
      --purple:       #0080ff;
      --pink:         #00c8f0;
      --cyan-dim:     rgba(0, 240, 224, 0.15);
      --purple-dim:   rgba(0, 128, 255, 0.15);
      --pink-dim:     rgba(0, 200, 240, 0.15);
      --glass:        rgba(0, 200, 255, 0.04);
      --glass-hover:  rgba(0, 200, 255, 0.07);
      --border:       rgba(0, 200, 255, 0.10);
      --border-glow:  rgba(0, 240, 224, 0.25);
      --text:         #d0f4ff;
      --text-dim:     rgba(196, 234, 255, 0.75);
      --hero-name:    #ffffff;
      --input-bg:     rgba(255,255,255,0.03);
      --grid-line:    rgba(0,245,255,0.012);
      --bg-rgb:       2, 19, 28;
      --nav-w:        160px;
    }

    [data-theme="light"] {
      --bg:           #e7ebf6;
      --bg2:          #d5dff0;
      --cyan:         #006b80;
      --purple:       #004bb0;
      --pink:         #006580;
      --cyan-dim:     rgba(0, 107, 128, 0.12);
      --purple-dim:   rgba(0, 75, 176, 0.12);
      --pink-dim:     rgba(0, 101, 128, 0.12);
      --glass:        rgba(0, 60, 100, 0.04);
      --glass-hover:  rgba(0, 60, 100, 0.08);
      --border:       rgba(0, 60, 100, 0.15);
      --border-glow:  rgba(0, 107, 128, 0.45);
      --text:         #031520;
      --text-dim:     rgba(3, 21, 32, 0.7);
      --hero-name:    #031520;
      --input-bg:     rgba(0,30,50,0.04);
      --grid-line:    rgba(0,100,150,0.028);
      --bg-rgb:       231, 235, 246;
    }

    /* Hero hardcoded colors → dark equivalents in light mode */
    [data-theme="light"] .hero-greeting { color: rgba(3, 21, 32, 0.7) !important; }
    [data-theme="light"] .hero-desc-text { color: rgba(3, 21, 32, 0.72) !important; }
    [data-theme="light"] .hero-desc-text strong { color: rgba(3, 21, 32, 0.95) !important; }
    [data-theme="light"] .hero-desc-line {
      background: linear-gradient(180deg, rgba(0, 107, 128, 0.6), transparent) !important;
    }
    [data-theme="light"] .hero-corner,
    [data-theme="light"] .hero-part,
    [data-theme="light"] .hero-float { color: rgba(3, 21, 32, 0.55) !important; }
    [data-theme="light"] .hero-pill { color: rgba(3, 21, 32, 0.7); border-color: rgba(3, 21, 32, 0.18); }

    /* Buttons in light mode */
    [data-theme="light"] .btn-primary {
      background: rgba(3, 21, 32, 0.9);
      color: #ffffff;
      border-color: rgba(3, 21, 32, 0.9);
      box-shadow: 0 4px 24px rgba(0, 107, 128, 0.15);
    }
    [data-theme="light"] .btn-primary:hover {
      background: rgba(3, 21, 32, 1);
    }
    [data-theme="light"] .btn-ghost {
      color: rgba(3, 21, 32, 0.85);
      border-color: rgba(0, 107, 128, 0.4);
      background: rgba(0, 107, 128, 0.06);
    }
    [data-theme="light"] .btn-ghost:hover {
      color: var(--cyan);
      background: rgba(0, 107, 128, 0.12);
    }

    html { scroll-behavior: smooth; }

    body {
      background: var(--bg);
      color: var(--text);
      font-family: var(--font-display);
      overflow-x: hidden;
      min-height: 100vh;
      -ms-overflow-style: none;
      scrollbar-width: none;
      transition: background 0.4s ease, color 0.4s ease;
    }

    body::-webkit-scrollbar,
    html::-webkit-scrollbar { width: 0; height: 0; display: none; }

    html { scrollbar-width: none; -ms-overflow-style: none; }

    /* ─── Background ────────────────────────────────────── */
    .grid-bg {
      position: fixed;
      inset: 0;
      pointer-events: none;
      z-index: 0;
    }

    /* Fine film grain — a tiled greyscale fractal-noise texture (inline
       SVG feTurbulence, no external file) blended over the flat
       background so it reads a little grittier/analog instead of dead
       smooth. Sits on .grid-bg (z-index 0) so it only grains the
       backdrop; page content sits above it and stays crisp. Works in
       both themes since the grain just lightens/darkens whatever colour
       is behind it. */
    .grid-bg::before {
      content: '';
      position: absolute;
      inset: 0;
      pointer-events: none;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='fg'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23fg)'/%3E%3C/svg%3E");
      background-size: 150px 150px;
      opacity: 0.105;
      mix-blend-mode: multiply;
    }

    .grid-bg::after {
      content: '';
      position: absolute;
      inset: 0;
      background: radial-gradient(ellipse 80% 50% at 50% -10%, rgba(0,128,255,0.12), transparent),
                  radial-gradient(ellipse 60% 40% at 90% 90%, rgba(0,240,224,0.08), transparent);
    }

    /* Ambient glow for the flower field, scoped to #guestbook itself
       (not .grid-bg, which is position:fixed and would otherwise smear
       this same glow across every other section's viewport-bottom too —
       e.g. right on top of the About illustration). Sits behind the
       planted flowers (.flowerfield is z-index:5) and the grass canvas. */
    #guestbook {
      position: relative;
    }
    #guestbook::before {
      content: '';
      position: absolute;
      inset: 0;
      z-index: 0;
      pointer-events: none;
      /* linear, not radial — a radial ellipse fades out toward the left
         and right edges of the screen, reading as an uneven patch
         rather than a clean strip flush with the bottom edge where the
         planted flowers sit. */
      background: linear-gradient(to top, rgba(0,128,255,0.16), transparent 20%);
    }

    /* Desktop: the same blue bottom glow runs across the ENTIRE site
       (every stop of the horizontal deck), not just the flower field.
       body::after is free on desktop — the mobile frosted strip that
       uses it lives inside the max-width:900px block below — and
       position:fixed means one viewport-wide band regardless of which
       section is in view. z-index 0 keeps it behind main (z1), the
       ground strip and the chicks, same layering as the guestbook-
       scoped glow it replaces (hidden here to avoid doubling up in
       that one section). */
    @media (min-width: 901px) {
      body::after {
        content: '';
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        height: 100%;
        z-index: 0;
        pointer-events: none;
        background: linear-gradient(to top, rgba(0,128,255,0.16), transparent 20%);
      }
      #guestbook::before { display: none; }
    }

    /* ─── Floating home icon ──────────────────────────────
       Appears in the top-left once the hero section has scrolled out
       of view. Click to return to the homepage. Uses the same ginkgo
       / flower-spark icon as the nav-bar marker, via CSS mask so it
       picks up the current text colour on both light and dark themes. */
    .home-icon {
      position: fixed;
      top: calc(20px + 2vh + env(safe-area-inset-top, 0px));
      left: calc(32px + 5vw);
      width: 58px;
      height: 58px;
      padding: 0;
      background: transparent;
      border: none;
      cursor: pointer;
      opacity: 0;
      transform: translateX(-60px);
      transition: opacity 0.45s ease,
                  transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
      pointer-events: none;
      z-index: 90;
    }
    .home-icon.visible {
      opacity: 1;
      transform: translateX(0);
      pointer-events: auto;
    }
    .home-icon::before {
      content: '';
      display: block;
      width: 100%;
      height: 100%;
      background-color: var(--text);
      -webkit-mask-image: url('../Images/home-icon.png');
              mask-image: url('../Images/home-icon.png');
      -webkit-mask-repeat: no-repeat;
              mask-repeat: no-repeat;
      -webkit-mask-position: center;
              mask-position: center;
      -webkit-mask-size: contain;
              mask-size: contain;
      transition: transform 0.3s ease;
    }
    .home-icon:hover::before { transform: scale(1.08); }
    @media (max-width: 900px) {
      .home-icon { top: 14px; left: calc(16px + 5vw); width: 51px; height: 51px; }

      /* Prevent horizontal overflow on mobile — without these, the
         work-grid's row of 390px-fixed project cards stretches the body
         to ~1600px wide and pushes the floating nav off-screen. */
      html, body { overflow-x: hidden; max-width: 100vw; }
      main, main > section { max-width: 100vw; }

      .work-grid {
        flex-direction: column;
        gap: 18px;
      }
      .project-card {
        width: 100%;
        min-width: 0;
        max-width: 100%;
      }

      /* Make sure the floating nav and the home icon sit above the
         frosted blur strips. */
      #sidenav { z-index: 120; }
      .home-icon { z-index: 120; }
    }

    /* ─── ASCII ground (plants + grass) at the bottom ──────
       A 4-line monospace strip pinned to the bottom of the viewport.
       Stays behind interactive overlays (theme toggle, music toggle,
       sidenav) via z-index. pointer-events: none so it never blocks
       clicks. The text is generated by home.js so it always tiles to
       the current viewport width. */
    .ground {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      pointer-events: none;
      user-select: none;
      z-index: 1;
      overflow: hidden;
    }
    .ground-art {
      margin: 0;
      padding: 0 0 4px 0;
      font-family: var(--font-mono);
      font-size: 11px;
      line-height: 1.05;
      letter-spacing: 0;
      color: color-mix(in srgb, var(--cyan) 38%, transparent);
      text-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 22%, transparent);
      white-space: pre;
      opacity: 0.85;
    }
    /* Slightly bigger on desktop so the plants read clearly. */
    @media (min-width: 901px) {
      .ground-art { font-size: 12px; }
    }

    /* ─── Top Nav Bar ──────────────────────────────────── */
    #sidenav {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      width: 100%;
      height: 120px;
      display: flex;
      flex-direction: row;
      align-items: flex-start;
      justify-content: flex-end;
      padding: calc(21px + 2vh) 88px 0 32px;
      z-index: 100;
      background: linear-gradient(
        to bottom,
        rgba(var(--bg-rgb), 0.95) 0%,
        rgba(var(--bg-rgb), 0.85) 30%,
        rgba(var(--bg-rgb), 0.55) 60%,
        rgba(var(--bg-rgb), 0.22) 82%,
        rgba(var(--bg-rgb), 0) 100%
      );
      backdrop-filter: blur(14px);
      -webkit-backdrop-filter: blur(14px);
      -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 50%, rgba(0,0,0,0) 100%);
              mask-image: linear-gradient(to bottom, #000 0%, #000 50%, rgba(0,0,0,0) 100%);
      transition: background 0.4s ease;
    }

    #sidenav::before { display: none; }

    .nav-logo { display: none; }

    .current-page {
      display: none;
      align-items: center;
      gap: 8px;
      font-family: var(--font-mono);
      font-size: 13px;
      letter-spacing: 2px;
      text-transform: uppercase;
      color: var(--cyan);
      padding: 6px 12px;
      background: none;
      border: none;
      cursor: pointer;
      transition: opacity 0.2s ease;
    }

    /* ─── Theme toggle (sliding switch) ──────────────────────
       Geometry: a 76×36 pill with a 26×26 knob that slides between
       left:4 and left: calc(100% - 4 - 26) = 46. Knob centers fall at
       x=17 (dark) and x=59 (light), which are symmetric around the
       toggle center (38). Each icon is positioned by its centre using
       translate(-50%, -50%), so it sits exactly on the knob in its
       corresponding state regardless of the icon's intrinsic size. */
    .theme-toggle {
      position: fixed;
      bottom: calc(20px + 2vh + env(safe-area-inset-bottom, 0px));
      right: 32px;
      top: auto;
      width: 76px;
      height: 36px;
      border-radius: 100px;
      background: var(--bg);
      border: 2px solid var(--border-glow);
      cursor: pointer;
      padding: 0;
      z-index: 200;
      transition: background 0.3s ease, border-color 0.3s ease;
    }
    .theme-toggle:hover { border-color: var(--cyan); }

    /* Sliding knob — vertically centred, slides horizontally on theme change. */
    .theme-toggle::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 4px;
      width: 26px;
      height: 26px;
      border-radius: 50%;
      background: var(--cyan);
      box-shadow: 0 2px 6px rgba(0, 40, 70, 0.3);
      transform: translateY(-50%);
      transition: left 0.34s cubic-bezier(0.34, 1.45, 0.5, 1);
      z-index: 1;
    }
    [data-theme="light"] .theme-toggle::before { left: calc(100% - 4px - 26px); }

    /* Knob press feedback (no-op on transform; only adjust shadow so the
       horizontal-slide animation isn't fighting a transform animation). */
    .theme-toggle.squish::before { animation: theme-knob-squish 0.4s ease; }
    @keyframes theme-knob-squish {
      0%, 100% { box-shadow: 0 2px 6px rgba(0, 40, 70, 0.3); }
      50%      { box-shadow: 0 2px 12px rgba(0, 40, 70, 0.45); }
    }

    /* Icons — positioned by centre with translate(-50%, -50%). The
       left/right values match the knob centres (17 and 59 respectively
       in a 76-wide toggle), so the active icon is dead-centre on the
       knob in every state. */
    .theme-toggle svg {
      position: absolute;
      top: 50%;
      width: 14px;
      height: 14px;
      transform: translate(-50%, -50%);
      stroke: var(--bg);
      fill: none;
      stroke-width: 2;
      stroke-linecap: round;
      stroke-linejoin: round;
      z-index: 2;
      pointer-events: none;
    }
    .theme-toggle .icon-moon { left: 17px; display: block; }
    .theme-toggle .icon-sun  { left: calc(100% - 17px); display: none; }
    [data-theme="light"] .theme-toggle .icon-sun  { display: block; }
    [data-theme="light"] .theme-toggle .icon-moon { display: none; }

    /* ─── Music toggle ──────────────────────────────────────
       Sits just to the LEFT of the theme toggle. Same height as the theme
       toggle for visual pairing. The icon swaps between sound-off (muted,
       default) and sound-on (playing). Driven by .playing class added by
       home.js when audio actually starts. */
    .music-toggle {
      position: fixed;
      bottom: calc(20px + 2vh + env(safe-area-inset-bottom, 0px));
      right: calc(32px + 76px + 12px); /* theme-toggle width 76 + 12 gap */
      width: 36px;
      height: 36px;
      border-radius: 50%;
      background: var(--bg);
      border: 2px solid var(--border-glow);
      color: var(--cyan);
      cursor: pointer;
      padding: 0;
      z-index: 200;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      transition: background 0.3s ease, border-color 0.3s ease, transform 0.3s ease;
    }
    .music-toggle:hover { border-color: var(--cyan); transform: translateY(-1px); }
    .music-toggle.playing { border-color: var(--cyan); }

    /* Equalizer sound bars — 4 thin vertical bars centred in the pill.
       Animation runs when .music-toggle has .playing (added by JS on play).
       When idle, bars are flat at 25% height. */
    .sound-bars {
      display: inline-flex;
      align-items: flex-end;
      justify-content: center;
      gap: 2px;
      width: 18px;
      height: 14px;
      pointer-events: none;
    }
    .sound-bars > span {
      display: block;
      width: 2.5px;
      height: 25%;
      background: currentColor;
      border-radius: 1.5px;
      transform-origin: bottom center;
      transition: height 0.25s ease;
    }
    .music-toggle.playing .sound-bars > span {
      animation: sound-bar-bounce 1s ease-in-out infinite;
    }
    /* Each bar gets a different keyframe rhythm so they don't all move in
       sync — feels like a real equalizer rather than a metronome. */
    .music-toggle.playing .sound-bars > span:nth-child(1) {
      animation-duration: 0.9s; animation-delay: -0.20s;
    }
    .music-toggle.playing .sound-bars > span:nth-child(2) {
      animation-duration: 1.1s; animation-delay: -0.55s;
    }
    .music-toggle.playing .sound-bars > span:nth-child(3) {
      animation-duration: 0.8s; animation-delay: -0.10s;
    }
    .music-toggle.playing .sound-bars > span:nth-child(4) {
      animation-duration: 1.0s; animation-delay: -0.40s;
    }
    @keyframes sound-bar-bounce {
      0%, 100% { height: 25%; }
      20%      { height: 90%; }
      40%      { height: 45%; }
      60%      { height: 100%; }
      80%      { height: 30%; }
    }

    .current-page:hover { opacity: 0.75; }

    .current-page-arrow {
      transition: transform 0.25s ease;
      color: var(--cyan);
    }

    .current-page.open .current-page-arrow {
      transform: rotate(180deg);
    }

    .nav-items {
      display: flex;
      flex-direction: row;
      gap: 4px;
      align-items: center;
    }

    .nav-track { display: none; }
    .nav-indicator { display: none; }

    .nav-item {
      display: flex;
      align-items: center;
      gap: 0;
      padding: 8px 18px;
      cursor: pointer;
      position: relative;
      transition: all 0.3s ease;
      text-decoration: none;
      color: var(--text-dim);
    }

    .nav-dot { display: none; }

    .nav-label {
      font-family: var(--font-mono);
      font-size: 17px;
      letter-spacing: 2px;
      text-transform: uppercase;
      transition: all 0.3s ease;
    }

    .nav-item.active .nav-label { color: var(--cyan); }
    .nav-item:not(.active) .nav-label { opacity: 0.7; }
    .nav-item:hover .nav-label { opacity: 1; }

    .nav-number { display: none; }

    /* ─── Desktop: collapsed "Kathlyn" pill that expands to a vertical
           dropdown on hover ──────────────────────────────────────── */
    @media (min-width: 901px) {
      #sidenav {
        top: calc(20px + 2vh);
        right: 32px;
        left: auto;
        width: auto;
        height: auto;
        padding: 0;
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        background: none;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        -webkit-mask-image: none;
                mask-image: none;
        overflow: visible;
      }

      /* Pill trigger is hidden on desktop — sections are laid out
         directly as a horizontal nav row, no hover card needed. */
      .current-page { display: none; }
      .current-page::after { content: none; }

      /* Desktop: no slanted back card. The pill stands alone at rest;
         the horizontal dropdown (.nav-items) carries its own subtle
         rectangular backdrop. Mobile still uses the slanted-card design
         (overridden inside the max-width: 900px media query). */
      #sidenav { position: fixed; isolation: isolate; }
      #sidenav::before { display: none; }
      .current-page { position: relative; z-index: 1; }

      .current-page:hover,
      #sidenav:hover .current-page,
      #sidenav.open .current-page { opacity: 1; border-color: var(--cyan); transform: translate(0, -2px); }
      /* The pill stays where it is; .nav-items grows leftward from the
         right edge via width: max-content. No need for #sidenav width
         tricks. */
      #sidenav:hover .current-page-arrow,
      #sidenav.open .current-page-arrow { transform: rotate(180deg); }

      .nav-items {
        /* Inline always-visible horizontal nav row on desktop. */
        position: relative;
        top: auto;
        right: auto;
        left: auto;
        width: auto;
        display: flex;
        flex-direction: row;
        align-items: center;
        gap: 6px;
        min-width: 0;
        padding: 0;
        background: transparent;
        border: none;
        border-radius: 0;
        box-shadow: none;
        opacity: 1;
        clip-path: none;
        pointer-events: auto;
        transition: none;
      }
      .nav-items::before { display: none; } /* No hover-bridge needed. */

      .nav-item {
        display: flex;
        align-items: center;
        flex: 0 0 auto;
        padding: 10px 13px;
        border-radius: 0;
        border-bottom: none;
        color: var(--text-dim);
        text-decoration: none;
        white-space: nowrap;
        position: relative; /* anchor for the ::after ASCII underline */
        background: transparent; /* no hover-fill — animation does the work */
        transition: color 0.25s ease;
      }
      .nav-item .nav-label {
        font-size: 17px;
        letter-spacing: 2.1px;
        opacity: 0.78;
        transition: color 0.25s ease, opacity 0.25s ease;
      }
      .nav-item:hover .nav-label,
      .nav-item.active .nav-label { color: var(--cyan); opacity: 1; }

      /* ─── Nav flower marker ────────────────────────────────
         A 5-petal daisy-style flower with thin elliptical black petals
         and a white/blank core. Every nav item reserves space for the
         flower so the layout is stable; opacity controls which item
         actually shows it:
         - At rest, only the .active item shows the flower.
         - When the user hovers anywhere inside .nav-items, the active
           flower fades out and the hovered item's flower fades in.
         - When the hover leaves, the active item's flower fades back. */
      .nav-item::before {
        content: '';
        display: inline-block;
        width: 32px;
        height: 32px;
        margin-right: 2px;
        /* Hand-drawn ginkgo-leaf icon (transparent PNG). Used as a CSS
           mask so the icon picks up the current text colour, which
           keeps it readable on both light and dark themes. */
        background-color: var(--text);
        -webkit-mask-image: url('../Images/nav-icon.png');
                mask-image: url('../Images/nav-icon.png');
        -webkit-mask-repeat: no-repeat;
                mask-repeat: no-repeat;
        -webkit-mask-position: center;
                mask-position: center;
        -webkit-mask-size: contain;
                mask-size: contain;
        flex: 0 0 auto;
        align-self: center;
        /* Sits slightly above the line-box centre. */
        transform: translateY(-2px);
        opacity: 0;
        transition: opacity 0.25s ease;
      }
      /* Default: only the active section's flower is shown. */
      .nav-item.active::before { opacity: 1; }
      /* On nav hover: hide every flower by default, then let the hovered
         item's flower take over. Rule order matters — the :hover rule
         must come after the row-hover rule so it wins via source order
         when both apply to the hovered item. */
      .nav-items:hover .nav-item::before { opacity: 0; }
      .nav-items:hover .nav-item:hover::before { opacity: 1; }

      /* ─── Hover effect #6: letter wave ─────────────────────
         Each letter is a span (created by home.js) carrying a --i index.
         On hover, every letter lifts -3px with a 30ms-per-letter delay
         so it ripples left → right, then settles back the same way on
         leave. Uses a slightly springy cubic-bezier for a tactile feel. */
      .nav-letter {
        display: inline-block;
        transform: translateY(0);
        transition: transform 0.32s cubic-bezier(0.34, 1.45, 0.5, 1),
                    color 0.25s ease;
        transition-delay: calc(var(--i, 0) * 30ms);
      }
      .nav-item:hover .nav-letter {
        transform: translateY(-3px);
      }

      /* Hover state: letter wave only — the ASCII grass underline has
         been removed per design. The cyan label colour indicates active. */

      /* On desktop the hover dropdown replaces the click hamburger menu */
      .hamburger-dropdown { display: none !important; }
    }

    /* ─── Main Content ──────────────────────────────────── */
    main {
      margin-left: 0;
      margin-right: 0;
      position: relative;
      z-index: 1;
    }

    section {
      min-height: 100vh;
      padding: 80px 60px 80px 80px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      position: relative;
    }

    /* ─── Desktop: horizontal page layout ─────────────────────
       On desktop (>=901px) the page becomes a horizontal slide deck —
       sections live in a row inside <main>, each one viewport-wide, and
       horizontal scroll moves between them. Native vertical wheel events
       are translated to horizontal scroll by a small handler in
       assets/site-effects.js. Mobile (<=900px) keeps the original
       vertical stacked layout untouched. */
    @media (min-width: 901px) {
      /* Fixed desktop design width. --u is a "locked" viewport-width
         unit: it equals 1vw on screens at least as wide as --design-w,
         but stops shrinking below it (the max() clamps to design-w/100).
         So once the window is narrower than the design width, the
         horizontal layout keeps its full-screen scale and the deck
         simply crops / scrolls sideways instead of reflowing. Everything
         horizontal below is sized in var(--u) rather than raw vw; swap
         --design-w to retarget. Mobile (<=900px) keeps its own
         responsive layout untouched — --u only exists in this block. */
      :root {
        --design-w: 1440px;
        --u: max(1vw, calc(var(--design-w) / 100));
      }
      html, body {
        overflow: hidden;
        height: 100vh;
      }
      main {
        display: flex;
        flex-direction: row;
        flex-wrap: nowrap;
        width: 100vw;
        height: 100vh;
        overflow-x: auto;
        overflow-y: hidden;
        scroll-snap-type: none;     /* no snap — fully free / lerp-smooth scrolling */
        scroll-behavior: auto;      /* our wheel handler does the smoothing */
        scrollbar-width: none;
      }
      main::-webkit-scrollbar { display: none; }
      main > section {
        flex: 0 0 calc(var(--u) * 100);
        width: calc(var(--u) * 100);
        height: 100vh;
        min-height: 100vh;
        /* No vertical scrolling inside any section — content that would
         exceed the viewport is clipped instead. The page only scrolls
         horizontally between sections. */
        overflow-y: hidden;
        overflow-x: hidden;
        scrollbar-width: none;
      }
      main > section::-webkit-scrollbar { display: none; }
      /* Hero content (greeting + name + starter-pack) only fills a
         fraction of a full 100vw screen, leaving ~840px of dead space
         before Work — shrink hero to hug its content instead of
         staying pinned at 100vw. min-width keeps it reading as a
         proper opening beat rather than a sliver, while still closing
         most of that dead gap (paired with the small padding-right
         trim on #hero itself below). */
      main > #hero {
        flex: 0 0 auto;
        width: max-content;
        min-width: calc(var(--u) * 85);
      }
      /* Work section is widened beyond 100vw so all 4 fixed-width project
         pills (390px each) fit naturally as part of the main horizontal
         scroll — no need for an inner scrollbar inside the section.
         4 × 390 + 3 × 16 gap + 2 × 16 padding = 1640px. */
      main > #work {
        flex: 0 0 auto;
        width: max-content;
        min-width: calc(var(--u) * 100);
      }
      /* Playground is no longer a section in the horizontal deck — it's
         a hidden bottom-sliding drawer (see .games-drawer in games.css)
         opened by the arrow in #outro below, so #play needs no deck
         positioning here anymore. */
      /* Bridge note after the flower field, now the last section in
         the deck — widened back to the shared 100vw/full-viewport
         sizing (no override needed here beyond the base main >
         section rule) so it reads as a deliberate closing beat rather
         than a narrow afterthought squeezed in at the end. */
      /* About section content (text + photos) — top-align so "Hey!" is
         visible from the start of the section, and tighter top padding
         so the full content fits without needing vertical scroll.
         Extra right padding pushes the photos away from the section
         edge so there's no abrupt seam where about meets the flower
         field. Widened back up from an earlier 80vw (which was chosen
         to shorten the Connect+Guestbook span between Work and
         Playground) since the narrower width was squeezing the text and
         photo columns against each other. flex-basis (set via the flex
         shorthand) wins over width on a flex item, so both need
         overriding or the base flex:0 0 100vw rule keeps it at full
         width regardless of the width value here. */
      main > #connect {
        justify-content: flex-start;
        flex: 0 0 calc(var(--u) * 100);
        width: calc(var(--u) * 100);
        /* +6vh of extra clearance above the bio text (stacked in two
           +3vh passes) — the home icon sits fixed at
           top: calc(20px + 2vh), roughly the same top-left corner as
           "Hey there, I'm Kathlyn", so the text needed more room below
           it instead of crowding it. */
        padding-top: calc(40px + 5vh);
        padding-bottom: 24px;
        padding-right: 60px;
        /* Tighter left padding (was the base section rule's 80px)
           closes up the gap between Work and About — combined with
           #work's own reduced padding-right below, this halves the
           dead space between the two sections. */
        padding-left: 40px;
      }
      /* Tighter right padding pulls Work's last project card closer to
         About right after it (paired with #connect's own reduced
         padding-left above). Tighter left padding pulls Work's first
         project card closer to the hero right before it (paired with
         #hero's own reduced padding-right below). */
      main > #work {
        /* 8px was flush enough that the first card's hover state — which
           scales 1.03 AND straightens the resting -2deg tilt — pushed its
           top-left corner to about -5px, where the section's own
           overflow:hidden sliced it off. 24px is still tight against the
           hero but leaves room for the card to grow into. */
        padding-left: 24px;
        padding-right: 30px;
      }
      /* Tighter left padding pulls the flower field closer to About;
         tighter right padding pulls it closer to the #outro bridge
         section right after it. */
      main > #guestbook {
        padding-left: 60px;
        padding-right: 30px;
      }
      /* Tighter left padding than the base section rule (80px) closes
         up the gap between the flower field and the farewell note that
         follows it, and shifts the note itself further left. */
      main > #outro {
        padding-left: 12px;
      }
      /* The chain backdrop wrapper (only present in some builds) needs to
         participate in the horizontal flex flow too — it wraps multiple
         sections, so we make it a contents-only container. */
      main > .chain-wrap {
        display: contents;
      }
    }

    /* ─── Hero Section ──────────────────────────────────── */
    #hero {
      overflow: hidden;
      /* Top-aligned so the greeting line sits at the same vertical
         position as the top nav-bar items. Trimmed down further (was
         11vh, then 4vh) now that the starter-pack illustration below
         the name is sized off viewport height and needs the headroom —
         see .hero-starter-pack's own clamp(). Bottom padding trimmed
         too since nothing sits below the illustration in this section;
         it was just trailing whitespace. Right padding also trimmed
         (was 60px) to close up the gap to Work right after it (paired
         with #work's own reduced padding-left). */
      padding: calc(20px + 2vh) 6px 4vh 80px;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      justify-content: flex-start;
      position: relative;
    }

    /* Corner micro labels */
    .hero-corner {
      position: absolute;
      font-family: var(--font-mono);
      font-size: 13px;
      letter-spacing: 2.5px;
      color: rgba(200,210,255,0.3);
      text-transform: uppercase;
    }
    .hero-corner.tl { top: 36px; left: 80px; }
    .hero-corner.tr {
      top: 36px; right: 24px;
      display: flex; gap: 16px; align-items: center;
    }
    .hero-corner.bl { bottom: calc(36px + 10vh); left: 80px; }
    .hero-corner.br { bottom: 36px; right: 24px; }

    .hero-corner-sym {
      font-size: 16px;
      color: rgba(0,245,255,0.4);
      line-height: 1;
    }

    /* Part counter */
    .hero-part {
      font-family: var(--font-mono);
      font-size: 10px;
      letter-spacing: 3px;
      color: rgba(200,210,255,0.35);
      text-transform: uppercase;
      margin-bottom: 20px;
    }

    .hero-name-block { margin-top: 10vh; width: 100%; }
    /* Greeting stays pinned at top; the big "Kathlyn Teh" name drops
       below it. Extra clearance (was 1vh) accounts for the greeting's
       own visual-only translateY(5vh) shift (see its inline style in
       index.html) plus its larger font-size — that shift doesn't
       affect document flow, so without this the name would overlap
       the now-larger, lower-sitting greeting text above it. */
    .hero-name-outer { margin-top: 2vh; }

    /* Stacks the big name above the looping starter-pack illustration.
       align-items:flex-start pins the illustration to the same left
       edge as the name text — i.e. the left side of the hero content,
       not centered or trailing under any particular word. */
    .hero-name-row {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: clamp(14px, calc(var(--u, 1vw) * 1.6), 22px);
      /* Full width (with the block below) so the centred starter-pack
         illustration centres on the screen, not just within the name's
         own shrink-wrapped width. The name itself stays left via
         align-items:flex-start. */
      width: 100%;
    }

    /* Big editorial name */
    .hero-name-outer {
      position: relative;
      display: inline-block;
      z-index: 2;
      margin-bottom: 0;
    }

    /* Roles under the name: a tagline whose role word rotates (driven by
       home.js), plus a row of hand-drawn discipline tags echoing the
       Work cards' wobbly-ink badges. Theme-token colours so it adapts to
       the light/dark toggle. */
    .hero-roles {
      margin-top: clamp(12px, calc(var(--u, 1vw) * 1.4), 22px);
      position: relative;
      z-index: 2;
    }
    .hero-role-line {
      margin: 0 0 14px;
      font-family: var(--font-mono);
      font-size: clamp(16px, calc(var(--u, 1vw) * 1.45), 22px);
      line-height: 1.6;
      letter-spacing: 0.3px;
      color: var(--text-dim);
      text-wrap: pretty;
    }
    .hero-role-rotator {
      display: inline-block;
      box-sizing: border-box;
      position: relative;
      isolation: isolate;
      /* Width is pinned to the widest role in JS; shorter roles centre
         inside it so the text after the box never shifts. */
      text-align: center;
      font-family: var(--font-doodle), var(--font-display);
      font-size: 1.7em;
      line-height: 1;
      color: var(--cyan);
      padding: 7px 19px 9px;
      transition: opacity 0.28s ease;
    }
    /* Hand-drawn wriggly box around the role — an SVG outline used as a
       mask over a theme-coloured fill, so the line genuinely wobbles
       (waves along every edge, not just rounded corners) yet still adapts
       to the light/dark toggle. Sits behind the text via z-index. */
    .hero-role-rotator::before {
      content: '';
      position: absolute;
      inset: 0;
      z-index: -1;
      background: var(--border-glow);
      -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 44' preserveAspectRatio='none'%3E%3Cpath d='M16 6 Q44 3 62 5 Q82 7 104 5 Q115 5 115 15 Q117 22 115 31 Q115 39 104 39 Q82 41 60 39 Q40 37 16 39 Q5 39 5 29 Q3 22 5 15 Q5 5 16 6 Z' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / 100% 100%;
              mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 44' preserveAspectRatio='none'%3E%3Cpath d='M16 6 Q44 3 62 5 Q82 7 104 5 Q115 5 115 15 Q117 22 115 31 Q115 39 104 39 Q82 41 60 39 Q40 37 16 39 Q5 39 5 29 Q3 22 5 15 Q5 5 16 6 Z' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / 100% 100%;
      pointer-events: none;
    }
    .hero-role-rotator.is-swapping { opacity: 0; }
    /* First line of the tagline: larger + bold; the rest sits below it
       with a small paragraph gap. */
    .hero-role-lead {
      display: block;
      font-weight: 700;
      font-size: 1.16em;
    }
    .hero-role-rest {
      display: block;
      margin-top: 0.75em;
    }
    .hero-role-tags {
      list-style: none;
      margin: 0;
      padding: 0;
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
    }
    .hero-role-tags li {
      font-family: var(--font-doodle), var(--font-display);
      font-size: 20px;
      line-height: 1;
      letter-spacing: 0.5px;
      padding: 5px 14px 7px;
      border: 1.6px solid var(--border-glow);
      border-radius: 12px 6px 12px 6px / 6px 12px 6px 12px;
      color: var(--text-dim);
      white-space: nowrap;
    }
    /* Desktop: float the tags to the RIGHT of the starter-pack image, in
       the hero's empty horizontal space. Absolute so they add no height
       to the tight full-screen column (the tall pack would otherwise get
       pushed off the bottom). */
    @media (min-width: 901px) {
      .hero-name-row { position: relative; }
      .hero-roles {
        position: absolute;
        left: clamp(300px, calc(var(--u, 1vw) * 35), 480px);
        top: 58%;
        transform: translateY(-50%);
        margin: 0;
        max-width: calc(var(--u, 1vw) * 34);
      }
    }

    /* Two photos hard-cutting back and forth in place — see the
       setInterval loop in home.js that swaps which one carries
       .is-active. Sized off viewport HEIGHT (with aspect-ratio deriving
       the width) rather than vw — every section is a fixed 100vh with
       overflow-y:hidden on desktop (see main > section), so a portrait
       illustration this large has to respect vertical space or it gets
       silently clipped at the bottom; vw alone can't account for that
       on shorter/wider windows. */
    .hero-starter-pack {
      position: relative;
      flex-shrink: 0;
      /* +5% over the previous clamp(300px, 60vh, 560px). */
      height: clamp(315px, 63vh, 588px);
      width: auto;
      max-width: 441px;
      aspect-ratio: 3 / 4;
      overflow: hidden;
      /* Counteracts the extra 10vh added to .hero-name-block below —
         that shift is meant for the greeting/name text only; without
         this the whole flex row (including this tall portrait image)
         would drop with it and clip against the 100vh/overflow:hidden
         section bottom. */
      margin-top: -10vh;
    }
    .hero-starter-pack-img {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
      opacity: 0;
    }
    .hero-starter-pack-img.is-active { opacity: 1; }

    .hero-name {
      font-family: var(--font-doodle), var(--font-display);
      /* +20% over the previous clamp(52px, 9vw, 130px) — desktop hero
         title bumped up (mobile ≤600px has its own 32vw override).
         var(--u,1vw) so on desktop the size locks at the design width
         (stops shrinking when the window narrows); below 901px --u is
         undefined and it falls back to plain 1vw. */
      font-size: clamp(62px, calc(var(--u, 1vw) * 10.8), 156px);
      line-height: 1.02;
      /* Tight overall tracking (letters sit close); the "lyn" letters
         get their own extra margin inline in index.html so only they —
         the ones that kiss when they tilt — are spaced apart. */
      letter-spacing: 0.02em;
      margin-bottom: 0;
      position: relative;
    }

    .hero-name .first {
      display: inline;
      color: var(--hero-name);
    }

    .hero-name .last {
      display: inline;
      color: var(--hero-name);
    }

    /* Each letter tilts independently, staggered left-to-right via --i
       (set inline per <span class="letter"> in index.html) so it reads
       as a little wave rippling across the whole name. Snaps between
       fixed poses rather than smoothly swaying — each keyframe sets
       its own steps(1) timing so the rotate value holds then jumps
       straight to the next one, like a flipbook rather than a wobble. */
    .hero-name .letter {
      display: inline-block;
      transform-origin: 50% 90%;
      animation: hero-name-tilt 2.8s infinite;
      animation-delay: calc(var(--i, 0) * 0.14s);
    }
    @keyframes hero-name-tilt {
      0%   { rotate: 0deg;                          animation-timing-function: steps(1, jump-end); }
      33%  { rotate: calc(-1 * var(--amp, 7deg));   animation-timing-function: steps(1, jump-end); }
      66%  { rotate: var(--amp, 7deg);              animation-timing-function: steps(1, jump-end); }
      100% { rotate: 0deg; }
    }
    @media (prefers-reduced-motion: reduce) {
      .hero-name .letter { animation: none; }
    }

    /* Descriptor + CTA row beneath name */
    .hero-desc-cta {
      display: flex;
      align-items: center;
      gap: 80px;
      margin-top: 15vh;
      position: relative;
      z-index: 2;
      flex-wrap: wrap;
    }

    .hero-desc-row {
      display: flex;
      align-items: flex-start;
      gap: 40px;
      position: relative;
      z-index: 2;
    }

    .hero-desc-line {
      width: 1px;
      height: 60px;
      background: linear-gradient(180deg, rgba(0,245,255,0.5), transparent);
      flex-shrink: 0;
      margin-top: 4px;
    }

    .hero-desc-text {
      font-size: 20px;
      color: rgba(200,210,255,0.55);
      line-height: 1.7;
      max-width: 440px;
    }

    @media (max-width: 900px) {
      .hero-desc-text { font-size: 16px; max-width: 320px; }
    }

    /* Mobile — the illustration becomes a large, centered focal point
       sized off viewport WIDTH instead of height (mobile sections
       scroll normally, so there's no fixed-100vh clipping risk to
       guard against here — see main, main > section's own
       max-width:900px override in this file). height:auto lets
       aspect-ratio derive the height from that width instead of
       fighting the desktop rule's explicit height. "Kathlyn"/"Teh"
       also go back to two stacked lines here — sharing one line reads
       too cramped at mobile widths. */
    @media (max-width: 900px) {
      .hero-starter-pack {
        height: auto;
        /* Smaller (was min(88vw,440px)) and pulled up toward the name
           (negative margin-top) so the greeting + "Kathlyn Teh" + the
           starter pack all fit within the first screen on a phone. */
        /* +2% size (was min(62vw,310px)) and shifted up 3% (margin-top
           -11vh → -14vh). */
        width: min(66vw, 330px);
        max-width: none;
        align-self: center;
        margin-left: 0;
        /* The name fills the width on mobile so the roles can't sit
           beside it — they stack below, and the pack drops under them
           (it becomes a scroll-down reward rather than overlapping). */
        margin-top: 0;
      }
      .hero-roles { margin-top: 14px; }
      .hero-role-line { margin-bottom: 8px; }
      .hero-name .first,
      .hero-name .last {
        display: block;
      }
    }

    .hero-desc-text strong { color: rgba(200,210,255,0.9); font-weight: 500; }

    /* CTA row */
    .hero-cta {
      display: flex;
      gap: 52px;
      flex-wrap: wrap;
      position: relative;
      z-index: 2;
    }

    @media (max-width: 900px) {
      .hero-desc-cta {
        gap: 32px;
        align-items: flex-start;
      }
    }

    .btn {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      padding: 20px 43px;
      border-radius: 9999px;
      font-family: var(--font-mono);
      font-size: 12px;
      letter-spacing: 2px;
      text-transform: uppercase;
      text-decoration: none;
      cursor: pointer;
      border: none;
      transition: all 0.35s ease;
    }

    .btn-primary {
      background: rgba(255,255,255,0.12);
      color: #ffffff;
      font-weight: 600;
      border: 1px solid rgba(255,255,255,0.25);
      box-shadow: 0 4px 24px rgba(0,245,255,0.12), inset 0 1px 0 rgba(255,255,255,0.2);
    }

    .btn-primary:hover {
      transform: translateY(-2px) scale(1.03);
      background: rgba(255,255,255,0.18);
      box-shadow: 0 8px 32px rgba(0,245,255,0.22), inset 0 1px 0 rgba(255,255,255,0.3);
    }

    .btn-ghost {
      background: rgba(0,128,255,0.08);
      color: rgba(200,210,255,0.8);
      border: 1px solid rgba(0,128,255,0.25);
      box-shadow: 0 4px 24px rgba(0,128,255,0.1), inset 0 1px 0 rgba(255,255,255,0.08);
    }

    .btn-ghost:hover {
      transform: translateY(-2px) scale(1.03);
      background: rgba(0,128,255,0.15);
      border-color: rgba(0,245,255,0.4);
      color: var(--cyan);
      box-shadow: 0 8px 32px rgba(0,245,255,0.15), inset 0 1px 0 rgba(255,255,255,0.12);
    }

    /* ── Chrome blob ────────────────────────── */
    .hero-chains {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: calc(100% + 40vh);
      display: block;
      pointer-events: none;
      z-index: 1;
      opacity: 0.5;
    }

    /* Floating editorial labels near blob */
    .hero-float {
      position: absolute;
      font-family: var(--font-mono);
      font-size: 9px;
      letter-spacing: 2px;
      color: rgba(200,215,255,0.35);
      text-transform: uppercase;
      line-height: 1.6;
      max-width: 140px;
    }

    .hero-float.f1 { right: 380px; top: 28%; }
    .hero-float.f2 { right: 20px;  top: 22%; text-align: right; }
    .hero-float.f3 { right: 60px;  bottom: 26%; max-width: 160px; text-align: right; font-style: italic; }

    /* Barcode */
    .hero-barcode {
      position: absolute;
      bottom: 80px;
      right: 80px;
      display: flex;
      flex-direction: column;
      align-items: flex-end;
      gap: 4px;
    }

    .barcode-bars {
      display: flex;
      align-items: flex-end;
      gap: 2px;
      height: 32px;
    }

    .barcode-bars span {
      display: block;
      width: 2px;
      background: rgba(200,210,255,0.25);
    }

    .barcode-num {
      font-family: var(--font-mono);
      font-size: 8px;
      letter-spacing: 3px;
      color: rgba(200,210,255,0.2);
    }

    /* Bottom pill label */
    .hero-pill {
      position: absolute;
      bottom: 36px;
      left: 50%;
      transform: translateX(-50%);
      display: flex;
      align-items: center;
      gap: 0;
      font-family: var(--font-mono);
      font-size: 11px;
      letter-spacing: 3px;
      text-transform: uppercase;
      color: rgba(200,210,255,0.6);
      border: 1px solid rgba(200,210,255,0.15);
      border-radius: 100px;
      overflow: hidden;
    }

    .hero-pill-a {
      padding: 8px 20px;
      background: rgba(255,255,255,0.07);
    }

    .hero-pill-b {
      padding: 8px 20px;
    }

    .scroll-line {
      width: 40px;
      height: 1px;
      background: linear-gradient(90deg, transparent, var(--cyan));
      animation: scroll-pulse 2s ease-in-out infinite;
    }

    @keyframes scroll-pulse {
      0%, 100% { opacity: 0.3; }
      50% { opacity: 1; }
    }

    /* ─── Section Header ────────────────────────────────── */
    .section-tag {
      font-family: var(--font-mono);
      font-size: 14px;
      letter-spacing: 5px;
      text-transform: uppercase;
      color: var(--cyan);
      margin-bottom: 20px;
      display: flex;
      align-items: center;
      gap: 14px;
    }

    .section-tag::before {
      content: '';
      display: inline-block;
      width: 28px;
      height: 1px;
      background: var(--cyan);
    }

    .section-title {
      /* 50% smaller than before (was clamp(44, 5.5vw, 72)) — keeps the
         page hierarchy lighter, especially in the work section.
         var(--u,1vw) locks the size at the design width on desktop. */
      font-size: clamp(22px, calc(var(--u, 1vw) * 2.75), 36px);
      font-weight: 700;
      letter-spacing: -1px;
      line-height: 1.1;
      margin-bottom: 28px;
    }

    .gradient-text {
      color: var(--cyan);
      background: none;
      -webkit-background-clip: initial;
      -webkit-text-fill-color: currentColor;
      background-clip: initial;
    }

    /* ─── Glass Card ──────────────────────────────────────
       backdrop-filter removed for perf; opaque base color compensates so
       cards stay readable against the chain backdrop. */
    .glass-card {
      background: rgba(var(--bg-rgb), 0.55);
      border: 1px solid var(--border);
      border-radius: 16px;
      transition: all 0.3s ease;
    }

    .glass-card:hover {
      background: var(--glass-hover);
      border-color: var(--border-glow);
      box-shadow: 0 0 32px rgba(0,245,255,0.08);
    }

    /* ─── Story Section ─────────────────────────────────── */
    #story {
      padding-top: 100px;
    }

    /* The illustration + grid scale together as ONE proportional unit
       (see the --about-scale JS in home.js) instead of each reflowing
       independently at narrower widths — a narrower viewport should
       shrink this whole design like a scaled-down poster, not rewrap
       the paragraph into more lines or resize the image on its own
       timeline. #connect > * elsewhere sets position:relative on every
       direct child (for its own z-index stacking) — this wrapper picks
       that up for free and it's exactly what's needed here: it becomes
       the containing block for .story-lead-wrap's absolute centering
       below, and height:100% makes that centering resolve against the
       real page height rather than the content's own (much taller)
       height. */
    .story-scale-unit {
      height: 100%;
      transform: scale(var(--about-scale, 1));
      transform-origin: top left;
    }
    .story-grid {
      display: grid;
      /* text | photos — the illustration isn't a grid item here; it's
         positioned via .story-lead-wrap's own absolute positioning (see
         below), so reserving a whole grid track for it would just be
         dead space nothing occupies. margin-left below clears room for
         it instead, and the reclaimed track width goes to the paragraph
         column — wider text wraps to fewer lines, which raises where
         the Experience section starts without touching any of the
         spacing between the bio text and Experience itself. */
      grid-template-columns: minmax(0, 1.3fr) minmax(0, 0.8fr);
      gap: 90px;
      margin-top: 26px;
      /* Fixed px, not vw/clamp — .story-scale-unit's own transform is
         what shrinks this at narrower widths now (see above), so
         nothing in here should independently respond to viewport width;
         that'd double up with the transform and shrink unpredictably. */
      margin-left: calc(150px + 340px);
      width: 780px;
      align-items: start;
    }

    /* Illustration leading the About section. Taking .story-lead-wrap
       out of grid flow with position:absolute against .story-scale-unit
       (height:100%, so effectively the page height) centers it on the
       page instead of within the grid's own much-taller content row —
       align-self:center on a grid item would only center within that
       row. left:80px matches #connect's own padding-left (no override
       on that side — see main > #connect) so it still lines up with
       where the grid's first column starts; .story-text-col and
       .story-photos get explicit grid-column pins below since an
       absolutely-positioned item drops out of grid auto-placement and
       would otherwise leave them sliding left to fill columns 1–2.
       The centering transform lives on this wrapper rather than on
       .story-lead-figure itself because that element also carries
       .fade-in's own scroll-reveal transform (translateY(24px) →
       translateY(0) once .visible is added) — putting both transforms
       on the same element means whichever rule wins specificity fully
       replaces the other's, not composes with it, which silently broke
       the vertical centering once the figure scrolled into view. */
    .story-lead-wrap {
      position: absolute;
      top: 50%;
      /* scale() around the default center origin doesn't move the
         center point, so it composes cleanly with the translateY(-50%)
         centering — the image stays vertically centered on the page at
         any scale instead of drifting as it shrinks. Fixed width (was
         clamp(200px,22vw,340px)) for the same reason as .story-grid's
         own switch to a fixed width — proportional scaling replaces
         viewport-relative sizing here now. */
      transform: translateY(-50%);
      /* +20% (was 340px). left nudged in from 80px to keep a gap from
         the text column without widening .story-grid's margin-left —
         growing that would enlarge the design width and shrink the
         whole unit via --about-scale, cancelling out the size bump. */
      width: 408px;
      left: 42px;
    }
    .story-lead-figure {
      margin: 0;
    }
    /* Column 1 of 2 now (illustration no longer has its own grid track
       — see .story-grid above; the image-to-text gap lives in that
       rule's own margin-left, not here, so it doesn't eat into this
       column's reclaimed width). */
    .story-text-col { grid-column: 1; }
    .story-photos { grid-column: 2; }
    .story-lead-img {
      display: block;
      width: 100%;
      height: auto;
      border-radius: 14px;
      /* Mirror horizontally so the character faces to the right. */
      transform: scaleX(-1);
    }

    /* Inline (between-columns) variant of the social icons — they float
       as a vertical column between the bio text and the photos. */
    .connect-socials--inline {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 22px;
      padding: 0;
      align-self: stretch;
    }
    .connect-socials--inline .social-btn {
      width: 52px;
      height: 52px;
      animation: connect-social-float 6s ease-in-out infinite;
    }
    .connect-socials--inline .social-btn:nth-child(2) { animation-delay: -2s; }
    .connect-socials--inline .social-btn:nth-child(3) { animation-delay: -4s; }
    @keyframes connect-social-float {
      0%, 100% { transform: translateY(0); }
      50%      { transform: translateY(-6px); }
    }
    @media (prefers-reduced-motion: reduce) {
      .connect-socials--inline .social-btn { animation: none; }
    }

    .story-text {
      font-size: 17.31px;
      line-height: 1.55;
      color: var(--text-dim);
    }

    .story-text p { margin-bottom: 10px; }
    .story-text p:last-child { margin-bottom: 0; }
    /* Greeting opener — larger and bold to anchor the section. */
    .story-text p:first-child,
    .story-hello {
      font-size: 24.44px;
      font-weight: 700;
      color: var(--text);
      margin-top: 1vh;
      margin-bottom: 10px;
      line-height: 1.2;
    }
    /* Bullet list inside the bio. */
    .story-list {
      margin: 2px 0 10px;
      padding-left: 20px;
      list-style: disc;
    }
    .story-list li {
      margin-bottom: 3px;
      color: var(--text-dim);
      line-height: 1.4;
    }
    /* Inline links in the closing paragraph. */
    .story-text a {
      color: var(--cyan);
      text-decoration: none;
      border-bottom: 1px solid color-mix(in srgb, var(--cyan) 45%, transparent);
      transition: border-color 0.25s ease, color 0.25s ease;
    }
    .story-text a:hover {
      color: var(--text);
      border-bottom-color: var(--text);
    }
    /* Experience block — header row (title + download CTA) followed by
       a list of role / company × date rows separated by hairlines. */
    .story-exp-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 18px;
      margin-top: calc(18px + 5vh);
      padding-bottom: 8px;
      border-bottom: 1px solid var(--border);
    }
    .story-exp-title {
      margin: 0;
      font-family: var(--font-mono);
      font-size: 13.44px;
      letter-spacing: 2.2px;
      text-transform: uppercase;
      color: var(--text);
      font-weight: 500;
    }
    .story-exp-list {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    .story-exp-item {
      padding: 9px 0;
      border-bottom: 1px solid var(--border);
    }
    .story-exp-item:last-child { border-bottom: none; }
    .story-exp-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 17px;
    }
    .story-exp-role {
      margin: 0;
      font-family: var(--font-display);
      font-size: 14.26px;
      font-weight: 600;
      color: var(--text);
      line-height: 1.25;
    }
    .story-exp-sub {
      font-weight: 400;
      color: var(--text-dim);
      margin-left: 2px;
    }
    .story-exp-date {
      font-family: var(--font-mono);
      font-size: 12.22px;
      letter-spacing: 1.4px;
      color: var(--text-dim);
      white-space: nowrap;
    }

    /* Download Resume CTA in the about section — borderless text link
       with a download arrow that nudges down on hover. */
    .story-resume-btn {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      padding: 0;
      border: none;
      background: transparent;
      color: var(--text);
      font-family: var(--font-mono);
      font-size: 13.24px;
      letter-spacing: 2px;
      text-transform: uppercase;
      text-decoration: none;
      cursor: pointer;
      transition: color 0.25s ease;
    }
    /* Override the generic `.story-text a { border-bottom }` underline. */
    .story-text a.story-resume-btn { border-bottom: none; }
    .story-resume-btn:hover { color: var(--cyan); }
    .story-resume-btn svg { transition: transform 0.25s ease; }
    .story-resume-btn:hover svg { transform: translateY(2px); }

    /* ─── Resume PDF viewer modal ─────────────────────────
       Fullscreen modal that loads the PDF in an iframe. Header bar
       shows the document title, a download CTA (forces file download
       via the <a download> attribute) and a close button. */
    .resume-modal {
      position: fixed;
      inset: 0;
      z-index: 1100;
      background: rgba(0, 0, 0, 0.65);
      backdrop-filter: blur(14px);
      -webkit-backdrop-filter: blur(14px);
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 40px 24px;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.28s ease;
    }
    .resume-modal.open {
      opacity: 1;
      pointer-events: auto;
    }
    .resume-modal-inner {
      position: relative;
      width: min(960px, 96vw);
      height: min(90vh, 1100px);
      background: var(--bg);
      border: 1px solid var(--border-glow);
      border-radius: 18px;
      box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
      display: flex;
      flex-direction: column;
      overflow: hidden;
      transform: scale(0.96);
      transition: transform 0.36s cubic-bezier(0.22, 1, 0.36, 1);
    }
    .resume-modal.open .resume-modal-inner { transform: scale(1); }
    .resume-modal-bar {
      display: flex;
      align-items: center;
      justify-content: flex-end;
      gap: 16px;
      padding: 14px 18px;
      border-bottom: 1px solid var(--border);
      background: rgba(var(--bg-rgb), 0.92);
    }
    .resume-modal-title {
      flex: 1 1 auto;
      font-family: var(--font-mono);
      font-size: 12px;
      letter-spacing: 1.6px;
      text-transform: uppercase;
      color: var(--text-dim);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .resume-modal-download {
      flex: 0 0 auto;
      display: inline-flex;
      align-items: center;
      gap: 8px;
      padding: 9px 18px;
      border-radius: 100px;
      border: 1.5px solid var(--border-glow);
      background: rgba(var(--bg-rgb), 0.6);
      color: var(--text);
      font-family: var(--font-mono);
      font-size: 12px;
      letter-spacing: 1.8px;
      text-transform: uppercase;
      text-decoration: none;
      cursor: pointer;
      transition: border-color 0.25s ease, color 0.25s ease,
                  background 0.25s ease, transform 0.25s ease;
    }
    .resume-modal-download:hover {
      border-color: var(--cyan);
      color: var(--cyan);
      transform: translateY(-1px);
    }
    .resume-modal-close {
      flex: 0 0 auto;
      width: 36px;
      height: 36px;
      border-radius: 50%;
      border: 1px solid var(--border-glow);
      background: rgba(var(--bg-rgb), 0.6);
      color: var(--text);
      font-size: 22px;
      line-height: 1;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      transition: border-color 0.25s ease, transform 0.25s ease;
    }
    .resume-modal-close:hover {
      border-color: var(--cyan);
      transform: scale(1.06);
    }
    .resume-modal-frame {
      flex: 1 1 auto;
      width: 100%;
      border: none;
      background: #2a2a2a;
    }
    body.resume-modal-open { overflow: hidden; }
    body.resume-modal-open main { overflow: hidden !important; }
    /* While the resume modal is open, swap the custom pixel-arrow for the
       native system cursor so PDF interactions (text-select, scroll,
       links) feel native and don't fight the custom-cursor layer. */
    body.resume-modal-open .site-cursor,
    body.resume-modal-open .site-cursor-label { display: none !important; }
    body.resume-modal-open,
    body.resume-modal-open *,
    body.resume-modal-open .resume-modal,
    body.resume-modal-open .resume-modal * { cursor: auto !important; }
    body.resume-modal-open .resume-modal-download,
    body.resume-modal-open .resume-modal-close { cursor: pointer !important; }
    .story-text strong { color: var(--cyan); font-weight: 400; }
    .story-text em { color: var(--cyan); font-style: normal; font-weight: 400; }

    /* About-section photos — sit to the right of the bio. Two
       polaroid-style photos that overlap in the middle so they read as
       a stacked pair on a pinboard. */
    .story-photos {
      display: flex;
      flex-direction: column;
      gap: 0;
      align-items: center;
      /* Nudge the photo column down so it sits below the bio text,
         shifted up 2% of viewport height per request. */
      margin-top: calc(32px - 2vh);
    }
    .story-photo {
      margin: 0;
      width: 100%;
      max-width: 300px;
      border-radius: 14px;
      overflow: hidden;
      border: 1px solid var(--border);
      box-shadow: 0 12px 28px rgba(0, 40, 70, 0.22);
      position: relative;
      /* Gentle morph on hover — small straighten + lift so the photo
         responds to the cursor landing on it. */
      transition: transform 0.35s cubic-bezier(0.2, 0.7, 0.2, 1),
                  box-shadow 0.35s ease;
    }
    .story-photo:nth-child(odd) {
      transform: translateX(-14px) rotate(-2deg);
      z-index: 2;
    }
    /* Pull the second photo up so its top overlaps the bottom of the
       first by ~20% of the photo's height (a touch less than before so
       the bottom photo sits a bit lower) — the first (top) photo keeps
       the higher z-index, so it's the one covering the other at the
       overlap, not the reverse. */
    .story-photo:nth-child(even) {
      transform: translateX(14px) rotate(2deg);
      margin-top: -72px;
      z-index: 1;
    }
    /* Hover nudge — each photo shifts outward + lifts slightly so the
     visitor sees it react to the cursor without the layout breaking. */
    .story-photo:nth-child(odd):hover {
      transform: translateX(-22px) translateY(-6px) rotate(-4deg);
      box-shadow: 0 20px 36px rgba(0, 40, 70, 0.28);
      z-index: 3;
    }
    .story-photo:nth-child(even):hover {
      transform: translateX(22px) translateY(-6px) rotate(4deg);
      box-shadow: 0 20px 36px rgba(0, 40, 70, 0.28);
      z-index: 3;
    }
    .story-photo img {
      display: block;
      width: 100%;
      height: auto;
      aspect-ratio: 4 / 5;
      object-fit: cover;
    }

    .story-stats {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 16px;
      align-content: start;
    }

    .stat-card {
      padding: 24px;
    }

    .stat-num {
      font-size: 40px;
      font-weight: 700;
      font-family: var(--font-mono);
      background: linear-gradient(135deg, var(--cyan), var(--purple));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      line-height: 1;
      margin-bottom: 8px;
    }

    .stat-label {
      font-size: 12px;
      color: var(--text-dim);
      letter-spacing: 1px;
    }

    .skills-row {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      align-content: flex-start;
    }

    .skill-chip {
      padding: 8px 16px;
      background: var(--glass);
      border: 1px solid var(--border);
      border-radius: 100px;
      font-family: var(--font-mono);
      font-size: 11px;
      letter-spacing: 1px;
      color: var(--text-dim);
      transition: all 0.3s ease;
    }

    .skill-chip:hover {
      border-color: var(--cyan);
      color: var(--cyan);
      background: var(--cyan-dim);
    }

    /* ─── Work Section ──────────────────────────────────── */
    #work {
      padding-top: 100px;
      /* Tighter horizontal padding on the work section so all 4 fixed-width
         pills (300px each) fit on common desktop viewports (≥1280px)
         without overflowing or getting cut off. */
      padding-left: 16px;
      padding-right: 16px;
    }

    .work-grid {
      display: flex;
      flex-direction: row;
      flex-wrap: nowrap;
      align-items: flex-start;
      gap: 64px;
      margin-top: 48px;
      /* No internal scrolling — the work section itself is widened
         on desktop (see #work below) so all 4 fixed-width pills fit
         naturally within the main horizontal scroll. align-items:
         flex-start (instead of the flex default, stretch) is what lets
         the alternating up/down offset below actually show — stretch
         would force every card back to the same height/top. */
      overflow: visible;
      padding-bottom: 8px;
    }

    .project-card {
      /* Sketchbook / doodle look — a cream paper card with a hand-drawn
         wobbly ink border (irregular border-radius) and an offset "pen"
         shadow, so the Work section reads like pages torn from a
         sketchbook. Per-card tilt/stagger + accent colour are set below;
         ink-dark text is fixed (paper doesn't switch with the theme). */
      position: relative;
      flex: 0 0 auto;
      width: 384px;
      min-width: 384px;
      max-width: 384px;
      scroll-snap-align: start;
      padding: 20px 20px 24px;
      /* Soft mint paper — a pale fresh green that reads cheerful against
         the cool blue site background. */
      background: #eef6ec;
      border: 2.5px solid #2b2620;
      /* Irregular radii read as a hand-drawn rounded rectangle. */
      border-radius: 20px 44px 24px 40px / 40px 24px 44px 20px;
      display: flex;
      flex-direction: column;
      text-align: left;
      gap: 12px;
      overflow: visible;
      text-decoration: none;
      color: #2b2620;
      cursor: pointer;
      box-shadow: 5px 6px 0 rgba(43, 38, 32, 0.16);
      transition: transform 0.3s ease, box-shadow 0.3s ease, rotate 0.3s ease;
      /* Gentle up/down bob — individual `translate` so it composes with
         the per-card `rotate` and the hover `transform` lift instead of
         overriding them. Per-card duration/delay staggered below. */
      animation: card-float 5s ease-in-out infinite;
    }
    @keyframes card-float {
      0%, 100% { translate: 0 0; }
      50%      { translate: 0 -7px; }
    }
    @media (prefers-reduced-motion: reduce) {
      .project-card { animation: none; }
    }

    /* Hand-drawn arrow doodle in the bottom-right — a little "go check
       it out" cue that nudges right on hover. */
    .project-card::after {
      content: '';
      position: absolute;
      right: 16px;
      bottom: 14px;
      width: 46px;
      height: 24px;
      pointer-events: none;
      background: center / contain no-repeat
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 56 30'%3E%3Cpath d='M3 17 C16 9 32 23 49 13' stroke='%232b2620' stroke-width='2.6' fill='none' stroke-linecap='round'/%3E%3Cpath d='M40 7 L51 13 L41 21' stroke='%232b2620' stroke-width='2.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
      transition: transform 0.3s ease;
    }
    .project-card:hover::after { transform: translateX(5px); }

    /* Lift and straighten on hover, like lifting a page off the desk —
       the pen shadow deepens/spreads. */
    .project-card:hover {
      transform: translateY(-8px) scale(1.02);
      rotate: 0deg;
      box-shadow: 8px 11px 0 rgba(43, 38, 32, 0.18);
    }
    .project-card.fade-in:hover { transform: translateY(-8px) scale(1.02); rotate: 0deg; }

    /* Web only: a softer, slightly more prominent drop shadow (less harsh
       than the hard pen-offset above), and a springy hover so the card
       gives a little upward "jump" — the overshoot easing pops it past
       the lift point and settles back. Scoped to desktop so the mobile
       swipe deck (which never hovers) keeps its own look. The
       .work-grid-prefixed selectors carry enough specificity to beat the
       .fade-in entrance transition (which otherwise forces a slow 1.1s
       lift) and the .fade-in.visible resting transform. */
    @media (min-width: 901px) {
      .work-grid .project-card {
        box-shadow: 4px 8px 16px -4px rgba(43, 38, 32, 0.30);
      }
      .work-grid .project-card,
      .work-grid .project-card.fade-in {
        transition: transform 0.36s cubic-bezier(0.34, 1.56, 0.64, 1),
                    box-shadow 0.3s ease,
                    opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
                    rotate 0.3s ease;
      }
      .work-grid .project-card:hover,
      .work-grid .project-card.fade-in:hover {
        transform: translateY(-14px) scale(1.03);
        box-shadow: 6px 16px 28px -5px rgba(43, 38, 32, 0.34);
      }
    }

    /* All cards share the cream paper; each just gets its own accent ink
       (used for the ★ + type tag), a slight tilt (alternating so
       neighbours don't lean the same way), and a row up/down stagger —
       needs .work-grid align-items:flex-start to show. */
    .work-grid .project-card:nth-child(1) { --accent: #e0972a; rotate: -2deg; margin-top: 0; animation-duration: 4.6s; animation-delay: -0.4s; }
    .work-grid .project-card:nth-child(2) { --accent: #d95186; rotate: 1.5deg; margin-top: 36px; animation-duration: 5.3s; animation-delay: -2.1s; }
    .work-grid .project-card:nth-child(3) { --accent: #2f92b0; rotate: -1.5deg; margin-top: 0; animation-duration: 4.9s; animation-delay: -3.6s; }
    .work-grid .project-card:nth-child(4) { --accent: #4f9e49; rotate: 2deg; margin-top: 36px; animation-duration: 5.6s; animation-delay: -1.2s; }

    /* Header banner — name (doodle) on the left, a small type badge on
       the right: the "name + HP" row of a collectible card. */
    .tc-top {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      position: relative;
      z-index: 1;
    }
    .tc-badge {
      flex-shrink: 0;
      font-family: var(--font-mono);
      font-size: 13px;
      font-weight: 700;
      letter-spacing: 1.5px;
      padding: 4px 12px;
      /* Hand-drawn little tag — wobbly outline, no fill. */
      border-radius: 12px 6px 12px 6px / 6px 12px 6px 12px;
      background: none;
      color: #2b2620;
      border: 2px solid #2b2620;
      white-space: nowrap;
    }

    /* Art window — the screenshot in an inset frame, like a card's art
       box: a light inner border plus a subtle inset shadow for depth. */
    .project-figure {
      margin: 0;
      width: 100%;
      aspect-ratio: 4 / 3;
      overflow: hidden;
      /* Hand-drawn ink frame (wobbly radii) around the screenshot. */
      border-radius: 14px 8px 16px 10px / 10px 16px 8px 14px;
      border: 2.5px solid #2b2620;
      background: #fff;
      position: relative;
      z-index: 1;
    }
    .project-figure img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      object-position: top center;
      display: block;
      transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
    }
    .project-card:hover .project-figure img {
      transform: scale(1.05);
    }

    /* Handwritten note under the art — a small category/★ line and a
       hand-lettered one-liner (no boxed panel; it's just scribbled on
       the paper). Right padding leaves room for the doodle arrow. */
    .tc-bottom {
      position: relative;
      z-index: 1;
      padding: 0 2px;
      display: flex;
      flex-direction: column;
      gap: 4px;
    }
    .tc-meta {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
    }
    .tc-cat {
      font-family: var(--font-mono);
      font-size: 10px;
      letter-spacing: 1.5px;
      text-transform: uppercase;
      color: rgba(43, 38, 32, 0.55);
    }
    .tc-rarity {
      font-size: 12px;
      letter-spacing: 1px;
      color: var(--accent, #e0a92e);
    }
    .tc-flavor {
      font-family: var(--font-doodle), var(--font-display);
      font-size: 21px;
      line-height: 1.05;
      color: #2b2620;
      padding-right: 52px;
    }

    /* Legacy device-wrapper styles still kept for backwards compat in case
       a card still uses the .project-device structure somewhere. */
    .project-card .project-device {
      order: 1;
      width: 100%;
      margin-bottom: 14px;
      flex-wrap: wrap;
      gap: 16px;
    }
    .project-card .project-info {
      order: 2;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    /* Scale the (wide) Slinghaus laptop down so it fits a half-width card */
    .work-grid .laptop-wrap { width: min(280px, 78%); }
    .work-grid .project-title { font-size: 38px; }

    /* Case-study thumbnail gallery — a mobile-only affordance (built by
       home.js) for jumping straight to a card in the deck. Hidden on
       desktop, where all four cards already show side by side. */
    .deck-gallery { display: none; }

    .project-index {
      font-family: var(--font-mono);
      font-size: 15px;
      color: var(--text-dim);
      letter-spacing: 2.5px;
      margin-bottom: 18px;
    }

    .project-title {
      /* Hand-drawn doodle font, echoing the "Kathlyn Teh" hero, so the
         card name reads as hand-lettered rather than typeset. No bottom
         margin — it sits in the .tc-top header row beside the badge. */
      font-family: var(--font-doodle), var(--font-display);
      font-size: 30px;
      font-weight: 400;
      margin: 0;
      letter-spacing: 0.5px;
      line-height: 1;
      position: relative;
    }
    /* A scribbled underline beneath the name. */
    .project-title::after {
      content: '';
      position: absolute;
      left: 0;
      right: -6px;
      bottom: -8px;
      height: 7px;
      background: center / 100% 100% no-repeat
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 8' preserveAspectRatio='none'%3E%3Cpath d='M2 4 C25 1 45 7 65 3 C85 0 100 6 118 3' stroke='%232b2620' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
    }

    .project-tags {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-bottom: 24px;
    }

    .tag {
      padding: 6px 14px;
      border-radius: 100px;
      font-size: 12px;
      font-family: var(--font-mono);
      letter-spacing: 1px;
      font-weight: 600;
    }

    .tag-cyan { background: var(--cyan-dim); color: var(--cyan); border: 1px solid rgba(0,245,255,0.2); }
    .tag-purple { background: var(--purple-dim); color: #60b8ff; border: 1px solid rgba(0,128,255,0.2); }
    .tag-pink { background: var(--pink-dim); color: #00e8f5; border: 1px solid rgba(0,200,240,0.2); }

    /* Light mode: stronger, more distinct tag colours */
    [data-theme="light"] .tag-cyan {
      background: rgba(0, 107, 128, 0.16);
      color: #004e5e;
      border: 1px solid rgba(0, 107, 128, 0.45);
    }
    [data-theme="light"] .tag-purple {
      background: rgba(0, 75, 176, 0.15);
      color: #002e7a;
      border: 1px solid rgba(0, 75, 176, 0.45);
    }
    [data-theme="light"] .tag-pink {
      background: rgba(168, 20, 98, 0.14);
      color: #7a0d45;
      border: 1px solid rgba(168, 20, 98, 0.45);
    }

    .project-link {
      font-family: var(--font-mono);
      font-size: 14px;
      letter-spacing: 2px;
      text-transform: uppercase;
      color: var(--cyan);
      text-decoration: none;
      display: inline-flex;
      align-items: center;
      gap: 8px;
      transition: gap 0.3s ease;
    }

    .project-link:hover { gap: 14px; }

    /* ─── Device Mockups ────────────────────────────────── */
    .project-device {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 24px;
    }

    /* Laptop Mockup — rendered as a 3D chrome-glass slab by chain.js, the
       same way phones are. The HTML chrome below is kept in the DOM only as
       a texture/label source and is visually hidden. The wrap carries an
       explicit aspect-ratio so the canvas keeps its size once the HTML
       chrome is hidden. */
    .laptop-wrap {
      position: relative;
      width: 440px;
      aspect-ratio: 16/10;
    }

    .laptop-wrap > canvas.device-3d {
      position: absolute;
      inset: -8% -6%;
      width: 112%;
      height: 116%;
      z-index: 0;
      pointer-events: none;
    }

    /* Visually hidden (texture source for chain.js), but stays in the DOM. */
    .laptop-screen {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip: rect(0 0 0 0);
      clip-path: inset(50%);
      opacity: 0;
      pointer-events: none;
      margin: 0;
    }

    .laptop-base,
    .laptop-stand { display: none; }

    .laptop-screen::before {
      content: '';
      position: absolute;
      top: 0; left: 0; right: 0;
      height: 20px;
      background: rgba(255,255,255,0.03);
      border-bottom: 1px solid rgba(255,255,255,0.06);
      z-index: 2;
    }

    .laptop-screen::after {
      content: '';
      position: absolute;
      top: 8px;
      left: 50%;
      transform: translateX(-50%);
      width: 5px;
      height: 5px;
      border-radius: 50%;
      background: rgba(255,255,255,0.15);
      z-index: 3;
    }

    .laptop-screen-inner {
      width: 100%;
      height: 100%;
      position: relative;
      overflow: hidden;
    }

    .laptop-screen-inner iframe {
      width: 100%;
      height: 100%;
      border: none;
      display: block;
    }

    .laptop-screen-inner img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      object-position: top center;
      display: block;
    }

    .screen-placeholder {
      width: 100%;
      height: 100%;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 12px;
      background: linear-gradient(135deg, #0a0a1e 0%, #0d0020 100%);
    }

    .screen-placeholder .ph-icon {
      font-size: 32px;
      opacity: 0.3;
    }

    .screen-placeholder .ph-text {
      font-family: var(--font-mono);
      font-size: 11px;
      line-height: 1.6;
      letter-spacing: 2px;
      color: rgba(200, 230, 255, 0.7);
      text-align: center;
      padding: 0 16px;
    }
    .phone-screen .screen-placeholder .ph-text { font-size: 10px !important; }

    /* Phone Mockup */
    .phone-wrap {
      position: relative;
      width: 110px;
      aspect-ratio: 9/19.5;
    }

    /* 3D chrome-glass phone frame, rendered by chain.js using the same
       shader as the chains. Canvas extends just a bit past the wrap so
       the bevel/silhouette isn't clipped — kept tight to avoid bleeding
       into adjacent cards. */
    .phone-wrap > canvas.device-3d {
      position: absolute;
      inset: -10% -14%;
      width: 128%;
      height: 120%;
      z-index: 0;
      pointer-events: none;
    }

    /* The screen content is now rendered as a textured plane *inside* the
       WebGL scene (chain.js setupDeviceFrame), parented to the phone mesh
       so it rotates with the chassis. This HTML overlay is kept in the
       DOM (so chain.js can read its img src / placeholder text and so
       screen-readers still get the label) but visually hidden. */
    .phone-body {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip: rect(0 0 0 0);
      clip-path: inset(50%);
      opacity: 0;
      pointer-events: none;
    }

    .phone-notch { display: none; }

    .phone-screen {
      width: 100%;
      height: 100%;
      position: relative;
    }

    .phone-screen iframe {
      position: absolute;
      top: 0;
      left: 0;
      width: 390px;
      height: 844px;
      border: none;
      transform-origin: top left;
      transform: scale(0.334);
      pointer-events: none;
    }

    .phone-screen-img {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
      object-position: top center;
      display: block;
      pointer-events: none;
    }

    .laptop-screen-inner iframe {
      position: absolute;
      top: 0;
      left: 0;
      width: 1440px;
      height: 900px;
      border: none;
      transform-origin: top left;
      transform: scale(0.222);
      pointer-events: none;
    }


    .phone-screen .screen-placeholder .ph-icon { font-size: 20px; }
    .phone-screen .screen-placeholder .ph-text { font-size: 8px; }

    /* ─── Mid-page chain backdrop ──────────────────────────
       Wraps Story → Work → Connect. The canvas sits absolutely behind
       the section content; section children are promoted above it with
       z-index. Animation paused off-screen by the IntersectionObserver
       inside setupChain (chain.js). */
    #work, #story, #connect { position: relative; z-index: 1; }
    #work > *, #story > *, #connect > * { position: relative; z-index: 1; }

    /* ─── Connect Section ───────────────────────────────── */
    #connect {
      padding-top: 100px;
      /* Trim the bottom gap so the flower field sits up close to the
         two About photos rather than after a tall blank stretch. */
      padding-bottom: 24px;
    }

    /* ─── Simplified socials row ─────────────────────────── */
    .connect-socials {
      display: flex;
      justify-content: center;
      align-items: center;
      gap: 36px;
      padding: 64px 0 48px;
    }

    .social-btn {
      position: relative;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: 0 0 auto;
      width: 78px;
      height: 78px;
      border-radius: 20px;
      color: var(--text-dim);
      text-decoration: none;
      transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
                  color 0.3s ease, border-color 0.3s ease, background 0.3s ease;
    }
    .social-btn svg { width: 32px; height: 32px; }
    .social-btn:hover {
      color: var(--cyan);
      transform: translateY(-8px);
    }

    /* Hover tooltip (data-tip) */
    .social-btn::after {
      content: attr(data-tip);
      position: absolute;
      bottom: calc(100% + 14px);
      left: 50%;
      transform: translate(-50%, 6px) scale(0.85);
      background: #16242e;
      color: #f1fbff;
      font-family: var(--font-mono);
      font-size: 13px;
      font-weight: 600;
      letter-spacing: 1px;
      padding: 7px 14px;
      border-radius: 100px;
      white-space: nowrap;
      opacity: 0;
      pointer-events: none;
      box-shadow: 0 10px 24px rgba(0, 40, 70, 0.25);
      transition: opacity 0.2s ease,
                  transform 0.28s cubic-bezier(0.2, 1.5, 0.4, 1);
    }
    .social-btn::before {
      content: '';
      position: absolute;
      bottom: calc(100% + 8px);
      left: 50%;
      transform: translateX(-50%);
      border: 6px solid transparent;
      border-top-color: #16242e;
      opacity: 0;
      transition: opacity 0.2s ease;
    }
    .social-btn:hover::after { opacity: 1; transform: translate(-50%, 0) scale(1); }
    .social-btn:hover::before { opacity: 1; }

    .connect-inner {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 48px;
      margin-top: 48px;
      align-items: start;
    }

    .connect-text {
      font-size: 16px;
      color: var(--text-dim);
      line-height: 1.8;
      margin-bottom: 32px;
    }

    .connect-links {
      display: flex;
      flex-direction: column;
      gap: 12px;
    }

    .contact-link {
      display: flex;
      align-items: center;
      gap: 16px;
      padding: 18px 24px;
      border-radius: 12px;
      text-decoration: none;
      color: var(--text);
      transition: all 0.3s ease;
    }

    .contact-link:hover {
      background: var(--glass-hover);
      border-color: var(--border-glow);
      transform: translateX(6px);
    }

    .contact-icon {
      width: 36px;
      height: 36px;
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      color: var(--text);
    }

    .contact-icon svg {
      width: 24px;
      height: 24px;
      display: block;
    }

    .contact-label {
      font-size: 12px;
      color: var(--text-dim);
      font-family: var(--font-mono);
      letter-spacing: 1px;
    }

    .contact-value {
      font-size: 14px;
      font-weight: 500;
    }

    .avail-badge {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      padding: 8px 16px;
      background: rgba(0,245,255,0.06);
      border: 1px solid rgba(0,245,255,0.15);
      border-radius: 100px;
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--cyan);
      letter-spacing: 1px;
      margin-bottom: 28px;
    }

    .avail-dot {
      width: 7px;
      height: 7px;
      border-radius: 50%;
      background: var(--cyan);
      animation: blink 2s ease-in-out infinite;
    }

    @keyframes blink {
      0%, 100% { opacity: 1; box-shadow: 0 0 6px var(--cyan); }
      50% { opacity: 0.3; box-shadow: none; }
    }

    .form-card {
      padding: 36px;
      border-radius: 20px;
    }

    .form-group {
      margin-bottom: 20px;
    }

    .form-group label {
      display: block;
      font-family: var(--font-mono);
      font-size: 10px;
      letter-spacing: 2px;
      text-transform: uppercase;
      color: var(--text-dim);
      margin-bottom: 8px;
    }

    .form-group input,
    .form-group textarea {
      width: 100%;
      background: var(--input-bg);
      border: 1px solid var(--border);
      border-radius: 10px;
      padding: 14px 16px;
      color: var(--text);
      font-family: var(--font-display);
      font-size: 14px;
      outline: none;
      transition: border-color 0.3s ease;
      resize: none;
    }

    .form-group input:focus,
    .form-group textarea:focus {
      border-color: rgba(0,245,255,0.4);
      background: rgba(0,245,255,0.02);
    }

    .form-group input::placeholder,
    .form-group textarea::placeholder { color: rgba(200,205,255,0.25); }

    /* ─── Footer ─────────────────────────────────────────── */
    footer {
      margin-left: 0;
      margin-right: 0;
      padding: 32px 60px;
      border-top: 1px solid var(--border);
      display: flex;
      align-items: center;
      justify-content: space-between;
      position: relative;
      z-index: 1;
    }

    .footer-copy {
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--text-dim);
      letter-spacing: 1px;
    }

    .footer-mark {
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--text-dim);
      letter-spacing: 1px;
    }

    .footer-mark span { color: var(--cyan); }

    /* ─── Divider glow ──────────────────────────────────── */

    /* ─── Corner deco ────────────────────────────────────── */
    .corner-deco {
      position: fixed;
      pointer-events: none;
      z-index: 50;
    }

    .corner-deco.tl {
      top: 20px;
      right: 20px;
      width: 40px;
      height: 40px;
      border-top: 1px solid rgba(0,245,255,0.3);
      border-right: 1px solid rgba(0,245,255,0.3);
    }

    .corner-deco.br {
      bottom: 20px;
      right: 20px;
      width: 40px;
      height: 40px;
      border-bottom: 1px solid rgba(0,128,255,0.3);
      border-left: 1px solid rgba(0,128,255,0.3);
    }

    /* ─── Mobile Responsive ─────────────────────────────── */
    @media (max-width: 900px) {
      :root { --nav-w: 0px; }

      /* Trim the empty gap above the hero on mobile + tablet (the base
         10vh pushed it down too far); the <=600px rule tightens it a
         touch more for phones. */
      .hero-name-block { margin-top: 96px; }
      /* Cut the greeting's big inline translateY(7vh) down so it doesn't
         re-add a chunk of top space on tablets (phones override this
         again below). */
      .hero-greeting { transform: translateY(2vh) !important; }
      /* Scale the big name up on tablet — the base desktop rule left it
         at ~10.8vw here, small and stranded in whitespace. Phones use
         their own 23vw below. */
      .hero-name { font-size: 16vw; line-height: 1.02; }

      /* Frosted-glass strip at the top of mobile viewports — blurs whatever
         page content scrolls behind the nav-bar pill, fading out to fully
         transparent just below the pill so the rest of the page reads
         crisply. The element sits below #sidenav (z-index 100) but above
         all in-flow page content. */
      body::before {
        content: '';
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        /* Fade band behind the nav pill. Trimmed back so it doesn't reach
           so far down that it washes out the hero greeting — the hero
           sits just below it (see .hero-name-block margin-top). The
           safe-area inset extends the band up under the iPhone notch
           (viewport-fit=cover) without moving its lower, page-facing
           edge. */
        height: calc(84px + env(safe-area-inset-top, 0px));
        z-index: 90;
        pointer-events: none;
        backdrop-filter: blur(14px);
        -webkit-backdrop-filter: blur(14px);
        background: linear-gradient(
          to bottom,
          rgba(var(--bg-rgb), 0.45) 0%,
          rgba(var(--bg-rgb), 0.22) 55%,
          rgba(var(--bg-rgb), 0)    100%
        );
        -webkit-mask-image: linear-gradient(
          to bottom, #000 0%, #000 30%, rgba(0,0,0,0) 100%
        );
                mask-image: linear-gradient(
          to bottom, #000 0%, #000 30%, rgba(0,0,0,0) 100%
        );
      }

      /* Mirror frosted-glass strip at the bottom — flush with the
         bottom edge of the screen (was floated 50px up, which made the
         blur band slice through photos mid-screen). z-index 1 with
         body::after painting last among z1 elements means it blurs
         page content scrolling into the bottom band, while the ground
         (ASCII flower) strip and the chicks — both raised to z2 — stay
         crisp IN FRONT of the blur, so it reads as sitting behind the
         flower field. */
      body::after {
        content: '';
        position: fixed;
        /* Overhang the bottom edge slightly (was 0) so the blur reaches
           past the very bottom of the screen — otherwise a thin sliver
           of unblurred content peeked through below the strip. */
        bottom: -16px;
        left: 0;
        right: 0;
        /* Taller band + a fade that starts higher up (was 55%) so the
           blur eases out over a much longer stretch — a more gradual
           gradient rather than a hard-edged strip. +16px to compensate
           for the negative bottom above so the faded top edge stays put;
           the safe-area inset likewise extends the band down past the
           iPhone home indicator (viewport-fit=cover) without moving its
           upper, page-facing edge. */
        height: calc(186px + env(safe-area-inset-bottom, 0px));
        z-index: 1;
        pointer-events: none;
        backdrop-filter: blur(14px);
        -webkit-backdrop-filter: blur(14px);
        background: linear-gradient(
          to top,
          rgba(var(--bg-rgb), 0.45) 0%,
          rgba(var(--bg-rgb), 0.22) 55%,
          rgba(var(--bg-rgb), 0)    100%
        );
        -webkit-mask-image: linear-gradient(
          to top, #000 0%, #000 30%, rgba(0,0,0,0) 100%
        );
                mask-image: linear-gradient(
          to top, #000 0%, #000 30%, rgba(0,0,0,0) 100%
        );
      }

      /* Above the bottom frosted strip (z1) so the coded flowers stay
         crisp in front of the blur, matching the chicks (also z2). */
      .ground { z-index: 2; }

      /* Mobile uses the same slanted-card pill nav as desktop — pill in the
         top-right with a tilted card peeking out behind it. Tapping the pill
         expands the back card into a vertical dropdown (driven by .open). */
      #sidenav {
        position: fixed;
        top: calc(14px + env(safe-area-inset-top, 0px));
        right: 16px;
        left: auto;
        width: auto;
        height: auto;
        padding: 0;
        gap: 0;
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        background: none;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        -webkit-mask-image: none;
                mask-image: none;
        overflow: visible;
        isolation: isolate;
        z-index: 100;
        /* Hidden on the homepage (hero); appears together with the
           floating home icon once the hero has scrolled past
           (nav-visible toggled in home.js's updateHomeIcon). */
        transition: opacity 0.32s ease, transform 0.32s ease;
        opacity: 0;
        transform: translateY(-10px);
        pointer-events: none;
      }
      #sidenav.nav-visible {
        opacity: 1;
        transform: none;
        pointer-events: auto;
      }

      /* Slanted card behind the pill (mobile variant). */
      #sidenav::before {
        content: '';
        display: block;
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        height: 100%;
        margin-top: 18px;
        background: var(--bg2);
        border: 3px solid var(--border-glow);
        border-radius: 16px;
        transform-origin: top center;
        transform: rotate(-4deg);
        z-index: 0;
        pointer-events: none;
        opacity: 1;
        transition:
          height     0.42s cubic-bezier(0.22, 1, 0.36, 1),
          margin-top 0.42s cubic-bezier(0.22, 1, 0.36, 1),
          transform  0.42s cubic-bezier(0.22, 1, 0.36, 1);
        will-change: height, margin-top, transform;
      }

      /* Pill button. Solid background covers the slanted card's outline
         within the pill's footprint (painting order: ::before, then button). */
      .current-page {
        position: relative;
        z-index: 1;
        isolation: isolate;
        display: inline-flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        font-family: var(--font-mono);
        font-size: 18px;
        letter-spacing: 2.2px;
        text-transform: uppercase;
        color: var(--cyan);
        padding: 14px 26px;
        background: var(--bg);
        border: 3px solid var(--border-glow);
        border-radius: 16px;
        cursor: pointer;
        transition: border-color 0.3s ease, background 0.3s ease, transform 0.3s ease, opacity 0.3s ease;
      }
      .current-page::after { content: none; }

      /* Keep the pill fully opaque on hover so the slanted card behind
         doesn't bleed through. (The base rule sets opacity: 0.75 on hover,
         which made the slanted outline visible through the pill.) */
      .current-page:hover { opacity: 1; }

      /* Open state — slanted card un-tilts, slides down, and grows to hold
         the menu. Pill highlights to cyan and the chevron flips. A
         min-width guarantees the card fits the longest label
         (PLAYGROUND) instead of clipping it, and the pill stretches to
         the card's full width so the two stay edge-aligned (keeping the
         pill-label ↔ menu-label left alignment). */
      #sidenav.open { min-width: 236px; }
      #sidenav.open .current-page {
        align-self: stretch;
        opacity: 1;
        border-color: var(--cyan);
        transform: translate(0, -2px);
      }
      #sidenav.open::before {
        /* Extend the card up to the top of the pill so there's no visible
           gap between pill and menu. The pill (solid bg, z-index 1) sits
           on top of the card; the +60px offset covers the y-distance from
           the pill's top edge down to where .nav-items starts. */
        height: calc(var(--nav-open-height, 190px) + 60px);
        margin-top: 0;
        transform: rotate(0deg);
      }
      #sidenav.open .current-page-arrow { transform: rotate(180deg); }

      /* Vertical dropdown menu inside the expanded card. */
      .nav-items {
        display: flex;
        position: absolute;
        top: 60px;
        right: 0;
        left: 0;
        flex-direction: column;
        align-items: stretch;
        gap: 2px;
        min-width: 0;
        /* Left inset (23 + the item's own 6 = 29) matches the pill
           label's inset (3px border + 26px padding) so the dropdown
           labels line up exactly under the pill label. */
        padding: 14px 23px;
        background: transparent;
        border: none;
        border-radius: 0;
        box-shadow: none;
        opacity: 0;
        z-index: 5;
        clip-path: inset(0 0 100% 0);
        pointer-events: none;
        transition: clip-path 0.42s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.26s ease;
      }
      .nav-items::before {
        content: '';
        position: absolute;
        top: -12px;
        left: 0;
        right: 0;
        height: 14px;
      }
      #sidenav.open .nav-items {
        opacity: 1;
        clip-path: inset(0 0 0 0);
        pointer-events: auto;
        transition-delay: 0.05s;
      }

      .nav-item {
        display: flex;
        align-items: center;
        gap: 0;
        padding: 10px 6px;
        border-radius: 6px;
        border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
        color: var(--text-dim);
        text-decoration: none;
        transition: color 0.25s ease, background 0.25s ease;
      }
      .nav-item:last-child { border-bottom: none; }
      .nav-item .nav-label {
        font-size: 15px;
        letter-spacing: 2.2px;
        opacity: 0.78;
        transition: color 0.25s ease, opacity 0.25s ease;
      }
      .nav-item.active .nav-label { color: var(--cyan); opacity: 1; }
      .nav-item .nav-dot,
      .nav-item .nav-number { display: none; }
      .nav-track, .nav-indicator { display: none; }

      /* The legacy hamburger dropdown is replaced by the slanted-card menu. */
      .hamburger-dropdown { display: none !important; }

      .theme-toggle {
        top: auto;
        bottom: calc(18px + env(safe-area-inset-bottom, 0px));
        right: 18px;
      }
      /* Keep the music toggle pinned to the left of the theme toggle on mobile. */
      .music-toggle {
        bottom: calc(18px + env(safe-area-inset-bottom, 0px));
        right: calc(18px + 76px + 10px);
      }

      .mobile-nav {
        display: none !important;
      }

      main, footer {
        margin-left: 0;
        margin-right: 0;
      }

      section {
        padding: 80px 24px;
      }

      /* Close up the dead space between Work and About: Work no longer
         holds a full-viewport min-height (its deck + gallery define its
         height), and both facing paddings are trimmed. */
      #work {
        min-height: auto;
        padding-bottom: 28px;
      }
      #connect {
        padding-top: 32px;
      }

      footer {
        padding: 24px 24px 90px;
        flex-direction: column;
        gap: 12px;
        text-align: center;
      }

      /* Undoes the desktop proportional-scale setup entirely — mobile
         stacks normally and reflows like everything else, rather than
         shrinking as one fixed-width unit. */
      .story-grid {
        grid-template-columns: 1fr;
        margin-left: 0;
        width: auto;
        transform: none;
      }
      /* Desktop pins these into a 2-column grid (text | photos) and
         gives text-col extra left margin to clear the illustration —
         on mobile's single stacked column, grid-column:2 would instead
         create an implicit second column and put photos beside the
         text rather than below it. */
      .story-text-col, .story-photos { grid-column: 1; }
      .story-text-col { margin-left: 0; }
      /* Undo the desktop centering setup entirely — mobile sections
         scroll normally (no fixed 100vh to center against), so the
         wrapper just needs to sit back in normal flow as a plain block
         above the stacked grid. Full grid width would make the image
         dominate the stacked layout — cap it and center it instead. */
      .story-lead-wrap { position: static; width: auto; transform: none; }
      .story-scale-unit { height: auto; transform: none; }
      .story-lead-figure { max-width: 224px; margin: 0 auto; }
      /* Un-mirror the chrome-kat on mobile (desktop keeps the scaleX(-1)
         flip so it faces the text column). */
      .story-lead-img { transform: none; }

      /* Mobile: two bigger photos, tilted opposite ways and slightly
         overlapped/staggered so they read as a scattered pair —
         borderless with rounded corners (no white polaroid frame). */
      .story-photos {
        flex-direction: row;
        justify-content: center;
        align-items: flex-start;
        gap: 0;
        margin-top: 30px;
      }
      .story-photo {
        /* Viewport-relative so the pair scales up to fill the phone
           screen nicely instead of sitting small with wide margins. */
        max-width: min(300px, 45vw);
        background: none;
        padding: 0;
        border: none;
        border-radius: 14px;
        overflow: hidden;
        box-shadow: 0 14px 30px rgba(0, 40, 70, 0.26);
      }
      .story-photo:nth-child(odd) {
        transform: rotate(-5deg);
        z-index: 2;
      }
      .story-photo:nth-child(even) {
        transform: rotate(4deg);
        margin-left: -22px;
        margin-top: 20px;
        z-index: 1;
      }
      .story-photo:nth-child(odd):hover,
      .story-photo:nth-child(even):hover {
        transform: translateY(-5px) rotate(0deg);
        z-index: 3;
      }

      .story-stats {
        grid-template-columns: repeat(2, 1fr);
      }

      /* No blue ambient glow in the flower field on mobile — remove the
         #guestbook::before gradient so the field reads clean (the
         desktop site-wide glow lives on body::after instead). */
      #guestbook::before { display: none; }

      /* Pull the farewell note / duck up closer to the flower field.
         The real gap was the outro section keeping the base
         min-height:100vh and centering its content (justify-content:
         center), so the duck floated in the middle of a full screen
         below the field. Letting it shrink to its content — plus
         trimming the paddings between the two stacked sections — seats
         the duck right under the flower field. */
      #guestbook { padding-top: 20px; padding-bottom: 16px; }
      #outro {
        padding-top: 8px;
        min-height: auto;
        justify-content: flex-start;
      }

      /* ── Overlapping case-study card deck (mobile) ──
         The four cards sit stacked on top of one another like a physical
         deck: the front card is fully shown, the ones behind peek out
         below it. Swipe the front card left/right to fling it away and
         reveal the next; a plain tap opens that case study. The stacking
         transforms, z-index and the container height are all driven by
         JS (see home.js). Vertical drags fall through to page scroll. */
      .work-grid {
        position: relative;
        flex-direction: column;
        align-items: center;
        gap: 0;
        margin-top: 40px;
        overflow: visible;
        touch-action: pan-y;
        /* height is set inline by JS to fit the tallest card + the peek */
      }
      .work-grid .project-card {
        position: absolute;
        left: 50%;
        top: 0;
        margin-top: 0;
        width: min(384px, 92vw);
        min-width: 0;
        max-width: 92vw;
        /* Everything is driven through the inline `transform`; drop the
           desktop bob + per-card tilt (individual translate/rotate) so
           they don't compound with the deck maths. */
        animation: none;
        translate: none;
        rotate: none;
        transform: translate(-50%, 0);
        opacity: 1;
        transition: transform 0.42s cubic-bezier(0.22, 0.61, 0.36, 1),
                    opacity 0.32s ease;
        backface-visibility: hidden;
        will-change: transform, opacity;
        touch-action: pan-y;
        -webkit-user-select: none;
        user-select: none;
      }
      /* The screenshot is where fingers naturally land when swiping —
         make sure it can't be image-dragged / long-press-selected and
         inherits the same gesture policy as the card itself. */
      .work-grid .project-card .project-figure,
      .work-grid .project-card img {
        touch-action: pan-y;
        -webkit-user-drag: none;
        -webkit-user-select: none;
        user-select: none;
        -webkit-touch-callout: none;
      }
      /* Neutralise the scroll fade-in on mobile — the deck controls its
         own visibility, and the fade-in's opacity:0 would hide cards. */
      .work-grid .project-card.fade-in,
      .work-grid .project-card.fade-in.visible {
        opacity: 1;
      }

      /* Thumbnail gallery under the deck. */
      .deck-gallery {
        display: flex;
        justify-content: center;
        flex-wrap: wrap;
        gap: 12px;
        margin-top: 26px;
        padding: 0 4px;
      }
      .deck-thumb {
        width: 64px;
        height: 64px;
        padding: 0;
        border: 2.5px solid #2b2620;
        border-radius: 12px 8px 14px 9px / 9px 14px 8px 12px;
        background: #eef6ec;
        overflow: hidden;
        cursor: pointer;
        opacity: 0.5;
        /* Inactive thumbnails sit smaller so the active one clearly
           stands out; .is-active scales back up below. */
        transform: scale(0.8);
        box-shadow: 2px 2px 0 rgba(43, 38, 32, 0.16);
        transition: opacity 0.25s ease, transform 0.25s ease;
      }
      .deck-thumb img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        object-position: top center;
        display: block;
      }
      .deck-thumb.is-active {
        opacity: 1;
        transform: translateY(-3px) scale(1.07);
      }

      .connect-inner {
        grid-template-columns: 1fr;
      }

      .laptop-wrap { width: 100%; max-width: 420px; }
      .project-device { flex-direction: column; }

      /* Cards that pair a laptop with a phone (Slinghaus) keep the two
         devices side by side instead of stacking, scaling down together so
         they still fit on narrow screens. */
      .project-device:has(.laptop-wrap) {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        justify-content: center;
        gap: 16px;
      }
      .project-device:has(.laptop-wrap) .laptop-wrap {
        width: min(420px, 58%);
        max-width: none;
      }
    }

    /* ─── Hamburger button ──────────────────────── */
    .hamburger {
      display: none;
      flex-direction: column;
      gap: 5px;
      background: none;
      border: none;
      cursor: pointer;
      padding: 6px;
      margin-left: 10px;
      z-index: 200;
    }

    /* Tablet: name on the left, starter pack to its right, tagline full
       width below both. Phones (<=600px) keep the fully stacked layout. */
    @media (min-width: 601px) and (max-width: 900px) {
      .hero-name-row {
        display: grid;
        grid-template-columns: 1fr auto;
        grid-template-areas:
          "name  pack"
          "roles roles";
        align-items: center;
        column-gap: clamp(18px, 3vw, 40px);
        row-gap: clamp(14px, 2.5vw, 28px);
      }
      /* Slightly smaller + no-wrap so "Kathlyn" stays on one line beside
         the picture rather than breaking the "n" onto its own line. */
      .hero-name { font-size: 15vw; }
      .hero-name .first,
      .hero-name .last { white-space: nowrap; }
      .hero-name-outer { grid-area: name; }
      .hero-roles { grid-area: roles; margin-top: 0; }
      .hero-starter-pack {
        grid-area: pack;
        width: min(44vw, 320px);
        justify-self: end;
        margin-top: -2vh;   /* nudge the picture a touch higher */
      }
    }

    @media (max-width: 600px) {
      /* Phone: size the name off viewport width so "Kathlyn" spans
         nearly the full screen, and scale the greeting proportionally
         off the same unit so the two stay in harmony as the screen
         width changes rather than being fixed px. */
      /* 28vw (was 32vw) — shrunk slightly so the greeting (now sitting
         below the top blur strip), the name, and the starter pack all
         fit on one screen even on the smallest phones. */
      .hero-name { font-size: 23vw; line-height: 1.02; }
      /* Fluid — scales with the phone's width (bounded so it stays
         readable on the smallest screens and doesn't balloon on tablets). */
      .hero-role-line { font-size: clamp(16px, 4.4vw, 24px); }
      .hero-role-tags li { font-size: 22px; }
      /* Nudge "Teh" up 5% of viewport height, tightening it toward
         "Kathlyn" (transform on the block wrapper — independent of the
         per-letter tilt animation on the inner .letter spans). */
      .hero-name .last { transform: translateY(-5vh); }
      /* Greeting scaled off vw too (was fixed 26px) so it grows with
         the name; letter-spacing dialled back a touch so the longer
         "HELLO, MY NAME IS" line still fits above the name. Overrides
         the inline translateY(10vh) + margin (which on desktop float
         it well above the name) so on phone it tucks right on top of
         "Kathlyn Teh" instead. */
      .hero-greeting {
        font-size: 10vw !important;
        letter-spacing: 1px;
        /* Nudged down 2% of viewport height (overrides the inline
           translateY(10vh) that floats it far up on desktop), plus a
           small shift right to sit under the nudged-right name. */
        transform: translateX(5vw) translateY(2vh) !important;
        margin-bottom: 6px !important;
      }
      /* Kill the desktop 5vh drop so the name sits snug under the
         greeting rather than a third of a screen below it. Plus a small
         shift right (transform, so it survives the margin-left:0 reset)
         to match the greeting's nudge above. */
      .hero-name-outer { margin-top: 0 !important; transform: translateX(5vw); }
      /* Line the tags up under the nudged-right name. */
      .hero-roles { transform: translateX(5vw); }
      /* Phone stacking order: name, then the starter pack, then the
         tagline — so the picture sits directly under "Kathlyn Teh"
         instead of being pushed to the bottom of the hero. The row is a
         flex column here, so `order` is enough; the 601-900 band lays the
         same three out on a grid, where order wouldn't apply anyway. */
      .hero-starter-pack { order: 1; }
      .hero-roles { order: 2; }
      /* Tighten the hero stack. The pack art carries its own transparent
         margin, so the measured gaps read smaller than they look — hence
         pulling it in with negative margins on both sides rather than
         just shrinking the row gap. */
      .hero-name-row { gap: 2px; }
      /* The pack art is a 2480x3508 PNG carrying ~668 transparent rows
         above the artwork, so the box gap and the gap you actually SEE
         differ hugely: with the boxes 2px apart the visible gap was still
         ~113px. Pull the box up so only ~20px of real space remains (an
         ~80% reduction); the overlap lands inside the image's own empty
         padding and never touches the "Teh" glyphs.

         The pull has to SCALE, not be a fixed px: that padding is
         0.269 x the image width, and the image is min(66vw, 330px) —
         so it's 69px at 390px wide but only 54px at 320px. A fixed pull
         tuned for a 390 phone left just 1px of gap on a 320 one. */
      .hero-starter-pack {
        margin-top: calc(-1 * (min(17.8vw, 89px) + 26px));
        margin-bottom: -8px;
      }
      .hero-roles { margin-top: 0; }
      #hero { padding: 0 20px 6vh 24px; align-items: flex-start; }
      #hero > div, .hero-name-outer, .hero-desc-row { margin-left: 0 !important; text-align: left; }
      .hero-name-row { gap: 10px; }
      .hero-grid-deco { display: none; }
      .story-stats { grid-template-columns: 1fr; }
      .hero-cta { flex-wrap: nowrap; gap: 12px; margin-left: 0; }
      .btn { padding: 12px 22px; font-size: 11px; letter-spacing: 1.5px; }
      .current-page-arrow { width: 14px; height: 14px; }
      .hero-corner.bl {
        position: fixed;
        bottom: calc(26px + env(safe-area-inset-bottom, 0px));
        left: 24px;
        right: auto;
        display: inline-flex;
        align-items: center;
        z-index: 100;
        transition: opacity 0.3s ease;
      }
      /* Shift the hero down just enough that the light-blue "HELLO, MY
         NAME IS" greeting clears the fixed top frosted-blur strip (a vh
         value wouldn't reliably clear a px-fixed strip on shorter
         phones, so this is a fixed px). */
      .hero-name-block { margin-top: 88px; }
      .hero-desc-cta { margin-top: 33vh !important; }
      .hero-desc-row { gap: 14px; }
    }
    .hamburger span {
      display: block;
      width: 22px;
      height: 2px;
      background: var(--cyan);
      border-radius: 2px;
      transition: all 0.3s ease;
    }
    .hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
    .hamburger.open span:nth-child(2) { opacity: 0; }
    .hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

    /* ─── Hamburger dropdown ────────────────────── */
    .hamburger-dropdown {
      display: flex;
      flex-direction: column;
      position: fixed;
      top: 64px;
      right: 16px;
      background: rgba(var(--bg-rgb), 0.92);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
      border: none;
      border-radius: 18px;
      padding: 16px 0;
      z-index: 200;
      min-width: 168px;
      box-shadow: none;
      opacity: 0;
      transform: translateY(-12px);
      pointer-events: none;
      transition: opacity 0.32s ease, transform 0.32s cubic-bezier(0.22, 1, 0.36, 1);
      -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,0) 0%, #000 18%, #000 82%, rgba(0,0,0,0) 100%);
              mask-image: linear-gradient(to bottom, rgba(0,0,0,0) 0%, #000 18%, #000 82%, rgba(0,0,0,0) 100%);
    }
    .hamburger-dropdown.open {
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }
    .hamburger-dropdown a {
      font-family: var(--font-mono);
      font-size: 14px;
      letter-spacing: 2.5px;
      text-transform: uppercase;
      color: var(--text-dim);
      text-decoration: none;
      padding: 12px 20px;
      margin: 2px 8px;
      border: none;
      border-radius: 0;
      background: radial-gradient(ellipse 70% 100% at center, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 100%);
      transition: color 0.3s ease, background 0.4s ease;
    }
    @media (hover: hover) {
      .hamburger-dropdown a:hover {
        color: var(--cyan);
        background: radial-gradient(ellipse 70% 100% at center, rgba(0, 107, 128, 0.22) 0%, rgba(0, 107, 128, 0) 75%);
      }
    }
    .hamburger-dropdown a:focus { outline: none; }

    /* Mobile Nav */
    .mobile-nav {
      display: none;
      position: fixed;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      background: rgba(10, 0, 24, 0.8);
      backdrop-filter: blur(20px);
      border: 1px solid var(--border);
      border-radius: 100px;
      padding: 10px 20px;
      gap: 20px;
      z-index: 100;
    }

    .mobile-nav a {
      font-family: var(--font-mono);
      font-size: 9px;
      letter-spacing: 1px;
      text-transform: uppercase;
      color: var(--text-dim);
      text-decoration: none;
      padding: 6px 8px;
      border-radius: 100px;
      transition: all 0.3s ease;
    }

    .mobile-nav a.active {
      color: var(--cyan);
      background: rgba(0,245,255,0.1);
    }


    /* ─── Scroll fade-in ─────────────────────────────────── */
    .fade-in {
      opacity: 0;
      transform: translateY(24px);
      transition: opacity 0.7s ease, transform 0.7s ease;
    }

    .fade-in.visible {
      opacity: 1;
      transform: translateY(0);
    }

    /* Smooth slide-up for project cards — re-triggers on every scroll */
    .project-card.fade-in {
      opacity: 0;
      transform: translateY(60px) scale(0.97);
      transition: opacity 1.1s cubic-bezier(0.16, 1, 0.3, 1),
                  transform 1.1s cubic-bezier(0.16, 1, 0.3, 1);
      will-change: opacity, transform;
    }

    .project-card.fade-in.visible {
      opacity: 1;
      transform: translateY(0) scale(1);
    }

    /* ─── Resume Section ───────────────────────────────────── */
    #resume {
      padding-top: 100px;
      padding-bottom: 80px;
    }

    .resume-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 48px;
      margin-top: 48px;
    }

    .resume-column-title {
      font-family: var(--font-mono);
      font-size: 13px;
      letter-spacing: 3px;
      text-transform: uppercase;
      color: var(--cyan);
      margin-bottom: 28px;
    }

    .resume-timeline {
      position: relative;
      padding-left: 24px;
      border-left: 1px solid var(--border);
    }

    .resume-item {
      position: relative;
      padding-bottom: 36px;
    }
    .resume-item:last-child { padding-bottom: 0; }

    .resume-item::before {
      content: '';
      position: absolute;
      left: -29px;
      top: 6px;
      width: 10px;
      height: 10px;
      border-radius: 50%;
      background: var(--cyan);
      box-shadow: 0 0 12px var(--cyan-dim), inset 0 0 4px rgba(255, 255, 255, 0.18);
      transition: transform 0.3s ease, box-shadow 0.3s ease;
    }
    .resume-item:hover::before {
      transform: scale(1.18);
      box-shadow: 0 0 18px var(--cyan), inset 0 0 4px rgba(255, 255, 255, 0.2);
    }

    .resume-period {
      font-family: var(--font-mono);
      font-size: 11px;
      letter-spacing: 2.5px;
      text-transform: uppercase;
      color: var(--text-dim);
      margin-bottom: 8px;
    }

    .resume-role {
      font-size: 18px;
      font-weight: 600;
      color: var(--text);
      margin: 0 0 4px;
      letter-spacing: -0.005em;
    }

    .resume-org {
      font-size: 14px;
      color: var(--cyan);
      margin: 0 0 10px;
    }

    .resume-desc {
      font-size: 14px;
      line-height: 1.65;
      color: var(--text-dim);
      margin: 0;
      max-width: 38ch;
    }

    .resume-skills {
      margin-top: 72px;
    }
    .resume-skills .skills-row {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      margin-top: 4px;
    }

    .resume-cta {
      display: flex;
      justify-content: center;
      margin-top: 56px;
    }
    .resume-cta .btn span {
      display: inline-block;
      transition: transform 0.3s ease;
    }
    .resume-cta .btn:hover span { transform: translateY(3px); }

    @media (max-width: 900px) {
      .resume-grid { grid-template-columns: 1fr; gap: 32px; }
      .resume-skills { margin-top: 48px; }
      .resume-cta { margin-top: 40px; }
    }
