/* ============================================================
   Scene B — About. Revealed in place from the side-nav (about.js / index.html).
   2026-07 simplification (Figma "About — Full Page · 1440", node 1012:2704):
   ONE quiet content column instead of the old 2-col grid + journey timeline —
   - single 600px measure, centered between the col-2 line and the right gutter;
   - sections breathe on a --space-3xl (144px) rhythm; the page scrolls
     NATURALLY (no scroll-snap, no wheel paging, no scroll-jack);
   - the statement headline is a 30px regular whisper, not a hero;
   - photos are a small 3-up strip on desktop (still a swipe carousel <1100).
   Inherits the homepage foundations so it reads as the SAME site:
   - Hyperlegible Sans (self-hosted; set globally on body + per-element here).
   - The 8-column --col / --gutter grid (columns by ALIGNMENT, never lines).
   - One uniform background: #sB is transparent so the shared #bgCanvas
     dot-grid shows through (portrait stays hidden).
   - The Ink & Paper tokens (--fg / --fg-muted / --fg-faint / --neutral).
   Everything scoped under #sB. No visible borders anywhere.
   ============================================================ */

#sB { font-family: 'Hyperlegible Sans', system-ui, sans-serif; --ab-rise-dur: 1.6s; --ab-ph-dur: 1.2s; }  /* entrance/reveal pace · photo clip-wipe */
#sB.scene { z-index: 2; background: transparent; color: var(--fg); }
/* Entry — the scene itself does NOT fade. Like Work (#sC), the nav rail must read as
   the SAME (already-visible) home nav that held in place during the swap, so fading the
   whole #sB would fade the rail too and flick at the swap. The rail stays solid; only the
   CONTENT blooms (the .ab-anim / .is-revealed reveal-on-scroll, further down). */

/* On-open content reveal keyframes (shared by the reveal-on-scroll block below). */
@keyframes ab-rise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes ab-fade { from { opacity: 0; } to { opacity: 1; } }
/* NOTE: the horizontal insets are NEGATIVE half-gaps — clip-path clips
   HIT-TESTING too, so a plain inset(0) would amputate the ::after hover
   extension that keeps photo→photo hovering free of dead zones (desktop
   bloom). Visually identical: nothing paints outside the border box. */
@keyframes ab-ph-reveal {
  from { clip-path: inset(0 calc(var(--space-2xs) / -2) 100% calc(var(--space-2xs) / -2)); }
  to   { clip-path: inset(0 calc(var(--space-2xs) / -2) 0    calc(var(--space-2xs) / -2)); }
}

/* On-close exit — a true MIRROR of the entrance: the same elements animate out
   with the entrance's durations + reveal easing, but in REVERSE order and with
   reversed motion (rise→sink, fade-in→fade-out, clip-reveal→clip-hide). Each
   group mirrors its OWN entrance cascade: exit-delay = (group's max entrance
   delay − that element's entrance delay), so whatever arrived LAST leaves FIRST.
   index.html's closeAbout() adds .ab-exiting, waits out the cascade (max delay
   800ms + 1.6s run = 2400ms), then swaps to the home stage. The nav rail is NOT
   part of the exit — it HOLDS (continuous with the home nav, like Work). */
@keyframes ab-rise-out { from { opacity: 1; transform: none; }       to { opacity: 0; transform: translateY(18px); } }
@keyframes ab-fade-out { from { opacity: 1; }                        to { opacity: 0; } }
@keyframes ab-ph-out {
  from { clip-path: inset(0 calc(var(--space-2xs) / -2) 0    calc(var(--space-2xs) / -2)); }
  to   { clip-path: inset(0 calc(var(--space-2xs) / -2) 100% calc(var(--space-2xs) / -2)); }
}

/* Statement column — reverse of its entrance. .ab-left children:
   (1) eyebrow · (2) title group · (3) photo media (clip, not rise) ·
   (4) note · (5) note · (6) socials. */
#sB.ab-anim.ab-exiting .ab-about.is-revealed .ab-left > *:not(.ab-about-media) {
  animation: ab-rise-out var(--ab-rise-dur) cubic-bezier(0.22, 1, 0.36, 1) both;
}
#sB.ab-anim.ab-exiting .ab-about.is-revealed .ab-about-media .ab-ph {
  animation: ab-ph-out var(--ab-ph-dur) cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: 500ms;
}
#sB.ab-anim.ab-exiting .ab-about.is-revealed .ab-ph-dots {
  animation: ab-fade-out var(--ab-rise-dur) cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: 500ms;
}
#sB.ab-anim.ab-exiting .ab-about.is-revealed .ab-left > *:nth-child(6) { animation-delay: 0ms; }    /* socials (arrived last) */
#sB.ab-anim.ab-exiting .ab-about.is-revealed .ab-left > *:nth-child(5) { animation-delay: 120ms; }
#sB.ab-anim.ab-exiting .ab-about.is-revealed .ab-left > *:nth-child(4) { animation-delay: 240ms; }
#sB.ab-anim.ab-exiting .ab-about.is-revealed .ab-left > *:nth-child(2) { animation-delay: 720ms; }
#sB.ab-anim.ab-exiting .ab-about.is-revealed .ab-left > *:nth-child(1) { animation-delay: 800ms; }
/* Stack section (if it was revealed when closing) — quick reverse cascade. */
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-eyebrow,
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-title,
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-item {
  animation: ab-rise-out var(--ab-rise-dur) cubic-bezier(0.22, 1, 0.36, 1) both;
}
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-item:nth-child(6) { animation-delay: 0ms; }
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-item:nth-child(5) { animation-delay: 60ms; }
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-item:nth-child(4) { animation-delay: 120ms; }
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-item:nth-child(3) { animation-delay: 180ms; }
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-item:nth-child(2) { animation-delay: 240ms; }
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-item:nth-child(1) { animation-delay: 300ms; }
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-title             { animation-delay: 420ms; }
#sB.ab-anim.ab-exiting .ab-stack.is-revealed .ab-stack-eyebrow           { animation-delay: 500ms; }

/* ---------------- shared type (homepage tokens, mirrored) ---------------- */
#sB .ab-eyebrow,
#sB .ab-stack-eyebrow {
  font-size: var(--fz-microcopy); font-weight: 500; letter-spacing: .05em;
  text-transform: uppercase; color: var(--fg-faint);
}
/* The statement headline — a quiet conversational block, NOT a hero.
   20px on small mobile → 30px from tablet up (fz-section-head is 20/30/40,
   so desktop re-pins it to --fz-mid 30 below). Regular weight per the Figma. */
#sB .ab-title {
  font-size: var(--fz-section-head); font-weight: 400; line-height: 1.2;
  color: var(--fg);
}
#sB .ab-title-group {
  display: flex; flex-direction: column; gap: var(--space-2xs);
  margin-top: var(--space-sm);
}
#sB .ab-note {                                       /* = .sa-greet-sub */
  font-size: var(--fz-body); font-weight: 400; color: var(--fg-muted);
  line-height: 1.5; margin-top: var(--space-lg);
}
/* tighter gap BETWEEN consecutive paragraphs (vs the photos→body gap). */
#sB .ab-note + .ab-note { margin-top: var(--space-md); }
/* Super-wide tier — AB-7 (Jacky 2026-08-21): on a wide monitor the 600px
   column read as a sliver ("唔夠 balance"), but 600px at 16px body is
   already the ~75ch measure ceiling (CS-4 F4) — widening alone would blow
   the measure. So the column and the type scale TOGETHER, mirroring the
   case study's ≥2560 ×1.25 container precedent: body 16→20 with the
   column 600→750 keeps the measure constant at ~75ch. Threshold pulled
   forward to 1680 (the RES-2 direction, piloted here first — his monitor
   is ~2000px and the old 2560 gate never fired for him). The column rule
   itself lives with its base rule in the ≥1100 block below (source order). */
@media (min-width: 1680px) {
  #sB { --fz-body: 20px; }
}

/* ---------------- socials row (closes the statement column) ----------------
   Same 24px icon row as the home greeting column; order mirrors the Figma
   (IG · Pinterest · GitHub · LinkedIn — sketchbook side first on About). */
#sB .ab-socials { display: flex; gap: var(--space-sm); align-items: center; margin-top: var(--space-lg); }
#sB .ab-socials a {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; color: var(--fg-muted);
  transition: color var(--t-base);
}
@media (hover: hover) { #sB .ab-socials a:hover { color: var(--fg); } }
#sB .ab-socials svg { width: 24px; height: 24px; display: block; fill: currentColor; }

/* ---------------- mobile (default flow; body scrolls) ---------------- */
/* No rail on mobile — chrome is the GLOBAL wordmark (home) + hamburger (nav),
   promoted to <body> in index.html, exactly like the home stage and Work. */
#sB .ab-rail { display: none; }

#sB .ab-section { padding: var(--space-2xl) var(--gutter); }
/* AB-2: the last section needs a real tail on mobile. Round 1 used 3xl
   (144px → 88px of clearance past the fixed toggle) and Jacky accepted it
   but wanted the content to sit nearer the top at full scroll — his spec is
   ~400px of empty tail (2026-08-12). A deliberate one-off length, not a
   rhythm token: the spacing scale tops out at 144 and this is page
   architecture, not inter-block rhythm. Desktop resets this (its scroller
   .ab-main already carries the 3xl block padding). */
#sB .ab-main > .ab-section:last-child { padding-bottom: 400px; }
/* First section clears the fixed global wordmark + burger (same top inset Work's .wk-main uses). */
#sB .ab-main > .ab-section:first-child { padding-top: calc(32px + 28px + var(--space-lg)); }

/* ---------------- About-me (01) photo set ----------------
   tablet/mobile = full-bleed horizontal swipe carousel + pager dots;
   desktop (≥1100) = a small static 3-up strip inside the column (below). */
#sB .ab-about-media { margin-top: var(--space-lg); }
#sB .ab-photogrid {
  display: flex; gap: var(--space-2xs);
  overflow-x: auto; scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
  margin-inline: calc(-1 * var(--gutter)); padding-inline: var(--gutter);
  scroll-padding-inline: var(--gutter);
  /* pan-x AND pan-y (AB-4, found by Jacky on device 2026-08-10): pan-x alone
     told the browser to swallow vertical gestures that start on a photo, so
     the PAGE couldn't scroll while a finger was on the strip. With both axes
     allowed the browser routes by dominant direction natively: horizontal →
     this strip's own overflow-x scroll, vertical → page scroll. Touch swipe
     is native scrolling (about.js only drags for mouse), so JS is untouched. */
  cursor: grab; user-select: none; touch-action: pan-x pan-y;
}
#sB .ab-photogrid.is-dragging { cursor: grabbing; scroll-snap-type: none; }
#sB .ab-ph img, #sB .ab-ph { -webkit-user-drag: none; }
#sB .ab-photogrid::-webkit-scrollbar { display: none; }
#sB .ab-ph {
  flex: 0 0 84%; margin: 0; scroll-snap-align: start;
  aspect-ratio: 3 / 4; background-color: var(--neutral);
  background-size: cover; background-position: center; background-repeat: no-repeat;
  border-radius: 8px;                                /* Figma: rounded photo cards */
  transition: filter var(--t-base) ease;
}
#sB .ab-ph-a { background-image: url("../assets/about/about-a.webp"); }
#sB .ab-ph-b { background-image: url("../assets/about/about-b.webp"); }
#sB .ab-ph-c { background-image: url("../assets/about/about-c.webp"); }
#sB .ab-ph-dots { display: flex; justify-content: center; gap: var(--space-sm); margin-top: var(--space-md); }
#sB .ab-ph-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--fg-faint); cursor: pointer; transition: width var(--t-base) ease, background-color var(--t-base) ease; }
@media (hover: hover) { #sB .ab-ph-dots span:not(.is-active):hover { background: var(--fg-muted); } }
#sB .ab-ph-dots span.is-active { width: 18px; border-radius: 3px; background: var(--fg); }

/* tablet (≥737): carousel slides go landscape */
@media (min-width: 737px) {
  #sB .ab-ph { flex-basis: 78%; aspect-ratio: 3 / 2; }
}

/* ---------------- photo speech-bubble that rides the cursor (pixel sticker) ----
   A single Space Mono bubble (built once in about.js) that follows the pointer
   while it's over an About-me photo, carrying that photo's data-bubble caption.
   Desktop/mouse: native cursor is hidden over the photo and the bubble IS the
   cursor (about.js sets left/top per pointermove). Touch: tapping the centred
   photo parks the bubble over it (about.js). One element, so it lives ABOVE the
   photos — free of the persistent clip-path: inset(0) the reveal leaves on .ab-ph.
   Colours/size/spacing come from tokens: paper/ink flip with the theme. */
#sB .ab-cursor-bubble {
  --bubble-ink: var(--fg);
  --bubble-paper: var(--white);
  position: fixed; left: 0; top: 0; z-index: 60;   /* JS sets left/top to the pointer */
  pointer-events: none;
  /* pre = break ONLY at the author's newline in data-bubble, never auto-wrap —
     so the bubble shows the exact lines you write, no surprise reflow. */
  white-space: pre; text-align: center;
  padding: var(--space-2xs) var(--space-xs);
  font-family: 'Space Mono', monospace; font-weight: 700;
  font-size: var(--fz-microcopy); line-height: 1.3; letter-spacing: .5px; text-transform: uppercase;
  color: var(--bubble-ink); background: var(--bubble-paper);
  border: 2.5px solid var(--bubble-ink); border-radius: 7px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, .22);
  opacity: 0;
  /* anchor: bubble sits above the pointer, its tail tip at the pointer */
  transform: translate(-50%, calc(-100% - var(--space-xs))) scale(.9);
  transform-origin: bottom center;
  /* only opacity+transform animate (the reveal); left/top are NOT listed, so the
     position-follow is instant — no laggy trailing behind the cursor. */
  transition: opacity var(--t-fast) cubic-bezier(0.22, 1, 0.36, 1),
              transform var(--t-fast) cubic-bezier(0.22, 1, 0.36, 1);
}
#sB .ab-cursor-bubble::after {                       /* outlined downward pointer */
  content: ""; position: absolute; left: 50%; bottom: 0;
  width: 12px; height: 12px; background: var(--bubble-paper);
  border-right: 2.5px solid var(--bubble-ink);
  border-bottom: 2.5px solid var(--bubble-ink);
  transform: translate(-50%, 50%) rotate(45deg);
}
#sB .ab-cursor-bubble.is-on {
  opacity: 1;
  transform: translate(-50%, calc(-100% - var(--space-xs))) scale(1);
}
/* a photo wearing the bubble hides the native cursor (class toggled in about.js) */
#sB .ab-about .ab-ph.is-bubble-cursor { cursor: none; }
@media (prefers-reduced-motion: reduce) {
  #sB .ab-cursor-bubble { transition: opacity var(--t-fast) ease; }
}

/* ---------------- desktop stage (≥1100): rail + one breathing column ---------------- */
@media (min-width: 1100px) {
  #sB.scene.active { position: fixed; inset: 0; display: block; }

  /* nav rail — geometry + type mirror the home .sa-mini-nav / Work .wk-railnav EXACTLY
     so the held home→about swap is seamless (same anchor, width, rhythm, tracking).
     Fixed full-bleed wrapper; clicks pass through to .ab-main, the rail itself is live. */
  #sB .ab-rail { display: block; position: fixed; inset: 0; pointer-events: none; z-index: 30; }
  #sB .ab-rail > * { pointer-events: auto; }
  #sB .ab-railnav {
    position: absolute; left: var(--gutter); top: 53%; transform: translateY(-50%);
    width: 200px; display: flex; flex-direction: column;
  }
  #sB .ab-railnav a {
    display: flex; align-items: baseline; gap: var(--space-sm); padding: var(--space-xs) 0;
    width: 100%; line-height: 1; text-decoration: none; color: var(--fg-muted);
    transition: opacity var(--t-base) ease, color var(--t-base) ease;
  }
  #sB .ab-railnav a.is-active { color: var(--fg); }   /* current scene (About) highlighted */
  #sB .ab-rl-meta {
    font-size: var(--fz-microcopy); font-weight: 500; letter-spacing: .05em;
    color: var(--fg-faint); min-width: 22px; line-height: 1;
  }
  #sB .ab-railnav a.is-active .ab-rl-meta { color: var(--fg); }
  /* Match the home nav EXACTLY: un-kerned uppercase label at --fz-mid + --nav-tracking
     (the home nav renders per-character so it can't kern; mirror that here). */
  #sB .ab-rl-label {
    /* BR-7 bold diet: rest = regular; bold marks hover + the current scene. */
    font-size: var(--fz-mid); font-weight: 400; line-height: 1; text-transform: uppercase; color: inherit;
    letter-spacing: var(--nav-tracking);
    font-kerning: none; font-variant-ligatures: none;
  }
  #sB .ab-railnav a.is-active .ab-rl-label { font-weight: 700; }
  /* sibling-dim on hover (homepage signature) — identical to .sa-mini-nav /
     .wk-railnav, with the bold hover voice (BR-7). .rail-settle
     (set by stageExit on swap-in, cleared on the first mousemove) suppresses
     the dim so a rail landing under a parked cursor never flashes. */
  #sB .ab-railnav:hover a { opacity: .4 !important; } /* BR-8: .25 → .4, see index.html note */
  #sB .ab-railnav a:hover { opacity: 1 !important; color: var(--fg); }
  #sB .ab-railnav a:not(.is-soon):hover .ab-rl-label { font-weight: 700; }
  #sB .ab-railnav.rail-settle:hover a { opacity: 1 !important; }
  /* PLAY is disabled site-wide (no scene yet) — the label refuses to light.
     Cursor + SOON-tag rules are global in index.html (a.is-soon / .nav-soon). */
  #sB .ab-railnav a.is-soon:hover { color: var(--fg-muted); }

  /* One naturally-scrolling column. NO scroll-snap / wheel paging — the air
     (--space-3xl rhythm) is the pacing now. */
  #sB .ab-main { position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden; padding-block: var(--space-3xl); }

  /* Sections give up the old 8-col grid: each is a block whose inner column
     (max 600px) centers between the col-2 line and the right gutter — the rail
     keeps col 1, the column floats in the remaining space (Figma placement). */
  #sB .ab-section { padding: 0 var(--gutter) 0 calc(var(--gutter) + var(--col)); }
  #sB .ab-main > .ab-section:first-child { padding-top: 0; }   /* .ab-main's padding-block is the top inset */
  #sB .ab-main > .ab-section:last-child { padding-bottom: 0; } /* mobile-only tail (AB-2) — .ab-main's padding-block is the bottom inset here */
  #sB .ab-section + .ab-section { margin-top: var(--space-3xl); }
  #sB .ab-left, #sB .ab-stack-inner { max-width: 600px; margin-inline: auto; }
  /* AB-7 super-wide: column scales WITH the ≥1680 body bump (16→20) so the
     measure stays ~75ch — 750 = 600 × 1.25, the case-study container
     precedent. Photo-strip bloom uses flex RATIOS, so it scales along. */
  @media (min-width: 1680px) {
    #sB .ab-left, #sB .ab-stack-inner { max-width: 750px; }
  }

  /* 30px statement (fz-section-head is 40 at desktop; re-pin to --fz-mid). */
  #sB .ab-title, #sB .ab-stack-title { font-size: var(--fz-mid); }

  /* photos: a small static 3-up strip inside the column (Figma: 240px, 8px gaps)
     + gentle hover BLOOM (Figma hover exploration): the hovered photo grows to
     300px wide — and proportionally taller, overflowing the row on both sides —
     while its siblings cede width. Numbers:
       rest width  = (600 − 2·8px) / 3 ≈ 195px
       hover scale = 300 / 195      ≈ 1.538  (--ab-ph-grow)
       flex-grow   = 300 / 142      ≈ 2.113  (siblings share 600 − 16 − 300)
     The media block RESERVES the vertical overflow (≈65px each side) so the
     visual --space-lg gap holds even mid-bloom — text never moves, nothing
     gets covered. Growth is flex-grow + height only (both px-animatable),
     one brand easing, --t-slow. */
  #sB .ab-about { --ab-ph-h: 240px; --ab-ph-grow: 1.538; }
  #sB .ab-about .ab-about-media {
    /* Plain --space-lg rhythm above AND below (Jacky's call, 2026-07-21). The
       hover bloom does NOT float over text — the strip grows in the layout and
       the content below GLIDES down with it (the container's height follows
       the tallest photo, so the reflow animates with the same easing). */
    margin-block: var(--space-lg);
  }
  #sB .ab-about .ab-photogrid {
    /* center: during the hover bloom the two resting photos sit vertically
       CENTERED against the grown one (Figma reference), not top-aligned. */
    align-items: center; overflow: visible; scroll-snap-type: none;
    margin: 0; padding: 0;
    cursor: auto; user-select: auto; touch-action: auto;   /* static strip, not a carousel */
    /* The strip's own height is animated SEPARATELY from the photos (below):
       while ANY photo is hovered the strip holds the full bloom height, so
       moving photo→photo crossfades the photos WITHOUT the container height
       dipping mid-swap (which nudged the text below up-and-down). The text
       only glides once — on entering / leaving the strip. */
    height: var(--ab-ph-h);
    transition: height var(--t-slow) cubic-bezier(0.22, 1, 0.36, 1);
  }
  #sB .ab-about .ab-ph {
    flex: 1 1 0; min-width: 0; height: var(--ab-ph-h); aspect-ratio: auto; scroll-snap-align: none;
    position: relative;
    transition: flex-grow var(--t-slow) cubic-bezier(0.22, 1, 0.36, 1),
                height    var(--t-slow) cubic-bezier(0.22, 1, 0.36, 1),
                filter    var(--t-base) ease;
  }
  /* Contiguous hover hit-areas: each photo extends an invisible half-gap into
     the gutters beside it, so travelling photo→photo never crosses a dead zone.
     Without this, the instant where NOTHING is hovered mid-crossing started a
     collapse that the next hover immediately reversed — read as a jitter. */
  #sB .ab-about .ab-ph::after {
    content: ""; position: absolute; top: 0; bottom: 0;
    left: calc(var(--space-2xs) / -2); right: calc(var(--space-2xs) / -2);
  }
  @media (hover: hover) {
    /* the strip locks to the bloom height while the cursor is anywhere on it
       (photos + their gap-covering ::after extensions), keeping the layout
       below rock-steady during photo→photo crossfades */
    #sB .ab-about .ab-photogrid:hover { height: calc(var(--ab-ph-h) * var(--ab-ph-grow)); }
    /* sibling-dim, the homepage nav signature at a whisper */
    #sB .ab-about .ab-photogrid:hover .ab-ph { filter: brightness(.94); }
    #sB .ab-about .ab-ph:hover {
      flex-grow: 2.113;
      height: calc(var(--ab-ph-h) * var(--ab-ph-grow));
      filter: brightness(1.04);
    }
  }
  #sB .ab-about .ab-ph-dots { display: none; }
}

/* ================= Tools ("My stack") ================= */
#sB .ab-stack-title {
  font-size: var(--fz-section-head); font-weight: 400; line-height: 1.2;
  color: var(--fg); margin-top: var(--space-xs);
}
#sB .ab-stack-list {
  list-style: none; margin: var(--space-xl) 0 0; padding: 0;
  display: grid; grid-template-columns: 1fr; gap: var(--space-md);
}
#sB .ab-stack-item { display: flex; align-items: center; gap: var(--space-xs); }
#sB .ab-stack-icon {
  flex: 0 0 52px; width: 52px; height: 52px; border-radius: 8px; overflow: hidden;
}
#sB .ab-stack-icon img { width: 100%; height: 100%; object-fit: cover; display: block; }
#sB .ab-stack-meta { display: flex; flex-direction: column; gap: 2px; }
/* AB-6 (Jacky 2026-08-20): the tool NAME sat on --fg-muted (60%) next to the
   desc's --fg-faint (56%) — the two greys are near-identical (BR-4), so the
   pair read as one colour. Name goes full --fg, same name/desc hierarchy as
   the Work tile caption (.wk-tile-name --fg / .wk-tile-meta muted). */
#sB .ab-stack-name { font-size: var(--fz-sub-head); font-weight: 500; line-height: 1.2; color: var(--fg); }
#sB .ab-stack-desc { font-size: var(--fz-body); font-weight: 400; line-height: 1.4; color: var(--fg-faint); }
/* ≥737: the list becomes the Figma 2×3 grid (placed AFTER the base rule so it wins). */
@media (min-width: 737px) {
  #sB .ab-stack-list { grid-template-columns: 1fr 1fr; }
}

/* ---------------- reveal-on-scroll (about.js adds .ab-anim / .is-revealed) ----------------
   Hidden state scoped to #sB.ab-anim so content stays visible if JS never runs.
   The reveal is an ANIMATION (not a transition) so the first section can be
   revealed in the same frame the scene becomes visible without snapping. */
#sB.ab-anim .ab-about .ab-left > *:not(.ab-about-media),
#sB.ab-anim .ab-stack .ab-stack-eyebrow,
#sB.ab-anim .ab-stack .ab-stack-title,
#sB.ab-anim .ab-stack .ab-stack-item,
#sB.ab-anim .ab-section .ab-ph-dots { opacity: 0; }
/* photos reveal with a top-down clip wipe (the GSAP curtain in pure CSS).
   Horizontal insets stay at the negative half-gap — see the keyframes note. */
#sB.ab-anim .ab-section .ab-about-media .ab-ph { clip-path: inset(0 calc(var(--space-2xs) / -2) 100% calc(var(--space-2xs) / -2)); }

#sB.ab-anim .ab-about.is-revealed .ab-left > *:not(.ab-about-media) {
  animation: ab-rise var(--ab-rise-dur) cubic-bezier(0.22, 1, 0.36, 1) both;
}
#sB.ab-anim .ab-about.is-revealed .ab-about-media .ab-ph {
  animation: ab-ph-reveal var(--ab-ph-dur) cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: 300ms;
}
#sB.ab-anim .ab-about.is-revealed .ab-ph-dots {
  animation: ab-fade var(--ab-rise-dur) cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: 300ms;
}
/* stagger: eyebrow → title → photos wipe → body → socials (unhurried) */
#sB.ab-anim .ab-about.is-revealed .ab-left > *:nth-child(2) { animation-delay: 80ms; }    /* title group */
#sB.ab-anim .ab-about.is-revealed .ab-left > *:nth-child(4) { animation-delay: 560ms; }   /* body para 1 */
#sB.ab-anim .ab-about.is-revealed .ab-left > *:nth-child(5) { animation-delay: 680ms; }   /* body para 2 */
#sB.ab-anim .ab-about.is-revealed .ab-left > *:nth-child(6) { animation-delay: 800ms; }   /* socials */

/* Tools reveal: eyebrow → headline → items cascade (~90ms apart) */
#sB.ab-anim .ab-stack.is-revealed .ab-stack-eyebrow,
#sB.ab-anim .ab-stack.is-revealed .ab-stack-title,
#sB.ab-anim .ab-stack.is-revealed .ab-stack-item {
  animation: ab-rise var(--ab-rise-dur) cubic-bezier(0.22, 1, 0.36, 1) both;
}
#sB.ab-anim .ab-stack.is-revealed .ab-stack-title            { animation-delay: 140ms; }
#sB.ab-anim .ab-stack.is-revealed .ab-stack-item:nth-child(1) { animation-delay: 320ms; }
#sB.ab-anim .ab-stack.is-revealed .ab-stack-item:nth-child(2) { animation-delay: 410ms; }
#sB.ab-anim .ab-stack.is-revealed .ab-stack-item:nth-child(3) { animation-delay: 500ms; }
#sB.ab-anim .ab-stack.is-revealed .ab-stack-item:nth-child(4) { animation-delay: 590ms; }
#sB.ab-anim .ab-stack.is-revealed .ab-stack-item:nth-child(5) { animation-delay: 680ms; }
#sB.ab-anim .ab-stack.is-revealed .ab-stack-item:nth-child(6) { animation-delay: 770ms; }

/* Super-wide (≥2560): the rail index numbers sit one type level larger (≈20), and the
   active section's number steps up one more (≈30). Placed after the ≥1100 rail rules so
   it wins on source order; values mirror the type ladder via the transform scale. */
@media (min-width: 2560px) {
  #sB .ab-railnav a           { font-weight: 500; --rl-num-scale: 1.43; }   /* ≈14 → 20 (sub-head) */
  #sB .ab-railnav a.is-active { font-weight: 600; --rl-num-scale: 2.14; }   /* ≈14 → 30 (mid) */
}

@media (prefers-reduced-motion: reduce) {
  #sB, #sB * { transition: none !important; animation: none !important; }
  #sB .ab-main { scroll-behavior: auto; }
}
