@import './tokens.css';

/* ══════════════════════════════════════
   PUBLIC SANS — the single typeface used site-wide.
   Variable fonts, so the two files below cover the whole 100–900
   weight range (plus italics) rather than one file per weight.
══════════════════════════════════════ */
@font-face {
  font-family: 'Public Sans';
  src: url('../assets/fonts/PublicSans-Variable.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* EB Garamond — titles only. The italic face is declared but only ever
   downloaded if something italic actually renders in it. */
@font-face {
  font-family: 'EB Garamond';
  src: url('../assets/fonts/EBGaramond-Variable.woff2') format('woff2');
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}

/* Instrument Serif — italic only, used for a single card's display title.
   Self-hosted like the rest; SIL OFL, licence alongside the file. Only the
   latin subset is vendored, which is all the title needs. */
@font-face {
  font-family: 'Instrument Serif';
  src: url('../assets/fonts/InstrumentSerif-Italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

/* Form controls don't inherit font-family from their parent by default —
   they fall back to a UA font (Arial here). Opt them back in so no control
   escapes the site typeface. */
button, input, select, textarea, optgroup {
  font-family: inherit;
}

/* ── Disable image dragging globally ── */
img {
  -webkit-user-drag: none;
  user-drag: none;
  -khtml-user-drag: none;
  -moz-user-drag: none;
  -o-user-drag: none;
  user-select: none;
  pointer-events: auto;
  draggable: false;
}

/* ── Custom cursor — native cursor hidden, DOM shape takes over ── */
/* Only hide on wide screens with a fine pointer (mouse/trackpad) */
@media (pointer: fine) and (min-width: 769px) {
  *, *::before, *::after {
    cursor: none !important;
  }
}

/* ── Cursor shape (CursorShape.js) — morph driven by rAF in JS ── */
.cs-root {
  position: fixed;
  top: 0; left: 0;
  pointer-events: none;
  z-index: 10001;
}

/* ── Cursor tooltip ── */
.cursor-tooltip {
  position: fixed;
  pointer-events: none;
  z-index: 9999;
  background: var(--color-primary);
  opacity: 0.9;
  border-radius: 8px;
  padding: 8px 16px;
  color: var(--color-text-on-primary);
  font-family: var(--font-family-primary);
  font-size: var(--tooltip-size);
  font-weight: var(--tooltip-weight);
  line-height: var(--tooltip-leading);
  white-space: pre-line;
  /* Fade in on first appearance */
  animation: tooltip-in 0.12s ease forwards;
}

@keyframes tooltip-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 0.85; transform: translateY(0); }
}

/* ── Reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
img { display: block; }

/* ── Body / background ── */
body {
  min-height: 100vh;
  background: var(--color-bg-body);
  font-family: var(--font-family-primary);
  color: var(--color-text-primary);
  transition: background 0.5s ease, color 0.5s ease;
}

/* ── Global semibold → gradient ── */
strong, b {
  font-weight: var(--weight-semibold);
  background: var(--gradient-brand);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

/* ── Back to top ── */
.back-to-top {
  position: fixed;
  bottom: 32px;
  right: 32px;
  z-index: 500;
  width: 40px;
  height: 40px;
  border-radius: 100px;
  border: none;
  background: var(--color-surface-action-icon);
  color: var(--color-text-primary);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 40px var(--color-shadow-action-icon);
  opacity: 0;
  transform: scale(0.8);
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease, background 0.5s ease;
  filter: var(--filter-action-icon);
}

.back-to-top--visible {
  opacity: 1;
  transform: scale(1);
  pointer-events: auto;
}

.back-to-top:hover {
  transform: scale(1.08);
}

.back-to-top:active {
  transform: scale(0.95);
}

/* ── Navigation ── */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  padding: 20px 48px;
  transform: translateY(0);
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}

.nav--hidden {
  transform: translateY(-110%);
}

.nav-logo {
  width: 64px;
  height: 64px;
  flex-shrink: 0;
}

.nav-pills {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 4px;
  background: var(--color-surface-nav-blur);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  padding: 4px;
  border-radius: 100px;
  transition: background 0.5s ease;
}

/* ── Hover ghost pill (iPad-like cursor replacement) ── */
.nav-pill-hover-ghost {
  position: absolute;
  top: 4px;
  bottom: 4px;
  border-radius: 100px;
  background: var(--color-surface-nav);
  opacity: 0;
  pointer-events: none;
  transition: left 0.25s cubic-bezier(0.25, 1, 0.5, 1),
              width 0.25s cubic-bezier(0.25, 1, 0.5, 1),
              opacity 0.18s ease;
  box-shadow: 0 0 12px var(--color-shadow-nav);
  /* Much fainter than the active indicator */
  filter: opacity(0.35);
}

/* Sliding pill indicator */
.nav-pill-indicator {
  position: absolute;
  top: 4px;
  bottom: 4px;
  border-radius: 100px;
  background: var(--color-surface-nav);
  box-shadow: 0 0 40px var(--color-shadow-nav);
  pointer-events: none;
  will-change: left, width, transform;
  transform-origin: center center;
  /* transition added only after initial placement */
}

.nav-pill-indicator--ready {
  transition: left 0.62s cubic-bezier(0.34, 1.56, 0.64, 1),
              width 0.62s cubic-bezier(0.34, 1.56, 0.64, 1),
              transform 0.48s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Liquid-glass: looser spring while stretching mid-flight */
.nav-pill-indicator--liquid {
  transition: left 0.72s cubic-bezier(0.22, 1, 0.36, 1),
              width 0.72s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
}

.nav-pill {
  position: relative; /* sit above the indicator */
  z-index: 1;
  padding: 8px 16px;
  border-radius: 100px;
  font-family: var(--font-family-primary);
  font-weight: 500;
  font-size: 16px;
  color: var(--color-text-primary);
  text-decoration: none;
  transition: color 0.2s;
}

/* Hide cursor inside pill area — the ghost pill IS the cursor */
.nav-pills:hover {
  cursor: none;
}
.nav-pill {
  cursor: none;
  user-select: none;
  -webkit-user-select: none;
}

/* Active text stays dark (indicator handles the background) */
.nav-pill.active {
  color: var(--color-text-primary);
}

/* Hover — teal text, no pill (ghost does the pill job) */
.nav-pill:not(.active):hover {
  color: var(--color-primary);
}

/* ── Lazy-mode toggle ── */

/* Press-down — matches bento-card :active feel */

/* Crossfade label during icon morph */

/* ── Mobile-only page logo (above the grid) ── */
.mobile-page-logo {
  display: none; /* hidden on desktop */
}

/* ── Main layout ── */
.main {
  display: flex;
  justify-content: center;
  padding: 110px 24px 80px;
}

/* ── Bento grid ──
   Fluid between the mobile breakpoint (768px) and the 1062px design width
   (4 × 252px columns + 3 × 18px gaps), so there's no dead zone where a
   fixed-width grid would overflow the viewport. Caps at 1062px and centers
   (via .main's justify-content) on anything wider, matching the original
   fixed-width desktop design at 1440px+. */
.grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* 252px = the column width at max-width: (1062 - 3 gaps of 18) / 4, so
     single-slot cards are square.
     This was previously minmax(calc((100% - 54px) / 4), auto). A percentage in
     grid-auto-rows resolves against the grid's own HEIGHT, not its width — so
     row height depended on how many rows there were. Removing three cards made
     the grid shorter, which made every row shorter (252 → 162.5px) and squashed
     every card on the page. A fixed value is what the file's own header comment
     always described: "auto rows 252 px". */
  grid-auto-rows: minmax(252px, auto);
  gap: 18px;
  width: 100%;
  max-width: 1062px;
  /* Breathing room below the last row, before the page padding. */
  margin-bottom: 32px;
}

/* ══════════════════════════════════════
   BENTO CARD (reusable base)
══════════════════════════════════════ */
.bento-card {
  background: var(--color-surface-card);
  border-radius: var(--radius-card);
  border: 1px solid var(--color-border-card);
  box-shadow:
    0 1px 3px var(--color-shadow-card-rest),
    0 1px 2px var(--color-shadow-card-rest),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
  overflow: hidden;
  position: relative;
  cursor: pointer;
  transition: background 0.5s ease, box-shadow 0.3s ease, border-color 0.3s ease, transform 0.15s ease;
  /* Fix Chrome bug: overflow:hidden + border-radius distortion */
  isolation: isolate;
  /* Staggered enter animation — delay overridden per card below */
  animation: card-enter 0.8s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  animation-delay: var(--card-enter-delay, 0s);
}

/* Grain texture layer — multiply on light, screen on dark */
.bento-card::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E");
  background-size: 180px 180px;
  opacity: var(--card-grain-opacity);
  mix-blend-mode: var(--card-grain-blend);
  pointer-events: none;
  z-index: 0;
  transition: opacity 0.5s ease;
}

.bento-card:hover {
  box-shadow:
    0 12px 40px var(--color-shadow-card-hover),
    0 4px 12px var(--color-shadow-card-hover-2),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
  transform: translateY(-3px);
}

.bento-card:active,
.bento-card--pressed {
  transform: scale(0.97);
  box-shadow:
    0 1px 3px var(--color-shadow-card-rest),
    0 1px 2px var(--color-shadow-card-rest),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
  transition: background 0.5s ease, box-shadow 0.08s ease, border-color 0.3s ease, transform 0.08s ease;
}

/* ── Action icon ── */
.action-icon {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 40px;
  height: 40px;
  background: var(--color-surface-action-icon);
  border-radius: 100px;
  box-shadow: 0 2px 40px var(--color-shadow-action-icon);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transform: scale(0.8);
  transition: opacity 0.2s ease, transform 0.2s ease, background 0.5s ease;
  z-index: 10;
}

/* Higher specificity so card-level img rules can't override it */
.bento-card .action-icon img,
.ai-card .action-icon img,
.vf-card .action-icon img {
  width: 16px !important;
  height: 16px !important;
  transform: none !important;
  filter: var(--filter-action-icon);
}

/* Lottie toggle inside the bulb-card action icon */
.bulb-card .action-icon .bulb-lottie {
  width: 22px;
  height: 22px;
  pointer-events: none;
}
.bulb-card .action-icon .bulb-lottie svg {
  display: block;
  filter: var(--filter-action-icon);
}

.bento-card:hover .action-icon {
  opacity: 1;
  transform: scale(1);
}

/* ══════════════════════════════════════
   CARD ENTER ANIMATION
══════════════════════════════════════ */
@keyframes card-enter {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Stagger delays — reading order: left→right, top→bottom, 0.1s apart */
/* Row 1 */
.about-card-wrapper { --card-enter-delay: 0s; }    /* cols 1-2 */
.gmail-card         { --card-enter-delay: 0.1s; }  /* col 3    */
.linkedin-card      { --card-enter-delay: 0.2s; }  /* col 4    */
/* Row 2 */
.ai-card        { --card-enter-delay: 0.5s; }  /* cols 3-4 */
/* Row 3 */
.uq-card            { --card-enter-delay: 0.6s; }  /* cols 1-2 */
/* Row 4 */
.ds-quote-card       { --card-enter-delay: 0.7s; }  /* cols 3-4 */
/* Row 5 */
.ux-quote-card       { --card-enter-delay: 0.8s; }  /* cols 1-2 */
.vf-card         { --card-enter-delay: 0.9s; }  /* cols 3-4 */
/* Row 6 */  /* cols 1-2 */
/* Row 7 */  /* cols 3-4 */
/* Row 9 */
.bulb-card          { --card-enter-delay: 1.3s; }  /* col 1, trailing */

/* ══════════════════════════════════════
   GRID SLOT (wrapper div per card)
   Grid placement is now driven by inline
   styles from filterLayouts.js via App.js.
══════════════════════════════════════ */
.grid-slot {
  min-width: 0;
  min-height: 0;
  transition: opacity 0.4s ease;
}

/* Cards inside a slot fill the slot area */
.grid-slot > * {
  width: 100%;
  height: 100%;
}

/* De-prioritised cards at 30 % opacity */
.grid-slot--dim {
  opacity: 0.3;
  pointer-events: none;
}

/* ══════════════════════════════════════
   GRID PLACEMENT (legacy — kept for
   reference; overridden by inline styles
   on .grid-slot wrappers)
══════════════════════════════════════ */
/* .about-card-wrapper { grid-column: 1 / 3; grid-row: 1; }
.bulb-card     { grid-column: 1;     grid-row: 2; }
.gmail-card    { grid-column: 3;     grid-row: 1; }
.linkedin-card { grid-column: 4;     grid-row: 1; }
.ai-card   { grid-column: 3 / 5; grid-row: 2 / 4; }
.uq-card       { grid-column: 1 / 3; grid-row: 3 / 5; }
.ux-quote-card  { grid-column: 1 / 3; grid-row: 5; }
.vf-card    { grid-column: 3 / 5; grid-row: 5 / 7; }
.ds-quote-card  { grid-column: 3 / 5; grid-row: 4; } */

/* ══════════════════════════════════════
   CARD CONTENT STYLES
══════════════════════════════════════ */

/* ── About card (collapsed) ── */
.about-card-wrapper {
  position: relative;   /* grid placement set in GRID PLACEMENT section */
}

.about-card-wrapper > .about-card {
  width: 100%;
  height: 100%;
}

.about-card {
  display: flex;
  align-items: center;
  padding-right: 28px;
  cursor: pointer;
  transition: background 0.5s ease, box-shadow 0.25s ease, border-color 0.3s ease, transform 0.15s ease, opacity 0.25s ease;
}
.about-avatar { width: 215px; height: 215px; object-fit: cover; flex-shrink: 0; }
.about-bio { font-size: var(--about-bio-size); line-height: var(--about-bio-leading); padding-left: 4px; }

/* Keep grid slot occupied, hide card while expanded */
.about-card--ghost { opacity: 0; pointer-events: none; }

/* Once the card has been interacted with, kill the entrance animation
   so it doesn't replay when returning from ghost state.
   Also snap opacity instantly (no transition) to avoid a flicker
   when the expanded overlay is removed on the same frame. */
.about-card.about-card--was-open {
  animation: none;
  transition: background 0.5s ease, box-shadow 0.25s ease, border-color 0.3s ease, transform 0.15s ease;
}

/* ── Backdrop — sits behind expanded card ── */
.about-backdrop {
  position: fixed;
  inset: 0;
  background: var(--color-backdrop);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  z-index: 200;
  animation: about-backdrop-in 0.3s ease forwards;
  touch-action: none;
}

@keyframes about-backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Backdrop fades out when closing */
.about-backdrop--out {
  animation: about-backdrop-out 0.45s ease forwards;
}

@keyframes about-backdrop-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* ── Expanded card — FLIP animated, above backdrop ── */
.about-expanded-card {
  position: fixed;
  background: var(--color-surface-card);
  border-radius: var(--radius-card);
  border: 1px solid var(--color-border-card);
  box-shadow:
    0 2px 200px var(--color-shadow-card-expanded),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
  z-index: 300;
  overflow: hidden;
  isolation: isolate;
  transition: background 0.5s ease, border-color 0.3s ease;
}

/* Grain texture — matches collapsed .bento-card */
.about-expanded-card::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E");
  background-size: 180px 180px;
  opacity: var(--card-grain-opacity);
  mix-blend-mode: var(--card-grain-blend);
  pointer-events: none;
  z-index: 0;
  transition: opacity 0.5s ease;
}

/* Transition fires once JS adds --settled (open) or keeps it during --closing */
.about-expanded-card--settled {
  transition:
    left         0.7s cubic-bezier(0.34, 1.1, 0.64, 1),
    top          0.7s cubic-bezier(0.34, 1.1, 0.64, 1),
    width        0.7s cubic-bezier(0.34, 1.1, 0.64, 1),
    height       0.7s cubic-bezier(0.34, 1.1, 0.64, 1),
    background   0.5s ease,
    border-color 0.3s ease;
}

/* Scrollable inner wrapper */
.about-expanded-inner {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 40px 80px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  scrollbar-width: thin;
  box-sizing: border-box;
}

/* Content fades in after growing, fades out instantly when closing */
.about-expanded-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  width: 100%;
  flex-shrink: 0;
  opacity: 0;
  transition: opacity 0.25s ease;
}

.about-expanded-card--settled .about-expanded-content {
  transition: opacity 0.3s ease 0.38s;
  opacity: 1;
}

/* Snap content out immediately so only the card shape shrinks back */
.about-expanded-card--closing .about-expanded-content {
  opacity: 0;
  transition: opacity 0.1s ease;
}

.about-expanded-avatar {
  width: 150px;
  height: 150px;
  object-fit: cover;
  border-radius: 50%;
  flex-shrink: 0;
}

/* Flying avatar — fixed, travels between collapsed and expanded positions */
.about-avatar-fly {
  position: fixed;
  object-fit: cover;
  z-index: 400;
  pointer-events: none;
}

.about-expanded-bio {
  font-size: var(--about-expanded-size);
  line-height: var(--about-expanded-leading);
  color: var(--color-text-body);
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
}

.about-expanded-bio p { margin: 0; }

/* Bio hyperlinks — animated underline (slide L→R in, R→L out, multi-line safe) */
.bio-link {
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: left bottom;
  background-size: 0% 1.5px;
  padding-bottom: 2px;
  transition: background-size 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

.bio-link:hover {
  background-size: 100% 1.5px;
}

/* ── Bio link preview (iOS-style peek) ── */
.bio-link-preview {
  position: fixed;
  z-index: 500;
  width: 320px;
  transform: scale(0.92);
  transform-origin: top left;
  opacity: 0;
  pointer-events: none;
  border-radius: 16px;
  overflow: hidden;
  box-shadow:
    0 20px 60px rgba(0, 0, 0, 0.25),
    0 8px 20px rgba(0, 0, 0, 0.12),
    0 0 0 1px rgba(0, 0, 0, 0.06);
  background: var(--color-surface-card);
  transition:
    opacity 0.25s cubic-bezier(0.2, 0, 0, 1),
    transform 0.25s cubic-bezier(0.2, 0, 0, 1);
  transform-origin: bottom center;
}

.bio-link-preview--visible {
  opacity: 1;
  transform: scale(1);
}

.bio-link-preview-img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  object-position: top left;
  background: var(--color-surface-card);
}

/* Shrink / close button */
.about-shrink-btn img {
  filter: var(--filter-action-icon);
}

.about-shrink-btn {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 40px;
  height: 40px;
  background: var(--color-surface-card-white);
  border: none;
  border-radius: 100px;
  box-shadow: 0 2px 40px var(--color-shadow-action-icon);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 1;
  flex-shrink: 0;
}

/* ── Case Study Overlay (shared fullscreen expand) ──────────── */

.cs-card-wrapper { position: relative; }
.cs-card-wrapper > .bento-card { width: 100%; height: 100%; }

/* Ghost — keep grid slot occupied while expanded */
.cs-card--ghost { opacity: 0; pointer-events: none; }
.cs-card--was-open { animation: none; }

/* ── Backdrop ── */
.cs-backdrop {
  position: fixed;
  inset: 0;
  background: var(--color-backdrop);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  z-index: 200;
  animation: cs-backdrop-in 0.3s ease forwards;
  touch-action: none;
}

@keyframes cs-backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.cs-backdrop--out {
  animation: cs-backdrop-out 0.45s ease forwards;
}

@keyframes cs-backdrop-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* ── Expanded fullscreen card — FLIP animated ── */
/* A deep-link open renders straight into the final state. The transition is
   killed outright rather than relied on to be a no-op: the element is created
   with the card's start geometry in the same frame, and any transition on
   width/height/top/left would animate away from it and register as layout
   shift. The class is dropped again the moment a close starts, so closing
   still animates. */
/* An open case study is the page: its own <h1> is the heading, so the home
   page's hidden one must stand down or the document has two h1s. */
.cs-overlay-open .home-title {
  display: none;
}

.cs-expanded--instant,
.cs-expanded--instant * {
  transition: none !important;
  animation: none !important;
}

.cs-expanded {
  position: fixed;
  background: var(--color-bg-body);
  border-radius: var(--radius-card);
  box-shadow: 0 2px 200px rgba(0, 0, 0, 0.01);
  z-index: 300;
  overflow: hidden;
  transition: background 0.5s ease;
}

.cs-expanded--settled {
  border-radius: 0;
  transition:
    left          0.7s cubic-bezier(0.34, 1.1, 0.64, 1),
    top           0.7s cubic-bezier(0.34, 1.1, 0.64, 1),
    width         0.7s cubic-bezier(0.34, 1.1, 0.64, 1),
    height        0.7s cubic-bezier(0.34, 1.1, 0.64, 1),
    border-radius 0.7s cubic-bezier(0.34, 1.1, 0.64, 1);
}

/* ── Header bar (logo left, close right) ── */
.cs-header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 40px calc(6.25% + 74px);
  z-index: 2;
  pointer-events: none;
}

.cs-header-close {
  pointer-events: auto;
}

.cs-header-close {
  width: 40px;
  height: 40px;
  background: var(--color-surface-action-icon);
  border: none;
  border-radius: 100px;
  box-shadow: 0 2px 40px var(--color-shadow-action-icon);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  transition: background 0.5s ease;
}

.cs-header-close img {
  filter: var(--filter-action-icon);
}

/* ── Scrollable inner area ── */
.cs-expanded-inner {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  box-sizing: border-box;
}

/* Content fades in after expand, fades out instantly on close */
.cs-expanded-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  flex-shrink: 0;
  opacity: 0;
  transition: opacity 0.25s ease;
  padding-bottom: 80px;
}

.cs-expanded--settled .cs-expanded-content {
  transition: opacity 0.3s ease 0.38s;
  opacity: 1;
}

.cs-expanded--closing .cs-expanded-content {
  opacity: 0;
  transition: opacity 0.1s ease;
}

/* ── Hero video in expanded view ── */
.cs-hero-video {
  display: block;
  border-radius: var(--radius-card);
  object-fit: cover;
  flex-shrink: 0;
  margin-top: 40px;
  transition: opacity 0.2s ease;
}

/* ── Back-to-top inside expanded case study ── */
.cs-back-to-top {
  position: absolute;
  bottom: 32px;
  right: 32px;
  z-index: 20;
  width: 40px;
  height: 40px;
  border-radius: 100px;
  border: none;
  background: var(--color-surface-action-icon);
  color: var(--color-text-primary);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 40px var(--color-shadow-action-icon);
  opacity: 0;
  transform: scale(0.8);
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease, background 0.5s ease;
  filter: var(--filter-action-icon);
  cursor: pointer;
}

.cs-back-to-top--visible {
  opacity: 1;
  transform: scale(1);
  pointer-events: auto;
}

.cs-back-to-top:hover {
  transform: scale(1.08);
}

.cs-back-to-top:active {
  transform: scale(0.95);
}

/* ── Flying video — fixed, travels between card and hero positions ── */
.cs-video-fly {
  position: fixed;
  object-fit: cover;
  z-index: 400;
  pointer-events: none;
}

/* ── Section nav — fixed bottom pill bar, section anchors +
   a single sweeping fill indicating overall reading progress.
   Sits in .cs-expanded's positioning context, same way .cs-back-to-top does. ── */
/* Bottom dock: expand/collapse-all, then the section nav. Flex rather than
   two absolutely-positioned elements, because the nav's width depends on how
   many sections a case study has — anchoring the toggle to a fixed offset
   would put it in a different place on every page. */
.cs-nav-dock {
  position: absolute;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 20;
  display: flex;
  /* stretch, not center: the toggle then takes the section nav's height
     automatically, so the pair stays level if the nav's type or padding ever
     changes. */
  align-items: stretch;
  gap: 8px;
  max-width: 94vw;
}

.cs-section-nav {
  position: relative;
  min-width: 0;
  max-width: min(88vw, 640px);
  border-radius: 100px;
  background: var(--color-surface-nav-blur);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  /* Same border + inset highlights as every white-background surface, so the
     dock reads as part of the same family. The wide ambient shadow is kept
     and the card's tight shadows are layered under it. */
  border: 1px solid var(--color-border-card);
  box-shadow:
    0 2px 40px var(--color-shadow-nav),
    0 1px 3px var(--color-shadow-card-rest),
    0 1px 2px var(--color-shadow-card-rest),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
  overflow: hidden;
  transition: background 0.5s ease, border-color 0.3s ease;
}

/* Expand/collapse-all. Same glass surface, radius and border treatment as
   the section nav it sits beside, so the pair reads as one control. */
.cs-expand-all {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  /* No vertical padding — height comes from the dock stretching this to match
     the section nav beside it. */
  padding: 0 16px;
  border-radius: 100px;
  background: var(--color-surface-nav-blur);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid var(--color-border-card);
  box-shadow:
    0 2px 40px var(--color-shadow-nav),
    0 1px 3px var(--color-shadow-card-rest),
    0 1px 2px var(--color-shadow-card-rest),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
  color: var(--color-text-primary);
  font-family: var(--font-family-body);
  font-size: 13px;
  font-weight: var(--weight-medium);
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.3s ease, border-color 0.3s ease, color 0.5s ease;
}

.cs-expand-all:hover {
  background: var(--color-surface-card-hover);
  border-color: var(--color-border-card-hover);
}

.cs-expand-all:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* Same CSS chevron as the collapsible headers, so the affordance matches
   the thing it operates. */
.cs-expand-all-chevron {
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.3s ease;
}

.cs-expand-all[aria-expanded="true"] .cs-expand-all-chevron {
  transform: translateY(2px) rotate(225deg);
}

/* Sweeping progress fill — width = % scrolled through the case study.
   Solid brand teal at low element-opacity so it reads clearly as a fill
   regardless of the light/dark glass background behind it. */
.cs-section-nav-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 0%;
  background: var(--color-primary);
  opacity: 0.18;
  pointer-events: none;
  transition: width 0.15s linear;
}

.cs-section-nav-track {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 6px;
  max-width: min(88vw, 640px);
  overflow-x: auto;
  scroll-behavior: smooth;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.cs-section-nav-track::-webkit-scrollbar {
  display: none;
}

.cs-section-pill {
  position: relative;
  flex-shrink: 0;
  padding: 8px 16px;
  border-radius: 100px;
  border: none;
  background: transparent;
  font-family: var(--font-family-primary);
  font-size: var(--action-size);
  font-weight: var(--action-weight);
  color: var(--color-text-subtle);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  transition: color 0.2s ease;
}
.cs-section-pill--active {
  color: var(--color-text-primary);
  font-weight: var(--weight-bold);
}
.cs-section-pill:not(.cs-section-pill--active):hover {
  color: var(--color-primary);
}

/* ── Case study body sections ── */
.cs-body {
  max-width: 680px;
  width: 100%;
  padding: 0 24px;
  margin-top: 80px;
}

.cs-title {
  font-family: var(--font-family-title);
  font-size: var(--cs-title-size);
  font-weight: var(--cs-title-weight);
  line-height: var(--cs-title-leading);
  letter-spacing: var(--cs-title-tracking);
  color: var(--color-text-primary);
  margin: 0 0 24px;
  transition: color 0.5s ease;
}

.cs-section-title {
  font-family: var(--font-family-title);
  font-size: var(--cs-section-size);
  font-weight: var(--cs-section-weight);
  line-height: var(--cs-section-leading);
  color: var(--color-text-primary);
  margin: var(--space-h2-top) 0 var(--space-h2-bottom);
  scroll-margin-top: 24px;
  transition: color 0.5s ease;
}

.cs-subsection-title {
  font-family: var(--font-family-title);
  font-size: var(--cs-subsection-size);
  font-weight: var(--cs-subsection-weight);
  line-height: var(--cs-subsection-leading);
  color: var(--color-text-primary);
  margin: var(--space-h3-top) 0 var(--space-h3-bottom);
  transition: color 0.5s ease;
}

.cs-body-text {
  font-family: var(--font-family-primary);
  font-size: var(--cs-body-size);
  font-weight: var(--cs-body-weight);
  line-height: var(--cs-body-leading);
  color: var(--color-text-body);
  margin: 0 0 var(--space-paragraph) 0;
  transition: color 0.5s ease;
}

/* Icon bullet list — icon badge replaces the plain dot marker. */
.cs-icon-list {
  display: flex;
  flex-direction: column;
  /* No gap. Each row carries equal padding above and below and the divider
     sits on the boundary, so every gap in the list is identical and the rule
     is centred in it. A gap here stacked on top of that padding and made the
     space above a divider differ from the space below it. */
  gap: 0;
  margin: 16px 0 24px 0;
  /* Half a row's padding, so the first and last rows sit the same distance
     from the container edge as rows sit from each other. */
  padding: 8px 24px;
  box-sizing: border-box;
}

/* The badge centres against the whole text block, so a bullet that wraps to
   several lines has its icon level with the middle of all of them. */
.cs-icon-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 0;
}

.cs-icon-row + .cs-icon-row {
  border-top: 1px solid var(--color-border-card);
  transition: border-color 0.3s ease;
}

/* Emoji, not SVG — the only icons on the site that are. Sizing is font-size
   rather than width/height, and the explicit emoji stack keeps the glyph from
   inheriting the body serif and shifting its metrics. */
.cs-icon-badge {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  line-height: 1;
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
}

.cs-icon-row-text {
  margin: 0;
}

/* Second cs-body block (Problem / Solution) — less top margin since it follows the antonym section */
.cs-body--continued {
  margin-top: var(--space-section);
}

/* Demo video inside case study body */
.cs-demo-video {
  display: block;
  width: 680px;
  /* No fixed aspect-ratio and no object-fit: the element takes the media's
     own natural ratio, so nothing is ever cropped. Width is fixed by the
     layout; height follows the video. */
  height: auto;
  border-radius: var(--radius-card);
  margin: 32px auto 0;
}

/* Chrome does not always clip a decoded video frame to the element's own
   border-radius (it can promote the video to a hardware overlay that
   ignores it), so the rounded corners have to come from a wrapper that
   establishes its own clipping context. */
.cs-video-frame {
  display: block;
  width: 680px;
  margin: 32px auto 0;
  border-radius: var(--radius-card);
  overflow: hidden;
  /* forces its own compositing layer so the clip is always applied */
  transform: translateZ(0);
  -webkit-mask-image: -webkit-radial-gradient(white, black);
  line-height: 0;
}

.cs-video-frame .cs-demo-video {
  width: 100%;
  margin: 0;
  border-radius: 0;
}

/* Before / After image toggle */
.ba-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  margin-top: 32px;
  width: 80vw;
  max-width: 80vw;
}
@keyframes ba-enter-right {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}
@keyframes ba-enter-left {
  from { transform: translateX(-100%); }
  to   { transform: translateX(0); }
}
@keyframes ba-exit-left {
  from { transform: translateX(0); }
  to   { transform: translateX(-100%); }
}
@keyframes ba-exit-right {
  from { transform: translateX(0); }
  to   { transform: translateX(100%); }
}

/* Cover image inside case study body */
/* A <picture> carrying .cs-cover-img gets the frame; the <img> inside just
   fills it. Without this the inner image keeps its intrinsic size and ignores
   the frame's width entirely. */
.cs-cover-img > img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: inherit;
}

.cs-cover-img {
  display: block;
  width: 680px;
  /* Required, not cosmetic. Figures carry width/height attributes so they
     reserve space before decoding; without height:auto the browser uses the
     height attribute literally against the CSS width and the image renders
     squashed rather than scaled. height:auto restores the intrinsic ratio,
     which is what keeps these uncropped. */
  height: auto;
  border-radius: var(--radius-card);
  margin: 32px auto 0;
}

/* ══════════════════════════════════════
   SHARED CARD SURFACE
   The exact treatment used by .bento-card on the home page, reused by every
   container in the article so they read as one family: same background,
   border, 4-layer shadow and 32px radius.
══════════════════════════════════════ */
.graph-chart,
.cs-collapse,
.cs-icon-list,
.cs-video-frame,
.ba-slider-frame{
  box-sizing: border-box;
  /* 20% of the card fill rather than the solid colour: these are sections
     inside an article, not cards on a page, so they read as a tint over the
     background. Border, radius and shadow are unchanged, so they still belong
     to the same family as the home-page cards. */
  background: var(--color-surface-card-soft);
  border: 1px solid var(--color-border-card);
  border-radius: var(--radius-card);
  box-shadow:
    0 1px 3px var(--color-shadow-card-rest),
    0 1px 2px var(--color-shadow-card-rest),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
  transition: background 0.5s ease, border-color 0.3s ease;
}

/* ── Media bleed inside a case-study body ─────────────────────────────
   .cs-body is 680px wide with 24px side padding, so its text sits in a
   632px column. Media is meant to be WIDER than that text column, which
   means `margin: auto` cannot centre it — per spec, once an element is
   wider than its containing block both auto margins resolve to 0 and it
   just overflows to the right (that's the "left aligned" bug).

   So the centring is done with negative side margins that cancel the
   padding instead. Widths are expressed relative to the parent rather
   than hard-coded, so they stay correct if .cs-body ever narrows.

     media : 632 + 48 = 680px  → flush with .cs-body's outer edges
     graph : 632 + 80 = 712px  → 16px past those edges on each side, so
                                 the bars inside its 16px padding measure
                                 680px,
.cs-body > .cs-video-frame,
.cs-body > .ba-slider,
.cs-body > .graph-chart,
.cs-body > .cs-icon-list {
  width: calc(100% + 80px);
  margin-left: -40px;
  margin-right: -40px;
}

/* ══════════════════════════════════════
   COLLAPSIBLE GROUP — folds several case-study sections behind one row
══════════════════════════════════════ */

/* Matches the graph container's footprint (the widest thing a case study
   can hold) and deliberately carries no horizontal padding, so a .cs-body
   nested inside keeps its own width/padding maths intact. */
.cs-collapse {
  /* The whole section toggles, so the whole surface is the affordance. */
  cursor: pointer;
  width: calc(680px + 2 * 16px);
  max-width: 100%;
  /* No bottom margin: `overflow: hidden` makes this a BFC, so its margins
     can't collapse with the next section's margin-top and the two would
     stack into a double gap. The following section supplies the spacing. */
  margin: 32px auto 0;
  overflow: hidden;               /* keeps the header's hover fill inside the radius */
}

.cs-collapse-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  padding: 24px;
  background: none;
  border: none;
  text-align: left;
  color: var(--color-text-primary);
  transition: background 0.2s ease, color 0.5s ease;
}

/* Hover belongs to the whole card, not just the title row.
   The background change alone was only legible while the group was closed:
   once open, the content inside (body copy blocks, graphs, image frames,
   icon lists) paints its own opaque card surfaces over it, so the only part
   that visibly reacted was the header strip. Lifting the shadow and the
   border reads along the whole outline instead, at any height, whatever is
   stacked inside. */
.cs-collapse {
  transition: background 0.5s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}

/* Two layers: the section's own 20% tint, with a dark wash on top. Using
   --color-surface-card-hover here meant hover jumped from a 20% tint to the
   SOLID card fill, which on a light page read as a white panel switching on.
   A wash keeps the tint and just darkens it a little. */
.cs-collapse:hover {
  background:
    linear-gradient(var(--surface-soft-hover-wash), var(--surface-soft-hover-wash)),
    var(--color-surface-card-soft);
  border-color: var(--color-border-card-hover);
  box-shadow:
    0 6px 20px var(--color-shadow-card-hover),
    0 2px 6px var(--color-shadow-card-hover-2),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
}

.cs-collapse-title {
  font-family: var(--font-family-title);
  font-size: var(--cs-section-size);
  font-weight: var(--cs-section-weight);
  line-height: var(--cs-section-leading);
}

/* Chevron drawn in CSS so it needs no asset and inherits the text colour. */
.cs-collapse-chevron {
  flex-shrink: 0;
  width: 10px;
  height: 10px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-3px) rotate(45deg);
  transition: transform 0.3s ease;
}

.cs-collapse--open .cs-collapse-chevron {
  transform: translateY(2px) rotate(225deg);
}

/* 0fr → 1fr animates to an automatic height, so the group still opens
   correctly when media inside it loads late and changes the height. */
.cs-collapse-body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.cs-collapse--open .cs-collapse-body {
  grid-template-rows: 1fr;
}

.cs-collapse-inner {
  overflow: hidden;
  min-height: 0;
}

/* Once fully open, stop clipping so media that bleeds past the text column
   (negative side margins) isn't cut off at the group's edges. */
.cs-collapse--settled .cs-collapse-inner {
  overflow: visible;
}

/* Trailing padding only — the sections inside bring their own top margins. */
.cs-collapse-content {
  /* No horizontal padding. The group is already exactly as wide as the widest
     thing a case study holds, so a .cs-body nested inside can keep its own
     680px cap and 24px padding and land on the same grid as a top-level one:
     text at 632, media bled to 712. Padding here made the inner column 662 and
     shifted every line 15px left of the text outside the group. */
  padding: 0 0 24px;
}

/* .cs-body carries its own 24px side padding for the article column; inside a
   collapsible the group already supplies that, so it is removed here to keep
   the body copy flush with the title above it. */
.cs-collapse-content > .cs-body {
  /* Centred rather than stripped. Removing the padding was what pushed body
     copy inside a group out to the container edges while copy outside stayed
     in the 632px column. */
  margin-left: auto;
  margin-right: auto;
}

/* Media inside a collapsible must not use the article-level negative-margin
   bleed — the group is already the full width, so it would spill out. */
/* No media override inside a group. Cancelling the bleed here left graphs,
   icon lists and image frames at 680 inside a group and 712 outside one, so
   the same component changed width depending on whether it happened to be
   folded away. The shared bleed now applies in both places. */

/* Centre whatever is nested inside. .cs-body normally gets centred because
   the overlay centres its own direct children — nested in here that no
   longer applies, so without this it sits flush-left and drags the
   negative-margin media bleed off the group's left edge. */
.cs-collapse-content > * {
  margin-left: auto;
  margin-right: auto;
}

/* The first section inside shouldn't add its usual 32px gap on top of the
   header's own padding. */
.cs-collapse-content > .cs-body:first-child {
  margin-top: 0;
}

@media (max-width: 768px) {
  .cs-collapse {
    width: calc(100vw - 48px);
    margin-left: auto;
    margin-right: auto;
  }
  .cs-collapse-header {
    padding: 16px 20px;
  }
}

/* ══════════════════════════════════════
   METRIC CHIP — inline highlight for figures in body copy
══════════════════════════════════════ */

/* `display: inline` (not inline-block) on purpose: vertical padding then
   paints without pushing the line box around, so chipping a number never
   changes the paragraph's line spacing. */
.cs-metric {
  display: inline;
  padding: var(--chip-pad-y) var(--chip-pad-x);
  /* Horizontal breathing room so two chips in a row never touch. Vertical
     margin is deliberately absent: it has no effect on an inline box, so
     the two-row collision is solved by the leading rule below instead of
     by a margin that would silently do nothing. */
  margin: 0 3px;
  border-radius: 999px;
  background: var(--color-metric-chip-bg);
  /* Border matches the section containers, so a neutral chip reads as the same
     material. The toned chips below drop it — a coloured wash carries its own
     edge and a border on top of it looks like a second treatment. */
  border: 1px solid var(--color-border-card);
  color: var(--color-metric-chip-text);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
  transition: background 0.5s ease, color 0.5s ease;
}

/* A chip's padding box is taller than its line box (that is the price of
   `display: inline`, which keeps unchipped paragraphs from reflowing). When
   a paragraph wraps, chips on adjacent lines therefore meet. Only
   paragraphs that actually contain a chip pay for the extra leading, so
   ordinary copy keeps its original rhythm. */
.cs-body-text:has(.cs-metric),
.cs-icon-row-text:has(.cs-metric) {
  line-height: 1.85;
}

/* Image/video/graph sections: the gap above the media (its own margin-top,
   32px) should equal the gap after it, before the next paragraph — and the
   gap to its own caption (.cs-hint) should be a bit more generous than the
   default hint spacing. When a caption follows, the caption carries the
   bottom margin (so the media-to-caption gap can be its own, smaller value
   without colliding with the outer symmetric margin); when there's no
   caption, the media element carries it directly. */
.cs-cover-img:not(:has(+ .cs-hint)),
.ba-slider:not(:has(+ .cs-hint)),
.graph-chart:not(:has(+ .cs-hint)),
.cs-video-frame:not(:has(+ .cs-hint)),
.cs-demo-video:not(:has(+ .cs-hint)) {
  margin-bottom: 32px;
}

/* …but not when the video is inside a frame. The frame is the element that
   spaces itself from the next block, and it already matched the rule above.
   Applying the margin to the inner video as well puts 32px INSIDE the
   frame's overflow:hidden box, where it paints as a band of card background
   under the video — the frame ends up 32px taller than its own content. */
.cs-video-frame .cs-demo-video {
  margin-bottom: 0;
}

.cs-cover-img + .cs-hint,
.ba-slider + .cs-hint,
.graph-chart + .cs-hint,
.cs-video-frame + .cs-hint,
.cs-demo-video + .cs-hint {
  margin-top: 20px;
  margin-bottom: 32px;
}

/* Draggable before/after image comparison — same 680px box as a single
   cs-cover-img, with a clip-path divider the user can drag or arrow-key. */
.ba-slider {
  width: 680px;
  margin: 32px auto 0;
}

.ba-slider-frame {
  position: relative;
  width: 100%;
  border-radius: var(--radius-card);
  overflow: hidden;
  cursor: ew-resize;
  touch-action: pan-y;
  user-select: none;
}

/* The first layer stays in flow so the frame inherits the image's natural
   height; the second is overlaid on top of it. Nothing is cropped. */
.ba-slider-layer {
  position: relative;
}

.ba-slider-layer--clip {
  position: absolute;
  inset: 0;
}

.ba-slider-img {
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
}

.ba-slider-handle {
  position: absolute;
  top: 0;
  height: 100%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  cursor: ew-resize;
  touch-action: pan-y;
}

.ba-slider-handle-line {
  flex: 1;
  width: 2px;
  background: #ffffff;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.35);
}

.ba-slider-handle-btn {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 2px solid #ffffff;
  background: rgba(0, 0, 0, 0.13);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}

.ba-slider-arrow {
  width: 0;
  height: 0;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
}

.ba-slider-arrow--left  { border-right: 7px solid #ffffff; }
.ba-slider-arrow--right { border-left:  7px solid #ffffff; }

/* ══════════════════════════════════════
   GRAPH CHART — reusable grouped bar chart
   (funnel breakdowns, before/after metric comparisons)
══════════════════════════════════════ */
.graph-chart {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 12px;
  /* 680px (the text block's full outer width) + 2×16px of its own side
     padding, so the bar area inside the padding is exactly as wide as the
     text block and the tinted container extends 16px past it on each side.
     No `max-width: 100%` here — the containing block is .cs-body's 632px
     content box, so 100% would clamp this back down to the text width and
     defeat the whole point. The mobile override below caps it instead. */
  width: calc(680px + 2 * 16px);
  margin: 32px auto 0;
  padding: 24px 24px 0;
  box-sizing: border-box;
  /* Charts with many groups scroll rather than crushing every bar. Thin and
     themed, so it does not read as browser chrome. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--color-border-card-hover) transparent;
  transition: background 0.5s ease;
}

.graph-chart::-webkit-scrollbar { height: 6px; }
.graph-chart::-webkit-scrollbar-track { background: transparent; }
.graph-chart::-webkit-scrollbar-thumb {
  background: var(--color-border-card-hover);
  border-radius: 999px;
}

.graph-group {
  /* Wide enough for two bars each holding a nowrap value container at
     --graph-chip-size. Below this the chart scrolls rather than letting a
     figure spill across the axis. */
  flex: 1 0 96px;
  min-width: 96px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 16px;
}

/* Height is set inline by GraphChart.js to match the tallest label in the
   chart (measured after render), so every group's bars start level even
   when one label wraps to more lines than the others. Text stays
   top-aligned by default since nothing here vertically centers it. */
/* C1 — this label sits above the bars, outside the plot area, so it carries no
   container: plain text in the design system's label style. */
.graph-group-label {
  width: fit-content;
  max-width: 100%;
  margin: 0 auto;
  padding: 0;
  box-sizing: border-box;
  font-family: var(--font-family-primary);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--color-text-primary);
  text-align: center;
  line-height: 1.3;
  transition: color 0.5s ease, height 0.2s ease;
}

.graph-bars {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  flex: 1;
}

/* Bar wrapper — plain flex, sizing only. */
.graph-bar {
  flex: 1;
  min-width: 0;
  display: flex;
}

/* The track — constant full-height box, tinted background, and the
   positioning root both children below are placed against. Always visible
   regardless of value, so every bar in a row reaches the same top edge
   (the box never resizes; only its fill does). No overflow clipping here
   — the value is allowed to rise above the track's own top edge at high
   fill percentages instead of being cut off. */
.graph-bar-track {
  position: relative;
  width: 100%;
  height: 220px;
  min-height: 44px;
  border-radius: 10px 10px 0 0;
  box-sizing: border-box;
  transition: background 0.5s ease;
}

.graph-bar--a .graph-bar-track { background: var(--color-graph-bar-a-track); }
.graph-bar--b .graph-bar-track { background: var(--color-graph-bar-b-track); }

/* Fixed to the track's own top edge — same Y for every bar in the chart,
   regardless of that bar's fill %. It never rides the fill's height, so it
   doesn't move as values change. Because a fixed spot at the top can land
   on top of either the tinted track OR a tall solid fill (near 100%), it's
   rendered as an opaque chip (own background + z-index above the fill) so
   it stays legible against either layer instead of just "not overlapping". */
/* Value sits in a small white chip using the same surface + border as the
   collapsible and graph cards, so the number reads as a label on the bar. */
/* C2 — every label that sits INSIDE the plot area shares this one container, so
   no chart can drift from another: same background, border, radius, padding,
   type size and colour. Both the value chip (top of the track) and the tag
   (bottom, over the fill) use it.

   C3 — the inset is positive and symmetric, so an in-plot container can never
   touch or overflow the axis lines. It was previously left/right: -6px, which
   deliberately borrowed the gutter between bars to fit a value like 25.43% on
   one line; that is exactly the overflow C3 forbids. Where content no longer
   fits, the mobile block below drops the font size rather than clawing back
   the inset. */
.graph-bar-value,
.graph-bar-tag {
  box-sizing: border-box;
  width: fit-content;
  padding: var(--graph-chip-pad-y) var(--graph-chip-pad-x);
  background: var(--color-surface-card);
  border: 1px solid var(--color-border-card);
  border-radius: 8px;
  box-shadow:
    0 1px 2px var(--color-shadow-card-rest),
    inset 0 1px 0 var(--color-card-inset-top);
  /* Centred on both axes. text-align alone left a single-line value sitting
     on the container's baseline rather than in the middle of the box. */
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-family-primary);
  font-size: var(--graph-chip-size);
  font-weight: var(--weight-bold);
  color: var(--color-text-primary);
  text-align: center;
  line-height: 1.15;
  /* Wrapping is allowed, but only at a space. `overflow-wrap: break-word` was
     splitting 100% into three stacked characters whenever the bar was narrower
     than the text; keep-all plus overflow-wrap: normal means a single figure
     has no break opportunity at all and always stays on one line, while a
     compound value like "57.9% / 42.1%" wraps cleanly to two. The max-width
     keeps either case inside the axis. */
  white-space: normal;
  overflow-wrap: normal;
  word-break: keep-all;
  max-width: calc(100% - (var(--graph-inset) * 2));
  transition: color 0.5s ease, background 0.5s ease;
}

.graph-bar-value {
  position: absolute;
  left: var(--graph-inset);
  right: var(--graph-inset);
  top: 10px;
  z-index: 2;
  margin: 0 auto;
}

/* The proportional fill — solid color, flush against the track on every
   edge except the top (that gap IS the encoding: room left to 100%).
   Bottom-anchored on desktop; rotates to left-anchored on mobile below.
   Rounds its own top corners (rather than relying on the track clipping
   it) since the track no longer clips, to keep the 100%-fill look
   consistent with the track's own rounded shape. */
.graph-bar-fill {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--graph-bar-fill, 0%);
  min-height: 32px;
  border-radius: 10px 10px 0 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  box-sizing: border-box;
  padding: 4px 2px;
  transition: height 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.graph-bar--a .graph-bar-fill { background: var(--color-graph-bar-a); }
.graph-bar--b .graph-bar-fill { background: var(--color-graph-bar-b); }

/* Container styling comes from the shared rule above. The per-bar text colours
   this used to carry (white on blue, dark on terracotta) existed because the
   tag sat directly on the coloured fill. It now sits on the card surface like
   the value chip, so a single inherited colour is correct and the contrast
   problem those overrides solved no longer exists. */
.graph-bar-tag {
  margin: 0 var(--graph-inset) 0;
  white-space: nowrap;
  /* Sized separately from the value chip above, because the two are clamped
     by different things. A value spans the whole track, but a tag sits inside
     .graph-bar-fill — its ceiling is the width the DATA gives it, and a bar
     at 4% has a fill a few pixels wide that no font size fits. So the two
     cannot be scaled up together on small screens. Same 9px by default; only
     the touch block at the end of this file pulls them apart. */
  font-size: var(--graph-tag-size);
}

/* ── Mobile: redesign to horizontal bars, groups stacked vertically ── */
@media (max-width: 768px) {
  .graph-chart {
    width: 100%;
    flex-direction: column;
    gap: 20px;
    padding: 16px;
    overflow-x: visible;
  }
  .graph-group {
    min-width: 0;
    max-width: none;
    width: 100%;
  }
  .graph-group-label {
    text-align: left;
  }
  .graph-bars {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }
  .graph-bar {
    width: 100%;
  }
  .graph-bar-track {
    width: 100%;
    height: 44px;
    min-height: 0;
    border-radius: 0 10px 10px 0;
  }
  /* Rotated: fixed to the track's own left (leading) edge instead of its
     top — same "constant position regardless of fill" idea as desktop,
     just along the horizontal axis since bars are horizontal here. */
  .graph-bar-value {
    left: 10px;
    right: auto;
    bottom: auto;
    top: 50%;
    transform: translateY(-50%);
    margin: 0;
    text-align: left;
    transition: color 0.5s ease, background 0.5s ease;
  }
  .graph-bar-fill {
    left: 0;
    top: 0;
    bottom: 0;
    right: auto;
    width: var(--graph-bar-fill, 0%);
    height: auto;
    min-height: 0;
    min-width: 64px;
    border-radius: 0 10px 10px 0;
    align-items: center;
    justify-content: flex-end;
    padding: 4px 10px;
  }
}

/* Header — fixed at top{
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
  z-index: 2;
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}

/* ── Colour Variables widget ──────────── */

.cv-outer {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  margin-top: 32px;
}
.cv-wrap {
  position: relative;
  width: 680px;
  max-width: calc(100vw - 48px);
  aspect-ratio: 4 / 3;
  border-radius: 20px;
  overflow: hidden;
  background: var(--color-widget-bg);
  color: var(--color-widget-text);
  font-family: var(--font-family-system);
  font-size: 12px;
  line-height: 1.4;
  display: flex;
  flex-direction: column;
  user-select: none;
}

/* Scrollbar */
.cv-wrap ::-webkit-scrollbar { width: 4px; height: 4px; }
.cv-wrap ::-webkit-scrollbar-track { background: transparent; }
.cv-wrap ::-webkit-scrollbar-thumb { background: var(--color-widget-scrollbar); border-radius: 2px; }

/* Responsive */
@media (max-width: 768px) {
  .cv-side { width: 120px; }
  .cv-col-name { width: 100px; }
  .cv-alias { font-size: 9px; }
  .cv-search input { width: 60px; }
}
.cc-outer .card-ripple { display: none; }
.cc-outer, .cc-outer * { cursor: default; }
.cc-outer select { cursor: pointer; }
.cc-outer {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  margin-top: 32px;
}

@keyframes cs-version-in {
  from { opacity: 0; transform: translateX(-32px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes cs-error-enter {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes cs-error-leave {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(-20px); }
}

@keyframes cs-marquee-left {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@keyframes cs-marquee-right {
  from { transform: translateX(-50%); }
  to   { transform: translateX(0); }
}

/* ── Case Study Image Scroll Reveal ───────────────────────── */

.cs-expanded-content .cs-cover-img,
.cs-expanded-content .cs-demo-video,
.cs-expanded-content .cs-account-link-steps,
.cs-expanded-content .ba-wrap {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity 1.2s ease, transform 1.2s ease;
}

.cs-expanded-content .cs-cover-img.cs-img-visible,
.cs-expanded-content .cs-demo-video.cs-img-visible,
.cs-expanded-content .cs-account-link-steps.cs-img-visible,
.cs-expanded-content .ba-wrap.cs-img-visible {
  opacity: 1;
  transform: translateY(0);
}

/* ── Account Link Steps ──────────────────────────────── */

.cs-account-link-steps {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: 24px;
  margin-top: 32px;
  max-width: 680px;
  width: 100%;
}

@media (max-width: 768px) {
  .cs-account-link-steps {
    gap: 12px;
    max-width: calc(100vw - 48px);
  }
}

/* ── Antonym Section ─────────────────────────────────────────── */

.cs-hint {
  font-family: var(--font-family-primary);
  font-size: var(--hint-size);
  font-weight: var(--hint-weight);
  color: var(--color-text-subtle);
  margin-top: 12px;
  transition: color 0.5s ease;
  /* Captions run the full width of the media they sit under, which is wider
     than the text column. At 768 that measured 79ch — past the comfortable
     reading limit. Everywhere else it's already well under, so this only
     bites at the one breakpoint. */
  max-width: 72ch;
}

/* ── Mobile antonym layout: cards left{
  display: flex;
  align-items: center;
  padding: 24px;
  gap: 20px;
  height: auto !important;
}

/* When content-type is hovered: default exits left{
  transform: translateX(-100%);
}

/* ── Sun / Moon Theme Toggle Card ─────────────────────────── */
.bulb-card {
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  overflow: hidden;
  transition: background 0.5s ease, box-shadow 0.25s ease;
}

/* Icon wrapper — sun and moon are stacked inside */
.icon-wrap {
  position: relative;
  width: 90px;
  height: 90px;
  z-index: 1;
  overflow: visible;
}

/* ── Theme toggle in the nav (top-right) ──────────────────────────────────
   Reuses the bulb card's icon markup verbatim and scales the 90px wrapper
   down to fit. Scaling rather than restyling is deliberate: every ray delay,
   rotation and fade is already tuned in the rules below, and a second
   hand-sized copy would drift from them the first time either is touched. */
/* margin-left:auto rather than absolute positioning: .nav-pills is already
   absolutely centred and out of flow, so the toggle is the nav's only other
   flex child. Pushing it with auto margin means its right inset matches the
   logo's left inset from the nav's own padding, instead of being hard-coded
   to a value that would drift from it. */
.nav-theme-toggle {
  margin-left: auto;
  width: 44px;
  height: 44px;
  position: relative;
  padding: 0;
  cursor: pointer;
  /* Same surface as the bento cards. */
  background: var(--color-surface-card);
  border-radius: var(--radius-card);
  border: 1px solid var(--color-border-card);
  box-shadow:
    0 1px 3px var(--color-shadow-card-rest),
    0 1px 2px var(--color-shadow-card-rest),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
  transition: background 0.5s ease, box-shadow 0.3s ease, border-color 0.3s ease, transform 0.15s ease;
}

.nav-theme-toggle:hover {
  background: var(--color-surface-card-hover);
  border-color: var(--color-border-card-hover);
  transform: translateY(-2px);
}

/* Absolute centring, not place-items. The wrapper is laid out at its full 90px
   even though the button is 44px, and centring an oversized grid item does not
   put its centre on the button's centre — it landed 23px down and right.
   Pinning to 50/50 and translating by half its own size makes the centre exact
   before the scale is applied, and scale then works about that same point.
   0.2 rather than 0.333 because the sun's rays reach well past the disc: its
   full visual extent is ~190px unscaled, so 0.333 spilled 63px of sun out of a
   44px container. At 0.2 the whole sun, rays included, sits inside. */
.nav-theme-toggle .icon-wrap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.2);
}

/* ── Sun ── */
.sun {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 1;
  transform: rotate(0deg) scale(1);
  transition:
    opacity    0.35s cubic-bezier(0.4, 0, 0.2, 1),
    transform  0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.sun.hidden {
  opacity: 0;
  transform: rotate(90deg) scale(0.4);
}

.sun-circle {
  position: absolute;
  width: 49px;
  height: 49px;
  background: var(--gradient-sun);
  border-radius: 50%;
  box-shadow: 0 0 18px var(--color-sun-glow);
  animation: sun-breathe 2.8s ease-in-out infinite;
  z-index: 2;
}

@keyframes sun-breathe {
  0%, 100% { box-shadow: 0 0 14px rgba(245, 166, 35, 0.45); }
  50%       { box-shadow: 0 0 36px rgba(245, 166, 35, 0.90), 0 0 64px rgba(255, 190, 40, 0.28); }
}

/*
 * B-style sharp burst rays:
 * .ray-arm  — zero-size anchor at icon centre, rotated per ray
 * .ray-bar  — sits at centre (bottom: 0), shoots outward via scaleY, fades as it travels
 */
.ray-arm {
  position: absolute;
  top: 50%; left: 50%;
  width: 0; height: 0;
  z-index: 1;
}

.ray-bar {
  position: absolute;
  width: 3px; height: 110px;
  bottom: 0; margin-left: -1.5px;
  transform-origin: bottom center;
  transform: scaleY(0);
  opacity: 0;
  border-radius: 2px 2px 1px 1px;
  background: linear-gradient(to top,
    rgba(245, 158, 11, 1)   0%,
    rgba(245, 158, 11, 0)   100%);
  animation: shoot-b 1.6s cubic-bezier(0.25, 0, 0.6, 1) infinite;
}

@keyframes shoot-b {
  0%   { transform: scaleY(0);    opacity: 0;    }
  5%   { transform: scaleY(0.05); opacity: 0.95; }
  100% { transform: scaleY(1);    opacity: 0;    }
}

/* Pause all sun animations when hidden (dark mode) */
.sun.hidden .ray-bar,
.sun.hidden .sun-circle { animation-play-state: paused; }

/* ── Moon ── */
.moon {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transform: rotate(-90deg) scale(0.4);
  transition:
    opacity    0.35s cubic-bezier(0.4, 0, 0.2, 1) 0.05s,
    transform  0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 0.05s;
}
.moon.visible {
  opacity: 1;
  transform: rotate(0deg) scale(1);
}

/* Crescent shape — full circle with a punched-out circle on top-right */
.moon-shape {
  position: relative;
  width: 50px;
  height: 50px;
}
.moon-shape::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--gradient-moon);
  box-shadow: 0 0 18px var(--color-moon-glow);
}
/* The punch-out circle uses the card's own background colour to carve the crescent */
.moon-shape::after {
  content: '';
  position: absolute;
  width: 35px;
  height: 35px;
  top: -7px;
  left: 18px;
  border-radius: 50%;
  background: var(--color-surface-card);
  transition: background 0.5s ease;
}

/* Twinkling stars beside the moon */
.moon-stars {
  position: absolute;
  inset: 0;
}
.mstar {
  position: absolute;
  border-radius: 50%;
  background: var(--color-moon-star);
  animation: twinkle 1.8s ease-in-out infinite;
}
.mstar:nth-child(1) { width: 7px; height: 7px; top:  5px; right:  2px; animation-delay: 0.0s; }
.mstar:nth-child(2) { width: 5px; height: 5px; top: 26px; right: -8px; animation-delay: 0.6s; }
.mstar:nth-child(3) { width: 5px; height: 5px; top:  0px; right: 15px; animation-delay: 1.1s; }

@keyframes twinkle {
  0%, 100% { opacity: 1;   transform: scale(1);   }
  50%       { opacity: 0.2; transform: scale(0.4); }
}

/* ── Click ripple (all cards) ── */
.card-ripple {
  position: absolute;
  border-radius: 50%;
  transform: scale(0);
  animation: card-ripple 0.52s linear forwards;
  background: var(--color-card-ripple);
  pointer-events: none;
  z-index: 10;
}
@keyframes card-ripple {
  to { transform: scale(20); opacity: 0; }
}

/* Gmail / LinkedIn */
.gmail-card, .linkedin-card { display: flex; align-items: center; justify-content: center; }

/* ── LinkedIn animated icon ── */
.li-svg {
  overflow: visible;
  display: block;
}

.li-dot {
  fill: var(--color-brand-linkedin);
  transform-origin: 11px 10px;
  transform: scale(0);
  opacity: 0;
  animation: li-pop-in 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

.li-path {
  fill: none;
  stroke: var(--color-brand-linkedin);
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 3.2;
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
  animation: li-draw var(--draw-dur, 0.5s) cubic-bezier(0.4, 0, 0.2, 1) var(--draw-delay, 0s) forwards;
}

@keyframes li-draw {
  to { stroke-dashoffset: 0; }
}
@keyframes li-pop-in {
  to { transform: scale(1); opacity: 1; }
}

/* ── Gmail copy animation ── */

/* 1. Container transitions: grows + flips bg after the icon fades (delay 0.1s) */
.gmail-card .action-icon {
  overflow: hidden;
  transition:
    width            0.28s cubic-bezier(0.34, 1.4, 0.64, 1) 0.1s,
    background-color 0.22s ease                              0.1s,
    padding          0.28s ease                              0.1s;
}

.gmail-card .action-icon.gmail-copied-label {
  opacity: 1 !important;
  transform: scale(1) !important;
  width: 86px;
  padding: 0 14px;
  background: var(--color-text-primary);
}

/* 2. Copy icon fades out immediately on click */
.gm-copy-icon {
  flex-shrink: 0;
  transition: opacity 0.12s ease;
}

.gm-copy-icon--out {
  opacity: 0 !important;
}

/* 3. "Copied" text fades in once the pill has grown (delay 0.3s) */
.gm-copied-text {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 3px;
  opacity: 0;
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  color: var(--color-surface-card);
  white-space: nowrap;
  letter-spacing: var(--tracking-tight);
  pointer-events: none;
  transition: opacity 0.15s ease 0s;
}

.gm-copied-text--in {
  opacity: 1;
  transition: opacity 0.4s ease 0.3s;
}

.gm-check-lottie {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.gm-check-lottie svg {
  display: block;
  width: 100%;
  height: 100%;
  filter: var(--filter-checkmark);
}

/* ── Gmail animated icon ── */
.gm-svg {
  overflow: hidden;
  display: block;
}

.gm-from-top   { transform: translateY(-50px); opacity: 0; }
.gm-from-left  { transform: translateX(-70px); opacity: 0; }
.gm-from-right { transform: translateX(70px);  opacity: 0; }

.gm-from-top,
.gm-from-left,
.gm-from-right {
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.34, 1.15, 0.64, 1);
  animation-delay: 0s;
  animation-fill-mode: both;
}

.gm-from-top   { animation-name: gm-in-top; }
.gm-from-left  { animation-name: gm-in-left; }
.gm-from-right { animation-name: gm-in-right; }

@keyframes gm-in-top {
  from { transform: translateY(-50px); opacity: 0; }
  to   { transform: translateY(0);     opacity: 1; }
}
@keyframes gm-in-left {
  from { transform: translateX(-70px); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}
@keyframes gm-in-right {
  from { transform: translateX(70px);  opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}

/* No surface override for the project cards. They used to paint
   --color-surface-card-dark, which is pure black in light theme, so the card
   underneath a still-loading clip was black on a light page while every other
   card was #FAF9F7. Background, radius, border and shadow all come from
   .bento-card now — identical to the quote cards in both themes. */

/* VF card video background */
.vf-card {
  padding: 0 !important;
  overflow: hidden;
}

.vf-card .action-icon {
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.20);
}

/* Plugin card video background */
.ai-card {
  padding: 0 !important;
  overflow: hidden;
}

.plugin-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(1.05);
  transform-origin: center;
}

.ai-card .action-icon {
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.20);
}

@-webkit-keyframes pc1-scroll {
  from { -webkit-transform: translateX(0); }
  to   { -webkit-transform: translateX(-50%); }
}

@keyframes pc1-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* Scrollable tall-image container */

/* Full-width image — stretches to scroll container, no rounding, no enter animation */

/* Design System */
.uq-card {
  padding: 0 !important;
  overflow: hidden;
}

.uq-card .action-icon {
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.20);
}

/* ── UX Quote Card — expandable wrapper ── */
.ux-quote-card-wrapper {
  position: relative;
  width: 100%; height: 100%;
}

.ux-quote-card-wrapper > .ux-quote-card {
  width: 100%; height: 100%;
}

/* Ghost: hide collapsed card while expanded, keep grid slot occupied */
.ux-quote-card--ghost { opacity: 0; pointer-events: none; }

/* ── UX Quote expanded content ── */
.ux-quote-expanded-inner {
  padding: 40px 56px !important;   /* narrower side padding for 560px card */
  justify-content: center;
}

.ux-quote-expanded-icon {
  width: 36px; height: auto;
  display: block; flex-shrink: 0;
}

.ux-quote-expanded-quote {
  font-family: var(--font-family-title);
  font-size: var(--quote-expanded-size);
  font-weight: var(--quote-expanded-weight);
  line-height: var(--quote-expanded-leading);
  text-align: center;
  color: var(--color-text-primary);
  transition: color 0.5s ease;
  margin: 0;
}

/* Emphasis inside a quote stays regular weight like the rest of the title —
   the brand gradient is what marks it, so no weight contrast is needed. */
.ux-quote-expanded-quote strong {
  color: var(--color-accent-text);
  font-weight: var(--weight-medium);
}

.ux-quote-expanded-body {
  font-size: var(--quote-body-size);
  line-height: var(--quote-body-leading);
  color: var(--color-text-body);
  display: flex;
  flex-direction: column;
  gap: 16px;
  text-align: left;
  width: 100%;
  transition: color 0.5s ease;
}

.ux-quote-expanded-body p { margin: 0; }

/* ── DS Quote Card — expandable wrapper ── */
.ds-quote-card-wrapper {
  position: relative;
  width: 100%; height: 100%;
}

.ds-quote-card-wrapper > .ds-quote-card {
  width: 100%; height: 100%;
}

.ds-quote-card--ghost {
  opacity: 0;
  pointer-events: none;
}

.ds-quote-expanded-inner {
  padding: 40px 56px !important;
  justify-content: center;
}

.ds-quote-expanded-icon {
  width: 36px;
  height: auto;
  display: block;
  flex-shrink: 0;
}

.ds-quote-expanded-quote {
  font-family: var(--font-family-title);
  font-size: var(--quote-expanded-size);
  font-weight: var(--quote-expanded-weight);
  line-height: var(--quote-expanded-leading);
  text-align: center;
}

.ds-quote-expanded-quote strong {
  background: var(--gradient-brand);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.ds-quote-expanded-body {
  font-size: var(--quote-body-size);
  line-height: var(--quote-body-leading);
  color: var(--color-text-body);
  display: flex;
  flex-direction: column;
  gap: 16px;
  text-align: left;
}

/* Quote cards */
.ux-quote-card, .ds-quote-card {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: 32px; gap: 16px;
}

.ux-quote-card .quote-text,
.ds-quote-card .quote-text {
  font-family: var(--font-family-title);
  font-size: var(--quote-size); line-height: var(--quote-leading);
  color: var(--color-text-primary); transition: color 0.5s ease;
  font-weight: var(--quote-weight);
}

.ux-quote-card strong,
.ds-quote-card strong {
  color: var(--color-accent-text);
  font-weight: var(--weight-medium);
}

/* Quote cards ("my UX principle" / "my design system principle") use a plain
   accent colour for their emphasis rather than the global gradient-clipped
   treatment. The global `strong, b` rule sets -webkit-text-fill-color to
   transparent, so that has to be reset here or the colour never shows. */
.ux-quote-card strong,
.ds-quote-card strong,
.ux-quote-expanded-quote strong,
.ds-quote-expanded-quote strong {
  background: none;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  -webkit-text-fill-color: currentColor;
  color: var(--color-accent-text);
  font-weight: var(--weight-medium);
}

.quote-icon {
  width: 29px; height: auto;
  display: block; flex-shrink: 0;
}

.design-principle {
  font-size: var(--principle-size);
  font-weight: var(--principle-weight);
  letter-spacing: var(--principle-tracking);
  text-transform: uppercase;
  color: var(--color-text-primary);
  opacity: 0.3;
  transition: color 0.5s ease;
}

/* ══════════════════════════════════════
   TABLET + MOBILE — 2-column grid
   The 4-column grid is only square at its design width: 1062px of grid plus
   2×24px of page padding = 1110px of viewport. Below that the columns keep
   shrinking while grid-auto-rows stays pinned at 252px, so cards turn into
   portrait slivers — measured 167×252 at 769px, 180×252 at 820px. Because the
   hero clips are object-fit: cover, that crops them hard enough to cut words in
   half. So the 2-column layout owns everything under 1110px, not just phones.
   Placement comes from MOBILE_LAYOUTS in filterLayouts.js, which switches on
   the same number.
══════════════════════════════════════ */
@media (max-width: 1109px) {

  /* Stack logo above grid instead of side-by-side */
  .main {
    flex-direction: column;
    align-items: center;
    padding-top: 40px;
  }

  /* ── Grid: 4 → 2 columns, fluid width ── */
  /* Row height = column width → 1:1 cards.
     width: 100% overrides align-items:center on .main so 1fr columns
     actually fill the available space.
     Gap is tighter than desktop's 18px: with content cards now full width,
     18px between stacked full-bleed cards read as dead space.

     --grid-w is the single source of truth for how wide the grid actually
     ends up, so row height and the quote-card floor below are both derived
     from it and cannot drift out of sync with the columns.

     The 772px cap is the reason a tablet doesn't look like a stretched
     phone. Uncapped, 100vw math gives 482px cards at 1024px — nearly twice
     the desktop card — and MOBILE_LAYOUTS was composed for phone-sized
     tiles, so the full-width quote cards became 719px of near-empty panel
     around one line of type. 772 = 2×380 + 12, which holds cards at the
     size they reach at 820px (iPad portrait) and then centres the grid
     instead of inflating it. Phones are unaffected: at 768px the viewport
     term is 720px, already under the cap. */
  .grid {
    --grid-w: min(calc(100vw - 48px), 772px);
    grid-template-columns: repeat(2, 1fr);
    grid-auto-rows: minmax(calc((var(--grid-w) - 12px) / 2), auto);
    gap: 12px;
    width: 100%;
    max-width: 772px;
  }

  /* Single-cell cards: enforce 1:1 via aspect-ratio as a safety net
     in case grid-auto-rows and column width drift apart. `bulb-card` is
     no longer single-cell (it spans both columns on one row), so forcing
     1:1 on it would make it a full-width square. */
  .gmail-card,
  .linkedin-card {
    aspect-ratio: 1 / 1;
  }

  /* Quote cards: 50% taller than one row so text doesn't crop.
     Full-width cards (1/3), alone in their row, so growing is safe.
     Derived from --grid-w rather than 100vw so the floor stops growing once
     the grid hits its cap — otherwise these were the cards that ballooned
     worst on tablets. */
  .ds-quote-card,
  .ux-quote-card {
    /* Capped at 386px — half the 772px grid cap, so at tablet widths these
       land on the same 2:1 landscape proportion they have on desktop
       (measured 522×252 at 1180px). Without the cap the 1.5×row floor keeps
       climbing with width and gives a 772×570 panel holding one line of
       type. On phones the floor is still what binds (247px at 390px), so
       the cap changes nothing there. */
    min-height: min(calc((var(--grid-w) - 12px) / 2 * 1.5), 386px);
  }

  /* Card grid placements are driven by inline styles from MOBILE_LAYOUTS
     in filterLayouts.js — no CSS placement rules needed here. */

  /* ── Animation delays — re-staggered for mobile reading order ── */
  .about-card-wrapper { --card-enter-delay: 0s;   }
  .gmail-card         { --card-enter-delay: 0.1s;  }
  .linkedin-card      { --card-enter-delay: 0.2s;  }
  .ai-card        { --card-enter-delay: 0.3s;  }
  .ds-quote-card      { --card-enter-delay: 0.4s;  }
  .uq-card            { --card-enter-delay: 0.5s;  }
  .ux-quote-card      { --card-enter-delay: 0.6s;  }
  .vf-card         { --card-enter-delay: 0.7s;  }
  .bulb-card          { --card-enter-delay: 1.0s;  }
}

/* ══════════════════════════════════════
   MOBILE BREAKPOINT — 2 columns
══════════════════════════════════════ */
@media (max-width: 768px) {

  /* ── Mobile type scale ──────────────────────────────────────────────────
     The desktop sizes were carrying straight through to 375px, where the
     text column is only 327px wide. Measured there, the title got 18
     characters per line and body copy 30 — the title wrapped into four
     ragged lines and read as a paragraph rather than a heading.

     Sizes come down; the leading values come *up* slightly to compensate,
     because tight desktop leading on smaller type closes the lines up too
     much. Ratios between the steps are preserved, so the hierarchy reads
     the same, just at mobile scale.

       title      37 → 28px   (18 → 24 chars/line)
       section    28 → 23px
       subsection 21 → 19px
       body       18 → 16px   (30 → 34 chars/line)
       caption    16 → 14px
       about bio  17 → 16px                                                */
  :root {
    --cs-title-size:      28px;
    --cs-title-leading:   1.22;
    --cs-section-size:    23px;
    --cs-section-leading: 1.3;
    --cs-subsection-size: 19px;
    --cs-body-size:       var(--text-xl);   /* 16px */
    --hint-size:          var(--text-base); /* 14px */
    --about-bio-size:     var(--text-xl);   /* 16px */
  }

  /* ── Touch targets ────────────────────────────────────────────────────
     Everything below measured under 44px on a touch viewport. These are
     grown rather than given an invisible expanded hit area, so the target
     you can see is the target you can hit — and so the audit can verify it
     from the rendered box instead of trusting a pseudo-element.
     Desktop keeps its tighter sizes; the pointer floor is 24px.           */
  .action-icon,
  .cs-header-close {
    width: 44px;
    height: 44px;
  }

  /* The mobile nav is a floating pill bar at the bottom whose pills span the
     full width, so margin-left:auto pushed the toggle off the right edge —
     only a sliver of the sun stayed visible. It moves to the top-right of the
     viewport instead, opposite .mobile-page-logo.
     position:fixed is resolved against .nav rather than the viewport, because
     .nav carries a transform and a transformed ancestor becomes the containing
     block for fixed descendants. That is wanted here: the toggle rides the
     nav's hide-on-scroll transform instead of detaching from it. */
  .nav-theme-toggle {
    position: fixed;
    margin-left: 0;
    left: auto;
    right: 24px;
    bottom: calc(100vh - 68px);
  }

  /* .nav-pill's height is set further down, where the mobile `padding`
     shorthand that would otherwise reset it lives. */

  /* .uis-toggle-btn's touch height is set next to its base rule further down —
     that rule uses the `padding` shorthand and, being later in the file, wins
     over anything set here (a media query adds no specificity). */

  .cs-section-pill {
    padding-top: 14px;      /* 33 → 45px tall */
    padding-bottom: 14px;
  }

  .ba-slider-handle {
    width: 44px;
  }

  /* ── Nav: floating pill at bottom ── */
  .nav {
    top: auto;
    bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    left: 24px;
    right: 24px;
    padding: 0;
    background: none;
    border: none;
  }

  /* Never hide on mobile — thumb needs it reachable */
  .nav--hidden {
    transform: none;
  }

  /* Logo lives at top of page on mobile, not in the nav */
  .nav-logo {
    display: none;
  }

  /* Hide lazy-mode toggle on mobile */

  /* Mobile page logo — centred above the grid */
  .mobile-page-logo {
    display: block;
    width: 48px;
    height: 48px;
    margin: 0 auto 20px;
  }

  /* Pills stretch to fill the nav, with more generous spacing */
  .nav-pills {
    position: relative;
    left: 0;
    transform: none;
    width: 100%;
    justify-content: center;
    padding: 6px;
    gap: 0;
  }

  /* This rule sits below the touch-target block and uses the `padding`
     shorthand, so it was silently resetting the longhand set there and the
     pill stayed 39px. Height lives here now rather than in two places. */
  .nav-pill {
    flex: 1;
    text-align: center;
    padding: 13px 8px;      /* 39 → 45px tall */
  }

  /* Bottom padding clears the fixed mobile nav. The rest of the stacking
     lives in the 1109px block above, which this range also matches. */
  .main {
    padding-bottom: calc(90px + env(safe-area-inset-bottom, 0px));
  }


  /* ── About card — stacked on mobile: image above text ──
     Side-by-side leaves the bio a ~2/3-width column that wraps every three
     or four words, and the copy ran under the expand icon in the top-right.
     Stacking gives the bio the full card width and moves it clear of the
     icon; the extra padding is what keeps it off the card edges once the
     avatar is no longer setting the left inset. */
  .about-card {
    flex-direction: column;
    align-items: flex-start;
    gap: 16px;
    padding: 24px;
  }

  .about-avatar {
    width:  calc((100vw - 48px) / 3);
    height: calc((100vw - 48px) / 3);
    border-radius: 16px;
  }

  /* The avatar previously supplied the bio's left inset; stacked, it needs
     none, and it must clear the absolutely-positioned expand icon. */
  .about-bio {
    padding-left: 0;
    padding-right: 40px;
  }

  /* ── Expanded about card — tighter side padding on mobile ── */
  .about-expanded-inner {
    padding-left: 40px;
    padding-right: 40px;
  }

  /* ── Case study header — bring close button to edge on narrow screens ── */
  .cs-header {
    padding: 24px 24px;
  }

  /* ── Section nav — use more width ──
     The `bottom: 48px` that used to sit here is gone. It was a relative lift
     that pushed the pills 48px out of .cs-nav-dock's own box to clear the
     mobile browser chrome, which left the dock's bounding box and its visible
     contents in two different places — the reason the back-to-top button read
     as "not overlapping the dock" while a tap on it still landed on a section
     pill. The dock is now positioned clear of the site nav as a whole (see the
     bottom-furniture block at the end of this file), so the pills belong back
     inside it. */
  .cs-section-nav {
    max-width: min(92vw, 640px);
  }
  .cs-section-nav-track {
    max-width: min(92vw, 640px);
  }

  /* ── Case study hero video — scale down with page padding ── */
  .cs-hero-video {
    width:  calc(100vw - 48px) !important;
    height: calc(100vw - 48px) !important;
    max-width: 448px;
    max-height: 448px;
  }

  /* ── outbe hero wrap — match shrunk hero size ── */
  .outbe-hero-wrap {
    width:  calc(100vw - 48px) !important;
    height: calc(100vw - 48px) !important;
    max-width: 448px;
    max-height: 448px;
  }

  /* ── UI Snapshots hero wrap — match shrunk hero size ── */
  .uis-hero-wrap {
    width:  calc(100vw - 48px) !important;
    height: calc(100vw - 48px) !important;
    max-width: 448px;
    max-height: 448px;
  }

  /* ── Marquee — full opacity, no hover, smaller for faster load ── */
  .cs-marquee-img {
    opacity: 1;
    transition: none;
    height: 100px;
  }
  .cs-marquee-img:hover {
    opacity: 1;
  }

  /* ── Case study back-to-top — clear mobile browser nav bar ── */
  .cs-back-to-top {
    bottom: 48px;
    width: 44px;      /* touch target — 40px on desktop */
    height: 44px;
  }

  /* ── Disable scroll-reveal on demo video only (IO unreliable in overlay on mobile) ── */
  .cs-expanded-content .cs-demo-video {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  /* ── Case study containers — fit viewport with same padding as text ── */
  /* Mobile: no bleed — media sits inside the text column, so the negative
     side margins from the desktop rules have to be cancelled or the
     content overflows the viewport.

     Width is `100%` (of the text column) rather than `calc(100vw - 48px)`.
     At the top level the two are identical, because .cs-body spans the
     viewport and its 24px padding leaves exactly 100vw - 48px. But once a
     section is nested inside something already inset — a CollapsibleGroup,
     say — a viewport-derived width is too wide and spills out the side,
     whereas `100%` stays correct at any nesting depth. */
  .cs-demo-video,
  .cs-video-frame,
  .cs-cover-img,
  .ba-slider,
  .cs-body > .cs-demo-video,
  .cs-body > .cs-video-frame,
  .cs-body > .cs-cover-img,
  .cs-body > .ba-slider,
  .cs-body > .graph-chart,
  .cs-body > .cs-icon-list {
    width: 100% !important;
    max-width: 680px;
    margin-left: auto !important;
    margin-right: auto !important;
  }

  /* ── Mobile container padding & radius ── */
  .cs-icon-list { padding: 20px; }
  .cs-collapse-header { padding: 20px; }
  .cs-collapse-content { padding: 0 20px 20px; }

  /* Graphs: no left padding so the bars start flush at the container edge,
     and no bottom padding, matching desktop. */
  .graph-chart { padding: 20px 20px 0 0; }

  /* Label sits closer to the bar row it describes */
  .graph-group { gap: 8px; }

  /* Consecutive graphs (with or without a caption between them) sit closer
     together than a graph does to body copy. */
  .graph-chart + .graph-chart,
  .cs-hint + .graph-chart {
    margin-top: 20px !important;
  }

  /* --radius-card is 20px globally now; no mobile override needed. */

  .cs-video-frame .cs-demo-video {
    width: 100% !important;
  }

  .cs-zoom-viewer,
  .cs-miro-viewer {
    width: calc(100vw - 48px) !important;
    max-width: 680px;
    height: 320px;
  }

  .cs-proto-row {
    gap: 8px;
  }

  .cs-proto-phone {
    width: calc((100vw - 48px - 16px) / 3);
    max-width: 200px;
    height: auto;
    aspect-ratio: 375 / 812;
    border-radius: 14px;
  }

  .ba-wrap {
    width: calc(100vw - 48px);
    max-width: calc(100vw - 48px);
  }

  .cs-error-section {
    width: calc(100vw - 48px);
    max-width: 680px;
  }

  /* Antonym & version section widths/heights set in JS via csScale */
  .cs-antonym-section { max-width: calc(100vw - 48px); }
  .cs-versions-section { max-width: calc(100vw - 48px); }

  /* Logo rail — 150% faster on smaller screen */
  .pc1-track {
    animation-duration: 16s;
  }

  /* ── Hide back-to-top on mobile homepage (nav is already at bottom) ── */
  .back-to-top {
    display: none;
  }

  /* ── Always show action icon (no hover needed) ── */
  .action-icon {
    opacity: 1;
    transform: scale(1);
  }

  /* ── Disable hover lift so tap goes straight to click ── */
  .bento-card:hover {
    transform: none;
    border-color: var(--color-border-card);
    box-shadow:
      0 1px 3px var(--color-shadow-card-rest),
      0 1px 2px var(--color-shadow-card-rest),
      inset 0 1px 0 var(--color-card-inset-top),
      inset 0 -1px 0 var(--color-card-inset-bottom);
  }

  /* ── Pressed state — MUST come after hover reset to win specificity ── */
  .bento-card:active,
  .bento-card--pressed {
    transform: scale(0.97) !important;
    box-shadow:
      0 1px 3px var(--color-shadow-card-rest),
      0 1px 2px var(--color-shadow-card-rest),
      inset 0 1px 0 var(--color-card-inset-top),
      inset 0 -1px 0 var(--color-card-inset-bottom) !important;
    transition: transform 0.08s ease, box-shadow 0.08s ease !important;
  }
}

/* ══════════════════════════════════════
   REDUCED MOTION
   When the OS (or low-power mode) signals reduced motion,
   disable all keyframe animations and heavy transitions.
   Cards appear instantly; hover/active still work via
   simple property changes — just without animated movement.
══════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  /* Kill all keyframe animations globally */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0s !important;
  }

  /* Cap transition durations to instant-feeling snaps */
  .bento-card,
  .bento-card:hover,
  .bento-card:active,
  .bento-card--pressed,
  .grid-slot,
  .nav-pill-indicator,
  .nav-pill-indicator--ready,
  .nav-pill-indicator--liquid,
  .back-to-top {
    transition-duration: 0.1s !important;
  }

  /* Hide custom cursor — native cursor is fine */
  .cs-root,
  .cursor-tooltip {
    display: none !important;
  }

  /* Restore native cursor when custom one is hidden */
  *, *::before, *::after {
    cursor: auto !important;
  }

  /* Remove persistent GPU layers that aren't needed without animation */
  .bento-card {
    will-change: auto !important;
  }

  .nav-pill-indicator {
    will-change: auto !important;
  }

  .ba-img {
    will-change: auto !important;
  }
}

/* ═══════════════════════════════════════════════════════════
 *  OUTBE HUB CASE STUDY
 * ═══════════════════════════════════════════════════════════ */

/* ── Card styling ── */
/* No background of its own. This was #1c1743 (and before that the terracotta
   brand orange) to hide the hairline ring at the rounded corners where the
   clip's radius and the card's don't land on the same pixels. But a fixed
   dark value also meant the card read as near-black on a light page whenever
   the clip hadn't decoded yet. The shared surface is used instead, so the
   residual corner ring now matches every other card. */

.outbe-card video {
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
}

/* Card-face background clip. The rule above zeroes opacity on every video in
   this card — a leftover from the removed preview thumbnail — so the
   background has to opt back out of it explicitly.
   translateZ forces its own compositing layer: without one Chrome hands a
   playing video to a hardware overlay that ignores the parent's radius, and
   the corners square off mid-playback. */
.outbe-card video.outbe-card-video {
  position: absolute;
  inset: 0;
  opacity: 1;
  object-fit: cover;
  border-radius: inherit;
  transform: translateZ(0);
}

/* Setting videoSrc gives a case study its hero clip, but the overlay renders
   that same clip a second time inside the collapsed card as a preview. These
   cards already have their own background clip, so the preview is hidden.

   position:absolute is the part that matters. Left in normal flow with no
   sizing, a <video> lays out at its intrinsic size — 1920x1080 here — which
   stretched grid slots from 522x522 to 522x1082 and broke the whole grid.
   Taking it out of flow means it cannot affect card height at all.

   opacity rather than display:none: the open animation measures this element's
   rect to know where to fly from, and display:none measures 0. The hero
   (.cs-hero-video) and the flying element (.cs-video-fly) use their own classes
   and are untouched by this rule. */
.bento-card > video.cs-hero-clip {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
}

/* ── A3 · Shared card thumbnail overlay ─────────────────────────────────────
   One treatment for every case-study thumbnail: same scrim strength, same
   position, same type style. A logo where the project has one, a short title
   as the fallback — never both, never neither.
   The scrim is what makes the overlay legible over light AND dark clips, which
   matters here because several thumbnails swap between a light and a dark cut
   with the theme. Raw text on the clip would fail on one of the two. */
.cs-thumb-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
  box-sizing: border-box;
  text-align: center;
  pointer-events: none;
  border-radius: inherit;
}

.cs-thumb-overlay::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--thumb-scrim);
  border-radius: inherit;
  pointer-events: none;
}

/* Per-card scrims. Set on the card so it cascades to the overlay's ::before
   in both the collapsed face and the expanded hero, rather than duplicating
   the ::before rule per card. Everything unlisted keeps --thumb-scrim.
   The funnel, AI and VF cards show a bare clip with no overlay at all, so
   they have no scrim to override. */

/* position:relative on both children so they paint above the ::before scrim.
   The scrim is absolutely positioned and so paints after in-flow content;
   without this it would cover the logo and title entirely. */
.cs-thumb-logo {
  position: relative;
  width: var(--thumb-logo-w);
  max-width: var(--thumb-logo-max);
  height: auto;
}

.cs-thumb-title {
  position: relative;
  margin: 0;
  font-family: var(--font-family-instrument);
  font-style: italic;
  font-size: var(--thumb-title-size);
  font-weight: 400;
  line-height: 1.05;
  color: var(--color-primitive-neutral-0);
  white-space: pre-line;
}

@media (max-width: 768px) {
  .cs-thumb-overlay { padding: 24px; }
  .cs-thumb-title { font-size: 24px; }
}

/* Screen-reader-only text. Used where a card's visible label has been removed
   in favour of imagery: the card is still a link, so it still needs a name. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Background clip on the "Designing for user quality" card. Same treatment as
   the other card clips; translateZ forces its own compositing layer, without
   which Chrome squares off the card's rounded corners during playback. */
.uq-card video.uq-card-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  transform: translateZ(0);
  pointer-events: none;
}

/* Background clip on the VF card. translateZ forces its own compositing
   layer, without which Chrome squares off the card's rounded corners during
   playback. */
.vf-card video.vf-card-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 1;
  object-fit: cover;
  border-radius: inherit;
  transform: translateZ(0);
  pointer-events: none;
}

/* Ready for the VF mark once the asset lands. position:relative keeps it above
   the clip;

/* Background clip on the AI-optimisation card. No scrim on this one by
   request — the title sits directly on the video. translateZ forces its own
   compositing layer, without which Chrome squares off the card's rounded
   corners during playback. */
.ai-card video.ai-card-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 1;
  object-fit: cover;
  border-radius: inherit;
  transform: translateZ(0);
  pointer-events: none;
}

/* position:relative promotes the logo above the scrim. The scrim is absolutely
   positioned and so paints after in-flow content — without this it would cover
   the logo entirely. Both are z-index auto{
  position: relative;
  width: 68%;
  max-width: 176px;
  height: auto;
}

/* ── Selected UI Snapshots card ── */
/* Background comes from .bento-card; a hardcoded #1a1a1a here made the card
   black on a light page before its clip decoded. */
.uis-card {
  position: relative;
  overflow: hidden;
}

/* Hidden layers: dashed border{
  border-style: dashed;
  border-color: rgba(255, 255, 255, 0.08);
}

@keyframes uis-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

/* Scrim between the clip and the title. #2c2c2c is the same value as
   --color-primitive-neutral-750; written as rgba because the token stores a
   hex{
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(44, 44, 44, 0.6);
  border-radius: inherit;
  pointer-events: none;
}

/* Background clip on the card face. translateZ forces its own compositing
   layer — without one Chrome hands a playing video to a hardware overlay that
   ignores the parent's border-radius, so the corners square off mid-playback. */
.uis-card-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  transform: translateZ(0);
  pointer-events: none;
}

.uis-card-title {
  /* Instrument Serif ships italic-only here, so font-style must be set
     explicitly — without it the browser synthesises a slanted upright. */
  font-family: var(--font-family-instrument);
  font-style: italic;
  font-size: 32px;
  font-weight: 400;
  line-height: 1.05;
  color: #f5f5f5;
  text-align: center;
  white-space: pre-line;
  margin: 0;
  /* Promotes the title to a positioned element so it paints after the scrim
     ::before above. Without this the scrim — absolutely positioned, and so
     painted after in-flow content — would cover the text entirely. Both are
     z-index auto, so DOM order decides and the title wins. */
  position: relative;
}

.uis-hero-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.uis-hero-wrap {
  border-radius: var(--radius-card);
  overflow: hidden;
  margin-top: 40px;
}

.uis-hero-wrap .cs-hero-video {
  margin-top: 0;
}

.uis-hero-overlay {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  background: #1a1a1a;
}

.uis-fly-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  pointer-events: none;
}

/* Was the legacy teal #0d9488{
  background: var(--color-primitive-blue-600);
  border-color: var(--color-primitive-blue-600);
  color: var(--color-primitive-neutral-0);
}

/* Touch height. Has to live here rather than up in the mobile block: the
   base rule above uses the `padding` shorthand and comes later in the file,
   and a media query adds no specificity, so an earlier override loses. */
@media (max-width: 768px) {
  .uis-toggle-btn {
    padding-top: 14px;      /* 28 → 44px tall */
    padding-bottom: 14px;
  }
}

@keyframes outbe-slide-in {
  from {
    opacity: 0;
    transform: translateX(-20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

/* ── Mobile responsive ── */
@media (max-width: 768px) {
  .outbe-workflow-card {
    flex-direction: column;
  }

  .outbe-workflow-arrow {
    transform: rotate(90deg);
    justify-content: center;
  }

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

  .outbe-token-name {
    min-width: 0;
    font-size: 9px;
  }

  .outbe-token-tier-desc {
    display: none;
  }

  .cs-meta {
    flex-direction: column;
  }
}

/* Card faces sit on each card's own background. ai-card and vf-card
   are black{
  color: var(--color-primitive-neutral-0);
}

/* outbe-card's surface is the terracotta accent{
  color: var(--color-primitive-neutral-950);
}

/* Semantic outcome chips. Applied by MEANING, not by sign — a metric going
   down can be the good result. Only case-study number chips use these;
   neutral/descriptive figures keep the default accent chip. */
.cs-metric--positive {
  background: rgba(102, 160, 81, 0.16);
  border-color: transparent;
  color: var(--color-positive-text);
}

/* Clearly red rather than brick. The previous #E56868 / #A83A3A pair sat
   close enough to the terracotta brand accent that a negative metric and a
   neutral one were hard to tell apart. */
.cs-metric--negative {
  background: rgba(214, 51, 51, 0.15);
  border-color: transparent;
  color: var(--color-negative-text);
}

/* ══════════════════════════════════════
   OUTBE HUB — visuals-only bento
   Media tiles only: no body copy, no captions, no section headings.
   Tiles reuse the shared card surface and the home-page hover treatment.
══════════════════════════════════════ */
.cs-bento {
  /* Rows, not a fixed column grid.
     A row that mixes a phone capture with a desktop one has to end up the same
     height on both, and neither may be cropped. Those two constraints fix the
     widths: at a shared height H an uncropped tile is H x ratio wide, so the
     widths must be in proportion to the ratios, which is not what equal
     quarter-columns give you. Each tile therefore grows in proportion to its
     own aspect-ratio (see --tile-ratio below) and equal height falls out of
     the arithmetic rather than being forced. */
  display: flex;
  flex-direction: column;
  gap: 16px;
  /* Matches the collapsible/graph footprint, so the grid lines up with
     everything else in the article. */
  width: calc(680px + 2 * 16px);
  max-width: 100%;
  margin: var(--space-media) auto 0;
}

/* A tile placed straight into .cs-bento takes the full width — that is the
   outbe case, whose captures are all the same shape and need no row. */
.cs-bento-row {
  display: flex;
  gap: 16px;
}

/* flex-basis 0 with grow set to the clip's aspect-ratio: widths come out
   proportional to the ratios, so every tile in the row resolves to the same
   height with nothing cropped. min-width:0 stops a wide clip from refusing to
   shrink below its intrinsic size. */
.cs-bento-row > .cs-tile {
  flex: var(--tile-ratio) 1 0;
  min-width: 0;
}

.cs-tile {
  /* No padding: the media is the tile. Height comes from the media's own
     ratio — set inline for video tiles, intrinsic for images — so it can
     never be cropped. overflow clips the corners to the card radius. */
  display: block;
  overflow: hidden;
  padding: 0;
  text-align: inherit;
  font: inherit;
  color: inherit;
  background: var(--color-surface-card);
  border: 1px solid var(--color-border-card);
  border-radius: var(--radius-card);
  box-shadow:
    0 1px 3px var(--color-shadow-card-rest),
    0 1px 2px var(--color-shadow-card-rest),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
  transition: background 0.3s ease, border-color 0.3s ease, transform 0.15s ease;
}

/* Same hover treatment as the home-page cards */
/* Only a tile that actually opens something gets a pointer and a lift. The
   image-only case study's tiles are not interactive, and a hover that says
   otherwise is a promise the page does not keep. */
.cs-tile--interactive {
  cursor: pointer;
}

/* The same hover as a home-page card, to the pixel. */
.cs-tile--interactive:hover {
  background: var(--color-surface-card-hover);
  border-color: var(--color-border-card-hover);
  box-shadow:
    0 12px 40px var(--color-shadow-card-hover),
    0 4px 12px var(--color-shadow-card-hover-2),
    inset 0 1px 0 var(--color-card-inset-top),
    inset 0 -1px 0 var(--color-card-inset-bottom);
  transform: translateY(-3px);
}

.cs-tile--interactive:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 3px;
}

/* ── Full-screen viewer for a snapshot ────────────────────────────────── */
/* The cursor tooltip belongs to the tile underneath. It would otherwise hang
   over the dialog until the next mouse move, since nothing has re-entered a
   new element yet. */
body:has(.uis-lightbox) .cursor-tooltip {
  opacity: 0 !important;
}

.uis-lightbox {
  position: fixed;
  inset: 0;
  /* Above the expanded case study (500) and below the custom cursor (9999),
     so the dialog covers the page it was opened from but the cursor still
     draws on top of it. */
  z-index: 900;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 48px 24px;
  box-sizing: border-box;
  background: var(--color-surface-lightbox);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  animation: uis-lightbox-in 0.2s ease;
}

@keyframes uis-lightbox-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .uis-lightbox { animation: none; }
}

.uis-lightbox-figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  max-width: 100%;
  min-height: 0;
}

/* The clip keeps its own ratio and is bounded on both axes, so a tall phone
   capture is limited by height and a wide desktop one by width. */
.uis-lightbox-media {
  display: block;
  max-width: min(1200px, 92vw);
  max-height: 78vh;
  width: auto;
  height: auto;
  border-radius: var(--radius-card);
  background: var(--color-surface-card);
}

.uis-lightbox-caption {
  max-width: 640px;
  text-align: center;
  font-family: var(--font-family-primary);
  font-size: var(--text-3xl);
  line-height: var(--leading-relaxed);
  color: var(--color-text-on-lightbox);
}

.uis-lightbox-close {
  position: absolute;
  top: 24px;
  right: 24px;
  width: 40px;
  height: 40px;
  border-radius: 100px;
  border: 1px solid var(--color-border-card);
  background: var(--color-surface-action-icon);
  color: var(--color-text-primary);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s ease;
}

.uis-lightbox-close:hover {
  background: var(--color-surface-card-hover);
}

.uis-lightbox-close:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

@media (max-width: 768px) {
  .uis-lightbox { padding: 64px 16px 24px; }
  .uis-lightbox-media { max-height: 66vh; }
  .uis-lightbox-caption { font-size: var(--text-2xl, 16px); }
}

/* width:100% + height:auto is the whole no-crop guarantee. The width/height
   attributes on the element give the browser the intrinsic ratio up front,
   so the row reserves correct height before the image decodes and nothing
   jumps on load. */
.cs-tile-img {
  display: block;
  width: 100%;
  height: auto;
}

/* The tile already carries the clip's exact aspect-ratio, so filling it can
   never crop. translateZ(0) is required: Chrome hands a playing video to a
   hardware overlay that ignores the parent's border-radius, and the corners
   come back square without it. */
.cs-tile-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  transform: translateZ(0);
}

@media (max-width: 768px) {
  /* Already one column; mobile only needs the gutter and gap corrected.
     `.cs-body` runs full-bleed at this width and its children carry their own
     gutters, so width:100% would push the tiles to the screen edges while the
     copy stayed inset. Graphs, collapsibles and video frames all measure
     24→366 at 390px; this matches them. Percentage rather than 100vw so it
     stays correct if the grid is ever nested. */
  .cs-bento {
    width: calc(100% - 48px);
    max-width: 680px;
    gap: 12px;
  }

  /* Rows unstack. A phone capture beside a desktop one at this width leaves
     each about 80px across, which is unreadable, so every tile takes the full
     width and the page becomes one column. */
  .cs-bento-row {
    flex-direction: column;
  }

  .cs-bento-row > .cs-tile {
    flex: 1 1 auto;
  }
}


/* ══════════════════════════════════════
   BOTTOM FURNITURE + TOUCH TARGETS — tablet and phone
   Appended last on purpose: these override the sizes and offsets set in the
   768px block above, and the 768px section here must follow the 1109px one.

   Three floating controls share the bottom of the screen once the viewport
   narrows: the section-nav dock (centred, up to 94vw wide), the back-to-top
   button (right: 32px) and — below 768px — the site nav itself. Hit-testing
   found the button was dead on every phone and small tablet: a tap at its
   centre landed on .cs-nav-dock at 390px and on a .cs-section-pill at 820px,
   because both carry z-index: 20 and the dock is painted later. The dock also
   sat 45px on top of the site nav at ≤768px (dock 761–820, nav 775–832 in an
   844px viewport).

   The fix is positional, not a z-index bump — raising the button would make
   it clickable while still drawing it on top of the dock. Each control is
   stacked clear of the one below it, so the offsets below are sums of
   measured heights rather than round numbers.
══════════════════════════════════════ */

@media (max-width: 1109px) {
  /* Tablets are touch devices, so they get the phone's 44px floor. Measured
     before this rule at 769–1024: nav pills 53×35 / 78×35 / 71×35 and
     .cs-header-close 40×40, all under the 44px minimum. */
  .action-icon,
  .cs-header-close,
  .cs-back-to-top,
  .uis-lightbox-close {
    width: 44px;
    height: 44px;
  }
  .nav-pill {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* Same floor for the two controls that only had it below 768px. The
     section pills measured 33px tall and the slider handle 40px wide at
     769–1024, which is squarely tablet territory. Padding rather than
     height on the pill, because a later rule sets its padding with the
     shorthand and would otherwise reset this. */
  .cs-section-pill {
    padding-top: 14px;
    padding-bottom: 14px;
  }
  .ba-slider-handle {
    width: 44px;
  }

  /* Dock bottom 24px + measured dock height 47px + 12px clear. */
  .cs-back-to-top {
    bottom: 83px;
  }

  /* 12px is the floor for text meant to be read on a handheld. Measured
     before this rule: chart values and tags rendered at 9px ("62.00%",
     "4–6", "Native") and the quote-card label at 11px. Desktop keeps its
     tighter sizes — those are read at arm's length with a pointer. */
  :root {
    --graph-chip-size: 12px;
    /* 10px, not 12: measured at 12px, 12 tags clipped at 390px and 30 at
       820px, because a tag can never be wider than its bar's fill. 10px is
       the largest that adds no clipping the 9px baseline didn't already have.
       The values — the numbers actually being read — do reach 12px. */
    --graph-tag-size: 10px;
    --principle-size: 12px;
  }

  /* Long tags ("Did not watch", "Uploaded video") were being cut off inside
     narrow fills even at the old 9px, because a later rule pins them to
     nowrap. Small screens have the vertical room to wrap instead — the
     mobile track is 44px tall — so let them. */
  .graph-bar-tag {
    white-space: normal;
  }
}

@media (max-width: 768px) {
  /* The site nav becomes a floating pill bar 12px off the bottom, 57px tall,
     so the dock starts 12px above its top edge: 12 + 57 + 12. */
  .cs-nav-dock {
    bottom: calc(81px + env(safe-area-inset-bottom, 0px));
  }
  /* Dock offset above + measured dock height 59px in this band + 12px clear. */
  .cs-back-to-top {
    bottom: calc(152px + env(safe-area-inset-bottom, 0px));
  }
}
