/* =====================================================================
   SHOWCASE SECTIONS — Partnerships, Experience, Education, FAQ

   Loaded last so it wins the id-scoped sweeps in tokens.css without
   needing !important on every declaration. Where a sweep still outranks
   a plain class (the square-radius and stroke sweeps), the selector
   repeats an attribute to lift past it — the same trick documented in
   css/playground-all.css.
   ===================================================================== */

:root {
  --showcase-line: rgba(17, 17, 17, .12);
  --showcase-rail: rgba(17, 17, 17, .10);
  --showcase-muted-bg: #ededed;
  --showcase-muted-ink: #8e8e8e;
  --showcase-active-bg: #ffffff;
  --showcase-active-ink: #141414;
  --showcase-ease: var(--ease-out);
}

:root[data-theme="dark"] {
  --showcase-line: rgba(255, 255, 255, .14);
  --showcase-rail: rgba(255, 255, 255, .12);
  /* Light mode separates the focused testimonial by a full step — white
     against grey. Dark mode's #212121 on #1a1a1a was barely a step at all,
     so the focused card read the same as its neighbours. The muted cards
     drop back and the active one lifts, giving the pair the same contrast
     ratio the light theme has. */
  --showcase-muted-bg: #141414;
  --showcase-muted-ink: #6e6e6e;
  --showcase-active-bg: #2e2e2e;
  --showcase-active-ink: #ffffff;
}

/* =====================================================================
   1 · PARTNERSHIPS — one row of quotes, the focused one lit

   The deck is a scroll-snap track rather than a wrapping grid: four
   cards read at desktop width, the rest are a swipe away, and the same
   markup carries the mobile carousel. Cards sit muted until pointed at
   (or, on touch, until they are the one centred in the track).
   ===================================================================== */
#partnerships .partnerships__deck {
  display: grid;
  gap: clamp(18px, 2vw, 26px);
}

#partnerships .partnerships__cards[class] {
  --pc-gap: clamp(10px, 1vw, 16px);
  display: grid;
  /* The bundle pins three explicit columns; the deck needs an implicit
     column per card instead, so the template has to be cleared before
     grid-auto-columns can size them. */
  grid-template-columns: none !important;
  grid-auto-flow: column;
  grid-auto-columns: calc((100% - 3 * var(--pc-gap)) / 4);
  align-items: stretch;
  min-height: 0;
  padding: 0 0 2px;
  /* The mobile rules in homepage-bundle reset this deck to
     overflow: visible, which silently killed the swipe. */
  overflow-x: auto !important;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory !important;
  scrollbar-width: none;
}

/* The old deck dimmed every non-hovered card; the active state does that
   job now, and the dimming fought it. */
#partnerships .partnerships__cards:hover .partnership-card:not(:hover) {
  opacity: 1 !important;
}
#partnerships .partnerships__cards::-webkit-scrollbar { display: none; }

#partnerships .partnership-card[class] {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: clamp(300px, 34vw, 430px) !important;
  gap: 0 !important;
  padding: clamp(20px, 1.8vw, 30px) !important;
  scroll-snap-align: start;
  background: var(--showcase-muted-bg) !important;
  color: var(--showcase-muted-ink);
  border: 0;
  transition: background-color var(--dur-3) var(--showcase-ease),
    color var(--dur-3) var(--showcase-ease),
    transform var(--dur-3) var(--showcase-ease);
}

/* The aurora was the old card's decoration; the new treatment is flat. */
#partnerships .partnership-card .pc__aurora { display: none !important; }

#partnerships .partnership-card blockquote {
  color: inherit;
  transition: color var(--dur-3) var(--showcase-ease);
}

#partnerships .partnership-card figcaption {
  margin-top: clamp(28px, 4vw, 56px);
  color: inherit;
}
#partnerships .partnership-card figcaption strong {
  color: inherit;
  transition: color var(--dur-3) var(--showcase-ease);
}
#partnerships .partnership-card figcaption small {
  opacity: .78;
}

#partnerships .partnership-card.is-active[class] {
  background: var(--showcase-active-bg) !important;
  color: var(--showcase-active-ink);
}
#partnerships .partnership-card.is-active blockquote,
#partnerships .partnership-card.is-active figcaption strong {
  color: var(--showcase-active-ink);
}
#partnerships .partnership-card:focus-visible {
  outline: 2px solid var(--focus-ring, #4dce16);
  outline-offset: -2px;
}

/* The rail under the deck: a hairline the width of the row, with a solid
   thumb tracking whichever card is active. */
#partnerships .partnerships__rail {
  position: relative;
  height: 1px;
  background: var(--showcase-rail);
}
#partnerships .partnerships__rail-thumb {
  position: absolute;
  top: -1px;
  left: 0;
  height: 3px;
  /* A fixed 100px base that JS scales, rather than an animated width:
     both the slide and the resize ride on one composited transform, so
     moving between cards of different widths never triggers layout. */
  width: 100px;
  background: var(--showcase-active-ink);
  transform-origin: left center;
  transform: translateX(0) scaleX(0);
  transition: transform var(--dur-3) var(--showcase-ease);
  will-change: transform;
}

@media (max-width: 1100px) {
  #partnerships .partnerships__cards[class] {
    grid-template-columns: none;
  }
}
@media (max-width: 760px) {
  #partnerships .partnerships__cards[class] {
    grid-template-columns: none;
    grid-auto-columns: 84% !important;
    --pc-gap: 12px;
  }
    #partnerships .partnership-card figcaption { margin-top: 26px; }
}

/* =====================================================================
   2 · EXPERIENCE + EDUCATION — ruled rows that open on demand

   Role left, dates right, description behind a disclosure. The height
   interpolates with grid-template-rows 0fr -> 1fr, which animates
   smoothly for any copy length (a fixed max-height snaps on the short
   ones).
   ===================================================================== */
#experience .exp__list[class],
#education-recognition .exp__list[class] {
  list-style: none;
}

#experience .exp__row[class],
#education-recognition .exp__row[class] {
  display: block;
  padding: 0;
  border-bottom: 1px solid var(--showcase-line);
}
/* No rule above the first row: the section heading already separates the
   list from what precedes it, and the extra line read as a stray edge. */
#experience .exp__list > .exp__row:first-child,
#education-recognition .exp__list > .exp__row:first-child {
  border-top: 0 !important;
}

#experience .exp__row-head,
#education-recognition .exp__row-head {
  display: flex;
  width: 100%;
  align-items: baseline;
  justify-content: space-between;
  gap: clamp(16px, 3vw, 48px);
  padding: clamp(16px, 1.6vh, 22px) 0;
  text-align: left;
}

#experience .exp__role,
#education-recognition .exp__role {
  font-weight: 500;
  letter-spacing: -.02em;
  transition: opacity var(--dur-2) ease;
}

#experience .exp__dates[class],
#education-recognition .exp__dates[class] {
  flex: 0 0 auto;
  color: var(--text-tertiary, #8e8e8e);
  letter-spacing: -.01em;
  white-space: nowrap;
}

/* Closed rows dim their title a touch so the open one leads. */
#experience .exp__row:not([data-open]) .exp__role,
#education-recognition .exp__row:not([data-open]) .exp__role { opacity: .82; }
#experience .exp__row-head:hover .exp__role,
#education-recognition .exp__row-head:hover .exp__role { opacity: 1; }

#experience .exp__row-body,
#education-recognition .exp__row-body {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows var(--dur-3) var(--showcase-ease),
    opacity var(--dur-2) ease;
}
#experience .exp__row-body > p,
#education-recognition .exp__row-body > p {
  min-height: 0;
  overflow: hidden;
  max-width: 68ch;
  padding-bottom: 0;
  transform: translateY(-4px);
  transition: transform var(--dur-3) var(--showcase-ease),
    padding-bottom var(--dur-3) var(--showcase-ease);
}
#experience .exp__row[data-open] .exp__row-body,
#education-recognition .exp__row[data-open] .exp__row-body {
  grid-template-rows: 1fr;
  opacity: 1;
}
#experience .exp__row[data-open] .exp__row-body > p,
#education-recognition .exp__row[data-open] .exp__row-body > p {
  padding-bottom: clamp(14px, 1.8vh, 22px);
  transform: none;
}

/* Stat cards: label on top, figure beneath, one flat plate each. The
   label was sitting at the scale's smallest step, which read as shrunken
   next to the figure it titles — one step up puts it back on the body
   ramp. */

/* Stat cards: label on top, figure beneath, one flat plate each. */
#experience .exp__counters[class] {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-bottom: clamp(30px, 4vh, 52px);
}
#experience .counter[class] {
  align-content: start;
  background: var(--showcase-active-bg);
}

@media (max-width: 760px) {
  #experience .exp__counters[class] { grid-template-columns: 1fr; }
  #experience .exp__row-head,
  #education-recognition .exp__row-head {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
  }
  #experience .exp__dates[class],
  #education-recognition .exp__dates[class] { white-space: normal; }
}

/* =====================================================================
   3 · FAQ — ruled rows, circled +/-

   The rows already open and close; this restores the hairline between
   them and swaps the glyph for the outlined circle in the comp.
   ===================================================================== */
#insights .accordion__item[class] {
  border-bottom: 1px solid var(--showcase-line) !important;
}
#insights .accordion__item + .accordion__item { margin-top: 0 !important; }

#insights .accordion--faq .accordion__icon[class] {
  flex: 0 0 auto;
  border: 1px solid var(--showcase-line);
  color: var(--text-secondary, #6e6e6e);
  transition: transform var(--dur-3) var(--showcase-ease),
    border-color var(--dur-2) ease,
    color var(--dur-2) ease;
}
#insights .accordion__head:hover .accordion__icon[class] {
  border-color: var(--text-primary, #141414) !important;
  color: var(--text-primary, #141414);
}
#insights .accordion__item.is-open .accordion__icon[class] {
  color: var(--text-primary, #141414);
  border-color: var(--text-primary, #141414) !important;
}

/* The plus becomes a minus: the vertical bar of the glyph is scaled out
   rather than the whole icon being rotated, so it reads as one stroke
   retracting instead of a spin. */
#insights .accordion__icon svg [d="M12 5v14"],
#insights .accordion__icon svg path:nth-child(2) {
  transform-origin: 12px 12px;
  transition: transform var(--dur-3) var(--showcase-ease), opacity var(--dur-2) ease;
}
#insights .accordion__item.is-open .accordion__icon svg path:nth-child(2) {
  transform: scaleY(0);
  opacity: 0;
}
/* The old rule spun the whole icon 45deg to fake the minus; with the bar
   animating there is nothing left to rotate. */
#insights .accordion__item.is-open .accordion__icon[class][class] {
  transform: none !important;
}

/* =====================================================================
   4 · WORDMARKS — one weight, and the footer set inside its frame

   Both wordmarks say "SOHAN J." in Zalando Sans Expanded. They now share
   the footer's original 500: the hero's 700 was the heavier of the two
   and the lighter cut is the one being kept.
   ===================================================================== */
.hero__wordmark[class],
.folio-footer__wordmark .folio-footer__word[class] {
  font-weight: 500 !important;
}

/* The footer word was sized past its frame — 1206px of glyph in a 1220px
   plate with a 25px inset on the left, so the final period bled off the
   right edge. The wordmark now owns an equal inset on both sides and the
   type is sized from the viewport so it always lands inside it. */
/* The three custom properties are solved in js/showcase-sections.js from
   the font's own metrics, so the GLYPHS — not the invisible text box —
   clear the plate by the same gap on the left, right and bottom. The
   fallbacks keep the word inside the plate if that script never runs. */
.folio-footer__wordmark[class] {
  left: 0 !important;
  right: 0 !important;
  bottom: var(--wordmark-bottom, 15px) !important;
  padding-inline: 0 !important;
  display: block;
  text-align: left;
}
.folio-footer__wordmark .folio-footer__word[class] {
  display: block;
  width: 100%;
  /* The word carries justify-self: center, which halved a margin-left
     shift (100px of margin moved it 50px). A transform is outside
     alignment's reach, so the solved offset lands exactly. */
  text-align: left !important;
  justify-self: start !important;
  margin-left: 0 !important;
  transform: translateX(var(--wordmark-shift, 24px));
  font-size: var(--wordmark-size, min(14vw, 190px)) !important;
  white-space: nowrap;
}

/* The two footer CTAs sat a full 18px apart, which read as two unrelated
   controls rather than a primary with its alternate beside it. */
#contact .folio-footer__actions[class] {
  gap: clamp(8px, .7vw, 10px);
}

/* =====================================================================
   5 · MOBILE NAV — tighter bar, toggle in the tray, wipe-in menu
   ===================================================================== */

@media (max-width: 900px) {
  /* The capsule's own inset already matches the page gutter; this is the
     padding INSIDE it, which was holding the logo and the burger a
     further 16px off each edge. */
  /* The bar's own padding rules are id-scoped and !important, so this has
     to carry an id too. */
  #nav[class],
  #nav.nav--on-dark[class] {
    padding-inline: 8px !important;
  }
}

/* The theme control keeps its place in the header, just before the menu
   button — but only while the menu is open, when that button reads as a
   close. Collapsed, the bar is the logo and the burger alone. */
@media (max-width: 900px) {
  .theme-toggle--mobile[class] {
    opacity: 0;
    visibility: hidden;
    transform: scale(.82);
    transition: opacity var(--dur-2) ease,
      transform var(--dur-3) var(--showcase-ease),
      visibility 0s linear .28s;
  }
  .mobnav-open .theme-toggle--mobile[class] {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity var(--dur-2) ease .18s,
      transform var(--dur-3) var(--showcase-ease) .18s,
      visibility 0s;
  }
}

/* ---- The entrance ----------------------------------------------------
   Each link rises letter by letter from behind its own clip, and the
   words cascade after one another; the footer pieces follow, each on its
   own step. Everything is keyed off the .mobnav-open class the toggle
   already sets, so opening and closing share one source of truth and
   nothing has to be measured. */
.mobnav__links a {
  overflow: hidden;
}
/* The wrapper only — a bare `a span` also matches the letters inside it,
   and pinning their transform to none stopped every letter from rising. */
.mobnav__links a > span[class],
.mobnav__links a > span {
  display: inline-block;
  transform: none !important;
  clip-path: none;
  opacity: 1;
}
/* Splitting a word into one inline-block per character drops every kerning
   pair, which spread "Expertise" 5.5px wider than the unsplit text — about
   .018em a character. The base tracking is -.02em, so the two are carried
   together here to put the pairs back. homepage-bundle pins
   `.mobnav__links a span` to -.02em with !important and the letters are
   spans too, so this needs an id and the same weight to land. */
#mobnav .mobnav__links a .mobnav__letter[class] {
  letter-spacing: -.038em !important;
}

/* Closed and open states are stated at the SAME specificity, so the open
   rule can win on order alone. (Raising only the closed rule for the
   tracking fix above once pinned every letter down permanently.) */
#mobnav .mobnav__links a .mobnav__letter {
  display: inline-block;
  transform: translateY(110%);
  transition: transform var(--dur-4) var(--showcase-ease);
  will-change: transform;
}
.mobnav-open #mobnav .mobnav__links a .mobnav__letter {
  transform: translateY(0);
  transition-delay: var(--letter-delay, 0ms);
}

.mobnav__foot[class] {
  opacity: 1 !important;
}
.mobnav__foot > * {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-3) ease,
    transform var(--dur-3) var(--showcase-ease);
}
.mobnav-open .mobnav__foot > * {
  opacity: 1;
  transform: none;
}
.mobnav-open .mobnav__foot > *:nth-child(1) { transition-delay: .52s; }
.mobnav-open .mobnav__foot > *:nth-child(2) { transition-delay: .59s; }
.mobnav-open .mobnav__foot > *:nth-child(3) { transition-delay: .66s; }
.mobnav-open .mobnav__foot > *:nth-child(4) { transition-delay: .73s; }

@media (prefers-reduced-motion: reduce) {
  .mobnav__letter,
  .mobnav__links a span,
  .mobnav__foot > * {
    transition: none !important;
    transform: none !important;
    opacity: 1 !important;
  }
}

/* The menu footer is uppercased as a block; the address is a literal and
   has to read exactly as it is typed. */
#mobnav .mobnav__foot a[href^="mailto:"] {
  text-transform: none;
}

/* =====================================================================
   6 · MOBILE MENU SURFACE — light frost in light mode

   Dark mode keeps exactly what it has: rgba(12,13,18,.72) behind a blur.
   Light mode gets a light frost instead — but the menu's type is white,
   and white on a light frost over a light page has no contrast at all.
   So the panel is built in two layers: the frost itself, and a soft
   darkening veil above it that the text sits on. The result still reads
   as frosted glass while keeping the labels legible.
   ===================================================================== */
:root:not([data-theme="dark"]) .mobnav {
  background: rgba(252, 252, 253, .55);
  -webkit-backdrop-filter: blur(26px) saturate(1.35) !important;
  backdrop-filter: blur(26px) saturate(1.35) !important;
}
:root:not([data-theme="dark"]) .mobnav::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* Denser at the top and bottom, where the links and the footer sit. */
  background: linear-gradient(
    to bottom,
    rgba(16, 17, 20, .64) 0%,
    rgba(16, 17, 20, .46) 45%,
    rgba(16, 17, 20, .72) 100%
  );
}
/* The footer's muted greys were tuned against a near-black panel; on this
   lighter one they need lifting to stay readable. */
:root:not([data-theme="dark"]) .mobnav__foot,
:root:not([data-theme="dark"]) .mobnav__foot span {
  color: rgba(255, 255, 255, .82);
}
:root:not([data-theme="dark"]) .mobnav__foot a:not(.mobnav__talk) {
  color: rgba(255, 255, 255, .94);
}
/* The tray's own children have to sit above that veil. */
:root:not([data-theme="dark"]) .mobnav > * {
  position: relative;
  z-index: 1;
}

/* =====================================================================
   7 · CARD TITLES AND INDICES — full white in dark mode

   The process cards carry an inactive/active split from the old pinned
   story: closed steps dimmed their title to --text-secondary and their
   numeral to 28% white. With every card now reading as a card rather
   than as a step in a filmstrip, that dimming just looked like grey type.
   Titles and numerals are the card's identity — they take full white.

   Selectors carry an id plus the theme attribute because the rules being
   corrected are themselves id-scoped and !important.
   ===================================================================== */
:root[data-theme="dark"] body #process .design-process__step .design-process__copy h3,
:root[data-theme="dark"] body #process .design-process__step:not(.is-active) .design-process__copy h3,
:root[data-theme="dark"] body #process .design-process__step .design-process__number,
:root[data-theme="dark"] body #process .design-process__step:not(.is-active) .design-process__number {
  color: #ffffff !important;
  opacity: 1 !important;
}

/* The same treatment wherever a numbered or titled card appears — the
   Experience and Education rows, the stat plates, the partnership
   attributions, and the case-study pages, which reuse these patterns. */
:root[data-theme="dark"] body #experience .exp__role,
:root[data-theme="dark"] body #education-recognition .exp__role,
:root[data-theme="dark"] body #experience .counter__value,
:root[data-theme="dark"] body #partnerships .partnership-card.is-active figcaption strong {
  color: #ffffff !important;
}

/* =====================================================================
   8 · WORK ON MOBILE — a plain list, quietly staggered

   The phone card used to be a dark plate with the copy floating on a
   blurred glass panel pulled up over the image. Stripped back: the media
   sits on the page, the words sit under it, and nothing is drawn around
   them. Each card arrives as it is scrolled to, and its own pieces follow
   one after another.
   ===================================================================== */
@media (max-width: 900px) {
  .sw-mobile-cards[class] {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(38px, 9vw, 54px) !important;
    margin-top: clamp(28px, 7vw, 40px);
  }

  .sw-mobile-card[class] {
    position: relative;
    display: grid;
    gap: 14px;
    overflow: visible;
    min-height: 0;
    background: none;
    color: inherit;
    -webkit-tap-highlight-color: transparent;
  }
  .sw-mobile-card[class]:active { transform: none !important; }

  .sw-mobile-card__media[class] {
    position: relative;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    /* Rounded, in line with the rest of the page's media. */
    border-radius: var(--radius-media, 16px) !important;
    background: var(--showcase-muted-bg);
  }
  /* The scrim existed to keep the old overlaid copy legible; the copy is
     out from under the image now, so the image is left alone. */
  .sw-mobile-card__media[class]::after { content: none !important; }

  .sw-mobile-card__copy[class] {
    position: static;
    display: grid;
    gap: 7px;
    margin: 0 !important;
    padding: 0 !important;
    overflow: visible;
    isolation: auto;
  }
  .sw-mobile-card__copy[class]::before { content: none !important; }

  .sw-mobile-card__category[class] {
    order: -1;
    color: var(--text-tertiary, #8e8e8e);
    font-size: var(--fs-small, .75rem);
    font-weight: 500;
    letter-spacing: .04em;
    text-transform: uppercase;
  }
  .sw-mobile-card strong[class],
  .sw-mobile-card strong {
    color: var(--text-primary, #141414) !important;
    font-size: clamp(1.18rem, 5.2vw, 1.5rem);
    font-weight: 500;
    line-height: 1.2;
    letter-spacing: -.03em;
  }
  .sw-mobile-card__description[class] {
    color: var(--text-secondary, #6e6e6e);
    font-size: clamp(.92rem, 3.9vw, 1rem);
    line-height: 1.5;
  }

  /* ---- Arrival --------------------------------------------------------
     The card lifts as a whole; inside it the media, then the category,
     title and description follow on their own steps. --card-index is set
     per card by js/mobile-linear-experience.js. */
  .sw-mobile-card > * {
    opacity: 0;
    transform: translateY(18px);
    transition: opacity var(--dur-3) ease,
      transform var(--dur-4) var(--showcase-ease);
  }
  .sw-mobile-card__media {
    transform: translateY(22px) scale(.985);
    transition: opacity var(--dur-3) ease,
      transform var(--dur-4) var(--showcase-ease);
  }
  .sw-mobile-card.is-in > * {
    opacity: 1;
    transform: none;
  }
  .sw-mobile-card.is-in .sw-mobile-card__media { transition-delay: 0ms; }
  .sw-mobile-card.is-in .sw-mobile-card__copy { transition-delay: 110ms; }
  .sw-mobile-card.is-in .sw-mobile-card__copy > * {
    opacity: 0;
    transform: translateY(12px);
    animation: sw-card-piece var(--dur-4) var(--showcase-ease) forwards;
  }
  .sw-mobile-card.is-in .sw-mobile-card__copy > *:nth-child(1) { animation-delay: 150ms; }
  .sw-mobile-card.is-in .sw-mobile-card__copy > *:nth-child(2) { animation-delay: 215ms; }
  .sw-mobile-card.is-in .sw-mobile-card__copy > *:nth-child(3) { animation-delay: 280ms; }

  @keyframes sw-card-piece {
    to { opacity: 1; transform: none; }
  }

  @media (prefers-reduced-motion: reduce) {
    .sw-mobile-card > *,
    .sw-mobile-card__media,
    .sw-mobile-card.is-in .sw-mobile-card__copy > * {
      opacity: 1 !important;
      transform: none !important;
      transition: none !important;
      animation: none !important;
    }
  }

  /* The pills read as plain text filters rather than chips. */
  #work .sw-pill[class] {
    background: none !important;
    border: 0 !important;
  }
}

/* =====================================================================
   9 · CARD → CASE STUDY

   The cover already claims `view-transition-name: case-study-cover` on
   both ends, but a cross-document transition only runs if BOTH documents
   opt in — without this the browser hard-cuts and the name does nothing.
   ===================================================================== */
@view-transition { navigation: auto; }

::view-transition-group(case-study-cover) {
  animation-duration: var(--dur-4);
  animation-timing-function: var(--ease-out);
}
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--dur-3);
}

/* =====================================================================
   10 · EXPERTISE FILTERS — read as controls, not as a caption

   The tabs already switch the cover, description and tag list on click
   (js/expertise.js owns that). What they did not do is LOOK tappable:
   plain text in a row that ran off the edge, with the active one only a
   shade darker. They are chips now, the active one inverted, in a strip
   that snaps and shows there is more to the right.
   ===================================================================== */
@media (max-width: 900px) {
  #expertise .xp__tabs[class] {
    margin-bottom: clamp(18px, 4vw, 26px);
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    /* The row is wider than the screen: fading the trailing edge says so
       without adding a control that would need its own explanation. */
    -webkit-mask-image: linear-gradient(to right, #000 86%, transparent 100%);
  }
  #expertise .xp__tabs::-webkit-scrollbar { display: none; }

  #expertise .xp__tab[class][class] {
    scroll-snap-align: start;
    display: inline-flex;
    align-items: center;
    background: transparent !important;
    transition: background-color var(--dur-2) var(--showcase-ease),
      border-color var(--dur-2) var(--showcase-ease),
      color var(--dur-2) var(--showcase-ease);
  }

  #expertise .xp__tab[class][class].is-active,
  #expertise .xp__tab[class][class][aria-selected="true"] {
    border-color: var(--text-primary, #141414) !important;
    background: var(--text-primary, #141414) !important;
  }
  :root[data-theme="dark"] #expertise .xp__tab[class][class].is-active,
  :root[data-theme="dark"] #expertise .xp__tab[class][class][aria-selected="true"] {
    background: #ffffff !important;
    border-color: #ffffff !important;
  }
}

/* =====================================================================
   11 · PLAYGROUND TRACKS ON MOBILE — a row, not a crowded square

   In the "All" grid the two music tiles collapse to ~163px squares, and
   inside each one a 38px numeral, a ring canvas, a 44px play control and
   a three-line meta block all stack on the same centre — the title runs
   straight through the play button. On a phone they become list rows
   instead: art on the left, words in the middle, play on the right, each
   with its own column and nothing overlapping.
   ===================================================================== */
@media (max-width: 900px) {
  /* One column, so every tile is a full-width block. */
  #perspective #pgPanelAll[class] {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    /* Shares the Playground-wide minimal gutter (css/playground-all.css). */
    gap: var(--pg-gutter, var(--grid-gap)) !important;
  }
  #perspective #pgPanelAll > [class] {
    grid-column: 1 / -1;
    grid-row: auto;
  }

  /* The two tracks keep the card they get on the web — gradient field,
     oversized index, centred play, meta at the foot — rather than being
     rebuilt as compact list rows.

     What stood here turned each card into a three-column
     "art meta play" grid: the canvas shrank to a 76px thumbnail, the index
     dropped to a small label beside the words, the meta went left-aligned
     and the play button moved to the right edge. It read as a different
     component from the Music tab one tab across, and it threw away the
     gradient, which IS the artwork. Only the aspect is set here, since
     without a height a card with absolutely-positioned internals collapses.

     Play/pause behaviour is untouched either way — it runs off .is-playing
     on the same button. */
  #perspective #pgPanelAll .pg-track[class][class] {
    aspect-ratio: 3 / 4;
    min-height: 0;
  }
}

/* =====================================================================
   12 · WORK — parallax scale-in (paired with js/work-parallax.js)

   The script writes four progress values, 0 → 1, as the hero's tail
   scrolls past its second statement. Everything here is transform and
   opacity only, so the whole reveal composites without a single layout
   pass. Nothing is hidden unless the script is live: `.has-work-parallax`
   on <html> gates the rest, so if the file never loads the section
   renders exactly as it does now.
   ===================================================================== */
.has-work-parallax #work .sw__title,
.has-work-parallax #work .sw__support {
  transform:
    translate3d(0, calc((1 - var(--w-title, 1)) * 46px), 0)
    scale(calc(.945 + .055 * var(--w-title, 1)));
  transform-origin: 0% 50%;
  opacity: var(--w-title, 1);
}
.has-work-parallax #work .sw__pills {
  transform:
    translate3d(0, calc((1 - var(--w-pills, 1)) * 34px), 0)
    scale(calc(.965 + .035 * var(--w-pills, 1)));
  opacity: var(--w-pills, 1);
}
/* The cover carries the most travel — it is the thing the eye lands on. */
.has-work-parallax #work .sw__center {
  transform:
    translate3d(0, calc((1 - var(--w-cover, 1)) * 58px), 0)
    scale(calc(.90 + .10 * var(--w-cover, 1)));
  opacity: var(--w-cover, 1);
}
.has-work-parallax #work .sw__list,
.has-work-parallax #work .sw__pager {
  transform:
    translate3d(0, calc((1 - var(--w-side, 1)) * 26px), 0)
    scale(calc(.975 + .025 * var(--w-side, 1)));
  opacity: var(--w-side, 1);
}
/* Left and right lists lean in from their own side, which reads as depth
   rather than as one block sliding. */
.has-work-parallax #work .sw__list--left {
  transform:
    translate3d(calc((1 - var(--w-side, 1)) * -22px), calc((1 - var(--w-side, 1)) * 26px), 0)
    scale(calc(.975 + .025 * var(--w-side, 1)));
}
.has-work-parallax #work .sw__list--right {
  transform:
    translate3d(calc((1 - var(--w-side, 1)) * 22px), calc((1 - var(--w-side, 1)) * 26px), 0)
    scale(calc(.975 + .025 * var(--w-side, 1)));
}

/* Compositor hints exist only while the reveal is in flight. */
#work.is-work-parallax .sw__title,
#work.is-work-parallax .sw__support,
#work.is-work-parallax .sw__pills,
#work.is-work-parallax .sw__center,
#work.is-work-parallax .sw__list,
#work.is-work-parallax .sw__pager {
  will-change: transform, opacity;
}

@media (prefers-reduced-motion: reduce) {
  .has-work-parallax #work :is(.sw__title, .sw__support, .sw__pills, .sw__center, .sw__list, .sw__pager) {
    transform: none !important;
    opacity: 1 !important;
  }
}

/* =====================================================================
   13 · TESTIMONIALS ON TOUCH — one card per swipe

   The deck is already a horizontal scroller, so a swipe is the native
   gesture; these rules make it land cleanly. `mandatory` snapping plus a
   card-width column means a flick always settles on a card rather than
   between two, and the page keeps its own vertical scroll because only
   the X axis is contained.
   ===================================================================== */
@media (hover: none), (pointer: coarse) {
  #partnerships .partnerships__cards[class] {
    scroll-snap-type: x mandatory !important;
    scroll-padding-inline: 0;
    -webkit-overflow-scrolling: touch;
    /* Horizontal only: a vertical drag still scrolls the page. */
    touch-action: pan-x pan-y;
    overscroll-behavior-x: contain;
  }
  #partnerships .partnership-card[class] {
    scroll-snap-align: center !important;
    scroll-snap-stop: always;
  }
}

/* Snap is suspended while the pointer is steering the deck from an edge;
   js/showcase-sections.js toggles this. Without it, `mandatory` snapping
   cancels every incremental scroll the steering applies. */
#partnerships .partnerships__cards.is-steering[class] {
  scroll-snap-type: none !important;
}

/* =====================================================================
   14 · WORK — text arrives piece by piece

   js/work-parallax.js splits the title into letters and the support line
   into words, each behind its own clip, and stamps a --wi on every piece:
   the point in its group's progress where that piece starts. The local
   value is worked out here rather than in script, so the scroll writes
   four numbers a frame and nothing else — the per-piece maths happens in
   the style engine.
   ===================================================================== */
.has-work-parallax #work .work-piece {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  /* Each letter becomes its own clipping box, and at this tracking the
     glyphs are wider than their advance — so the clip was shaving the
     edges. The box is widened by padding and the width given back with an
     equal negative margin, which keeps the line's measure unchanged. */
  /* The clip would shave a glyph's overhang at this tracking, so the box
     is padded and the width given straight back with a negative margin —
     the line measures exactly what the unsplit text does. */
  padding-inline: .09em;
  margin-inline: -.09em;
  /* --wi is this piece's start; the slice runs from there to the end of
     the group's window, so late pieces still finish on time. */
  --wt: clamp(0, calc((var(--w-title, 1) - var(--wi, 0)) / (1 - var(--wi, 0))), 1);
}
.has-work-parallax #work .work-piece__inner {
  display: inline-block;
  transform: translateY(calc((1 - var(--wt)) * 105%));
}
.has-work-parallax #work .sw__support .work-piece {
  --wt: clamp(0, calc((var(--w-title, 1) - var(--wi, 0)) / (1 - var(--wi, 0))), 1);
}
.has-work-parallax #work .sw__support .work-piece__inner {
  transform: translateY(calc((1 - var(--wt)) * 115%));
  opacity: var(--wt);
}

/* The block-level lift is now the pieces' job — the containers only fade,
   so the two movements do not compound into a double slide. */
.has-work-parallax #work .sw__title,
.has-work-parallax #work .sw__support {
  transform: none;
  opacity: 1;
}

/* Project rows land one after another on the same offsets. */
/* Rows stagger on transform ONLY. Their opacity already carries meaning —
   the Work controller dims every row but the active one — so driving it
   from here would flatten that state as the reveal finished. The fade is
   left to the list container. */
/* The Work controller already writes a transform on these rows, so the
   stagger has to out-weigh it rather than sit beside it. */
.has-work-parallax #work .sw__list .sw-li[class] {
  --wt: clamp(0, calc((var(--w-side, 1) - var(--wi, 0)) / (1 - var(--wi, 0))), 1);
  transform: translate3d(0, calc((1 - var(--wt)) * 18px), 0) !important;
}

#work.is-work-parallax .work-piece__inner {
  will-change: transform, opacity;
}
#work.is-work-parallax .sw-li {
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .has-work-parallax #work .work-piece__inner,
  .has-work-parallax #work .sw-li {
    transform: none !important;
  }
  .has-work-parallax #work .work-piece__inner { opacity: 1 !important; }
}

/* =====================================================================
   15 · NAV JUMPS LAND ON A SETTLED PAGE

   While `is-nav-jumping` is set (js/main.js sets it for the duration of
   the scroll), every reveal is forced to its finished state with its
   transition switched off — otherwise the pieces animate to that state
   during the travel and the whole trip reads as a flicker. The classes
   that make it permanent are applied by js/showcase-sections.js, so
   nothing snaps back when the flag clears.
   ===================================================================== */
:root.is-nav-jumping body .reveal-part,
:root.is-nav-jumping body #work .work-piece__inner,
:root.is-nav-jumping body #work .sw-li,
:root.is-nav-jumping body .sw-mobile-card > *,
:root.is-nav-jumping body .sw-mobile-card__copy > * {
  opacity: 1 !important;
  transform: none !important;
  clip-path: none !important;
  transition: none !important;
  animation: none !important;
}

/* The Work groups are variables, not transitions — pinning them here as
   well means the section is at rest even before the script's write lands
   on the very first frame of the jump. */
:root.is-nav-jumping body #work {
  --w-title: 1;
  --w-pills: 1;
  --w-cover: 1;
  --w-side: 1;
}

/* Dark mode paints every card surface from --surface-card through an
   id-scoped !important rule in homepage-bundle, which out-ranks the deck's
   own background above — so the focused card was the same grey as its
   neighbours. Stated here at matching weight so the pair separates the way
   it does in light mode. */
:root[data-theme="dark"] body #partnerships .partnership-card[class][class] {
  background: var(--showcase-muted-bg) !important;
}
:root[data-theme="dark"] body #partnerships .partnership-card.is-active[class][class] {
  background: var(--showcase-active-bg) !important;
}
