@font-face {
  font-family: 'Whiterock';
  src: url('/Whiterock.otf') format('opentype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

:root {
  --basalt: #0D0D0E;
  --panel: #17171A;
  --ember: #E4622C;
  --teal: #7FB8B3;
  --teal-dark: #3F6B68;
  --amber: #F0A94E;
  --text: #E8E6E3;
  --text-dim: #8A8A8D;
  --text-dim-strong: #A6A6AD;   /* dim text that must still read on glass panels */

  /* Category colors moved to works.json (applied inline as --cat/--chip by main.js).
     --rose stays: it drives form error styling. */
  --rose: #E07A8A;

  --font-display: 'Whiterock', serif;
  --font-body: 'Sora', sans-serif;
  --font-data: 'IBM Plex Mono', monospace;
  /* pull-quote voice: Fraunces — warm old-style serif, optically sized,
     distinct from Whiterock (headings) and Sora (body). Loaded from
     Google Fonts in the <head> link with its full variable axes:
     SOFT 0..100 and WONK 0..1. WONK=1 bends the quotes' letterforms
     (b wonky alternates, playful baseline shifts) — the fun dial this
     section turns on. */
  --font-quote: 'Fraunces', Georgia, serif;
  --mono-tracking: 0.06em;

  --spacing-xs: clamp(0.5rem, 1vw, 0.75rem);
  --spacing-sm: clamp(0.75rem, 1.5vw, 1rem);
  --spacing-md: clamp(1rem, 2vw, 1.5rem);
  --spacing-lg: clamp(1.5rem, 3vw, 2.5rem);
  --spacing-xl: clamp(2rem, 4vw, 3.5rem);
  --spacing-2xl: clamp(3rem, 6vw, 5rem);

  --radius-full: 999px;
  --radius-lg: 16px;
  --radius-md: 12px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }

/* Remove the browser's grey tap-flash on touch devices */
html {
  /* The ember tap-highlight square flashed on every touch press — felt
     non-standard and janky. Taps now give feedback through :active states
     (buttons already scale/glow) and the hero strip suppresses its own. */
  -webkit-tap-highlight-color: transparent;
  -webkit-text-size-adjust: 100%;
}

html {
  scrollbar-color: rgba(232,230,227,0.2) rgba(13,13,14,0.8);
  scrollbar-width: thin;
  overflow-x: clip;
  max-width: 100%;
}
@supports not (overflow: clip) {
  html, body { overflow-x: hidden; }
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: rgba(13,13,14,0.8); }
::-webkit-scrollbar-thumb { background: rgba(232,230,227,0.2); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(232,230,227,0.35); }

::selection { background: rgba(228,98,44,0.35); color: var(--text); }
::-moz-selection { background: rgba(228,98,44,0.35); color: var(--text); }

:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 3px;
}

body {
  font-family: var(--font-body);
  background: var(--basalt);
  color: var(--text);
  line-height: 1.6;
  overflow-x: hidden;
  position: relative;
  max-width: 100%;
  padding-top: 60px; /* match fixed nav height */
}

body::before {
  content: '';
  position: fixed;
  top: -50%; left: -25%;
  width: 150%; height: 150%;
  background: radial-gradient(circle at 30% 20%, rgba(228,98,44,0.08) 0%, transparent 50%);
  pointer-events: none;
  z-index: 0;
  animation: breathe 18s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

@keyframes breathe {
  0%, 100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.85;
    transform: scale(1.04);
  }
}

body::after {
  content: '';
  position: fixed;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 200px;
  opacity: 0.035;
  pointer-events: none;
  z-index: 0;
  mix-blend-mode: overlay;
}

body > * { position: relative; z-index: 1; }

/* ======================================================================
   BOOT VEIL — content-loading gate
   <html class="boot"> ships in the static HTML, so the very first paint
   is this branded veil, never the fallback content. main.js fetches
   works.json + site.json, applies EVERY patch (text, hero picks, stats),
   then removes .boot — the veil fades out over the finished page in one
   controlled reveal. Two independent failsafes so nobody is trapped:
   1. JS timer: main.js reveals the static defaults if the fetch hangs
      (same 3.5s budget the hero entrance has always used).
   2. Pure CSS: if the script never runs at all (404, JS off, CSP break),
      an animation removes the veil at 3.6s — one beat after the JS timer,
      so the JS path (which also removes .boot) always wins the race when
      it is alive. Reduced-motion and no-animation environments still get
      the animation (it is the failsafe's whole point), but instantly.
   The veil also masks the FOUC of webfonts (Sora/Whiterock swap) since
   the page underneath is fully formed before the veil lifts.
   ====================================================================== */
/* NOTE: no overflow:hidden here on purpose — if the script is dead the
   .boot class never comes off <html>, and a scroll lock would trap the
   no-JS failsafe path. The fixed full-viewport veil covers the content
   visually; a sub-3.5s scroll underneath is harmless. */
.boot-veil {
  position: fixed;
  inset: 0;
  z-index: 2000;                                 /* above nav(100), lightbox(1000) */
  background: var(--basalt);
  display: grid;
  place-items: center;
  /* the site's ambient ember breathe, already running underneath — echo it
     so the veil is not a flat black cut, but a brand moment */
  background-image: radial-gradient(circle at 30% 20%, rgba(228,98,44,0.08) 0%, transparent 50%);
  transition: opacity 420ms cubic-bezier(0.4, 0, 0.2, 1), visibility 0s 420ms;
}
.boot-veil .boot-mark {
  opacity: 0.9;
  animation: bootPulse 1.6s ease-in-out infinite;
  filter: drop-shadow(0 0 18px rgba(228,98,44,0.4));
}
@keyframes bootPulse {
  0%, 100% { opacity: 0.9; transform: scale(1); }
  50%      { opacity: 0.55; transform: scale(0.96); }
}
/* reveal: JS removes .boot from <html> */
html:not(.boot) .boot-veil { opacity: 0; visibility: hidden; pointer-events: none; }
/* CSS-only failsafe: if main.js never removes .boot, fade the veil away
   after 3.6s. Runs regardless of prefers-reduced-motion (being trapped is
   worse than one fade); duration kept at 420ms to match the reveal. */
html.boot .boot-veil {
  animation: bootFailSafe 420ms cubic-bezier(0.4, 0, 0.2, 1) 3.6s forwards;
}
@keyframes bootFailSafe {
  to { opacity: 0; visibility: hidden; }
}
/* after the failsafe the veil must not eat clicks even though .boot
   remains on <html> (script dead) */
html.boot .boot-veil { pointer-events: none; }

/* ======================================================================
   NAV
   ====================================================================== */
nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  width: 100%;
  z-index: 100;
  background: rgba(13,13,14,0.65);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border-bottom: 1px solid rgba(232,230,227,0.06);
  transition: border-color 500ms cubic-bezier(0.4,0,0.2,1),
              background 500ms cubic-bezier(0.4,0,0.2,1),
              box-shadow 500ms cubic-bezier(0.4,0,0.2,1);
}
nav.scrolled {
  background: rgba(13,13,14,0.92);
  border-bottom-color: rgba(228,98,44,0.2);
  box-shadow: 0 4px 32px rgba(0,0,0,0.4),
              0 1px 0 rgba(228,98,44,0.1),
              0 8px 16px rgba(228,98,44,0.04);
}

.nav-container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 0 var(--spacing-lg);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-md);
  height: 60px;
}

.nav-logo {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  text-decoration: none;
  color: var(--text);
  transition: transform 200ms cubic-bezier(0.4,0,0.2,1);
}
.nav-logo:active { transform: scale(0.97); }

.nav-logo-mark {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  transition: transform 300ms cubic-bezier(0.34,1.56,0.64,1), filter 300ms ease;
  animation: glowPulse 6s ease-in-out infinite;
  object-fit: contain;
  display: block;
}
.nav-logo:hover .nav-logo-mark {
  transform: scale(1.12) rotate(-4deg);
  filter: drop-shadow(0 0 12px rgba(228,98,44,0.5));
}

.nav-logo-text {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(1.1rem, 2vw, 1.3rem);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.nav-logo-text .apostrophe { color: var(--ember); }
.nav-logo-text .studio-suffix {
  font-family: var(--font-data);
  font-size: 0.7em;
  opacity: 0.4;
  margin-left: 0.15em;
  letter-spacing: 0.04em;
}

.nav-links {
  display: flex;
  gap: var(--spacing-md);
  list-style: none;
  align-items: center;
}
.nav-links li { position: relative; }

.nav-links a {
  font-family: var(--font-data);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: var(--mono-tracking);
  text-transform: uppercase;
  color: var(--text-dim);
  text-decoration: none;
  padding: 0.375rem 0.25rem;
  position: relative;
  display: inline-flex;
  align-items: center;
  transition: color 200ms cubic-bezier(0.4,0,0.2,1);
}
.nav-links a::after {
  content: '';
  position: absolute;
  bottom: -2px; left: 0;
  width: 100%; height: 2px;
  background: var(--ember);
  border-radius: 1px;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 250ms cubic-bezier(0.4,0,0.2,1);
  pointer-events: none;
}
.nav-links a:hover, .nav-links a:focus-visible { color: var(--text); }
.nav-links a:hover::after, .nav-links a:focus-visible::after { transform: scaleX(1); }
.nav-links a.active { color: var(--ember); }
.nav-links a.active::after { transform: scaleX(1); background: var(--ember); }

.nav-hamburger {
  display: none;
  background: none;
  border: 1px solid rgba(232,230,227,0.1);
  color: var(--text);
  width: 44px; height: 44px;
  cursor: pointer;
  padding: 0;
  border-radius: var(--radius-md);
  position: relative;
  transition: border-color 200ms ease, background 200ms ease;
}
.nav-hamburger:hover { border-color: rgba(232,230,227,0.25); background: rgba(232,230,227,0.05); }
.nav-hamburger:active { transform: scale(0.94); }

.nav-hamburger-icon {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 20px; height: 14px;
  position: relative;
  margin: auto;
}
.nav-hamburger-icon span {
  display: block;
  width: 100%; height: 2px;
  background: var(--text);
  border-radius: 2px;
  transition: transform 280ms cubic-bezier(0.4,0,0.2,1), opacity 200ms ease;
  position: absolute;
  left: 0;
}
.nav-hamburger-icon span:nth-child(1) { top: 0; }
.nav-hamburger-icon span:nth-child(2) { top: 50%; transform: translateY(-50%); }
.nav-hamburger-icon span:nth-child(3) { bottom: 0; }
.nav-hamburger[aria-expanded="true"] .nav-hamburger-icon span:nth-child(1) { top: 50%; transform: translateY(-50%) rotate(45deg); }
.nav-hamburger[aria-expanded="true"] .nav-hamburger-icon span:nth-child(2) { opacity: 0; }
.nav-hamburger[aria-expanded="true"] .nav-hamburger-icon span:nth-child(3) { bottom: 50%; transform: translateY(50%) rotate(-45deg); }

.nav-backdrop { display: none; }

/* Nav hairline: a static 2px ember thread under the nav that lights up
   when the page is scrolled and dims back at the top. No fill, no progress
   — this page is short enough that a progress bar felt heavy. */
.scroll-progress {
  position: fixed;
  top: 60px; left: 0;
  height: 2px;
  width: 100%;
  background: linear-gradient(to right, transparent, var(--ember) 20%, var(--amber) 50%, var(--ember) 80%, transparent);
  z-index: 101;
  pointer-events: none;
  opacity: 0;
  transition: opacity 450ms cubic-bezier(0.4, 0, 0.2, 1);
}
.scroll-progress.scrolled { opacity: 0.55; }

@keyframes glowPulse {
  0%, 100% { filter: drop-shadow(0 0 8px rgba(228,98,44,0.3)); }
  50%      { filter: drop-shadow(0 0 16px rgba(228,98,44,0.5)); }
}

/* ======================================================================
   HERO
   ====================================================================== */
.hero {
  max-width: 1400px;
  margin: 0 auto;
  padding: var(--spacing-2xl) var(--spacing-lg);
  display: grid;
  grid-template-columns: 1fr 1.4fr 1fr;
  gap: var(--spacing-md);
  min-height: clamp(550px, 75vh, 750px);
  align-content: center;
  position: relative;
  overflow-x: visible;
  overflow-y: visible;
  scroll-margin-top: 72px;
}

.hero::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: min(700px, 90vw); height: min(700px, 90vw);
  background: radial-gradient(ellipse 65% 55% at 50% 50%, rgba(228,98,44,0.16) 0%, rgba(240,169,78,0.09) 32%, rgba(228,98,44,0.04) 55%, transparent 70%);
  transform: translate(-50%, calc(-50% + var(--scroll-y, 0px) * 0.08));
  pointer-events: none;
  animation: heroGlowPulse 6s ease-in-out infinite;
  z-index: 0;
}

@keyframes heroGlowPulse {
  0%, 100% { opacity: 0.6; transform: translate(-50%, calc(-50% + var(--scroll-y, 0px) * 0.08)) scale(1); }
  50% { opacity: 1; transform: translate(-50%, calc(-50% + var(--scroll-y, 0px) * 0.08)) scale(1.1); }
}

.hero-eyebrow {
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 0.75rem;
  text-align: center;
}

/* The two words that carry the hook get a quiet ember glow, echoing the
   logo apostrophe. Purely decorative; reduced-motion users see it static. */
.hero-accent {
  color: var(--ember);
  text-shadow: 0 0 40px rgba(228,98,44,0.45);
  animation: accentBreathe 6s ease-in-out infinite;
}
@keyframes accentBreathe {
  0%, 100% { text-shadow: 0 0 40px rgba(228,98,44,0.45); }
  50%      { text-shadow: 0 0 64px rgba(228,98,44,0.7); }
}

.hero-content-centered {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  position: relative;
  z-index: 1;
}

.hero-content-centered h1 {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(2.25rem, 4.6vw, 3.85rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
  margin-bottom: var(--spacing-md);
  /* 24ch of Sora at hero size ≈ 860px — wider than a phone. Cap at the
     available column so the headline wraps on small screens instead of
     overflowing the viewport (the "text jumps right on load" bug was this:
     the fallback font fit, then the wide webfont blew the box out). */
  max-width: min(24ch, 100%);
  text-shadow: 0 0 60px rgba(228,98,44,0.4), 0 0 120px rgba(228,98,44,0.2);
  animation: fadeInUp 700ms cubic-bezier(0.2,0.7,0.3,1) both;
}

.hero-content-centered p {
  font-size: clamp(1.1rem, 2vw, 1.3rem);
  color: var(--text-dim);
  margin-bottom: var(--spacing-lg);
  max-width: min(45ch, 100%);
  animation: fadeInUp 700ms cubic-bezier(0.2,0.7,0.3,1) 120ms both;
}

.hero-ctas {
  display: flex;
  gap: var(--spacing-sm);
  flex-wrap: wrap;
  justify-content: center;
  animation: fadeInUp 700ms cubic-bezier(0.2,0.7,0.3,1) 240ms both;
}

.hero-wing-left,
.hero-wing-right {
  position: relative;
  align-self: center;
  z-index: 1;
}

.hero-card {
  position: absolute;
  border-radius: var(--radius-lg);
  overflow: hidden;
  width: clamp(200px, 17vw, 280px);
  aspect-ratio: 4/3;
  box-shadow: 0 16px 48px rgba(0,0,0,0.6);
  transition: transform 400ms ease, box-shadow 400ms ease, z-index 0s 0s;
}
.hero-card img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Hover lifts via transform only; the float loop runs on the independent
   translate/rotate properties, so scale COMPOSES with the drift instead
   of fighting it — no snap, no !important. Hover z-jump keeps the lifted
   card above its neighbors. Touch devices: the 768px block below nulls
   the transform (cards are pointer-events: none there anyway). */
.hero-card:hover {
  transform: scale(1.08);
  z-index: 10;
}

/* Old browsers without translate/rotate-property support: cards stay
   static and hover still works (transform keyframes would be no-ops).
   Rotation below is the DESIGNED BASE TILT — mirrored fan: left wing
   tilts +2/+1.5/-1.5 deg, right wing mirrors it -2/-1.5/+1.5, so the
   collage reads as a deliberate symmetrical arc, not random drift. */
@supports not (translate: 0 0) {
  .hero-card-1 { transform: rotate(1.5deg); }
  .hero-card-2 { transform: rotate(-6deg); }
  .hero-card-3 { transform: rotate(2.5deg); }
  .hero-card-4 { transform: rotate(6deg); }
  .hero-card-5 { transform: rotate(-2deg); }
  .hero-card-6 { transform: rotate(-1.5deg); }
}
/* The base tilt lives on the static `rotate` property — float keyframes
   only animate translate, so the tilt PERSISTS during floating, and
   hover scale composes cleanly on top (transform never touches rotate).
   SCHEME (per user): each wing's TOP cards mirror each other (+1.5/-1.5);
   each wing's BOTTOM PAIR leans AGAINST each other with the SAME visual
   orientation on both sides — tops tip outward (left-card tips left,
   right-card tips right). Signs are equal, not mirrored, because the
   wings' POSITIONS are already mirrored; mirroring the angles too
   inverted the lean on the right (tops converging) vs the left (tent).
   Left: card2 -2 / card3 +2.5. Right: card5 -2 / card6 +2.5. */
.hero-card-1 { rotate: 1.5deg; }
.hero-card-2 { rotate: -6deg; }
.hero-card-3 { rotate: 2.5deg; }
.hero-card-4 { rotate: 6deg; }
.hero-card-5 { rotate: -2deg; }
.hero-card-6 { rotate: -1.5deg; }

/* Base resting halos derive from each card's --hero-tint (set by main.js
   from the artwork's category color); these static values are only the
   pre-JS defaults while works.json loads (and for the 4 static cards
   whose palette is known from index.html). */
.hero-card[data-tint] { box-shadow: 0 16px 48px rgba(0,0,0,0.55), 0 0 0 1px rgba(0,0,0,0.3); }
@supports (color: color-mix(in srgb, red 10%, blue)) {
  .hero-card[data-tint] {
    box-shadow: 0 16px 48px color-mix(in srgb, var(--hero-tint) 22%, rgba(0,0,0,0.55)),
                0 0 0 1px color-mix(in srgb, var(--hero-tint) 30%, transparent);
  }
}

/* Left wing cards — staggered arc: top, middle-high, bottom */
.hero-card-1 {
  top: 0%;
  right: 10%;
  z-index: 3;
  animation: float1 6s ease-in-out infinite;
}
.hero-card-2 {
  top: 30%;
  right: 37%;
  z-index: 2;
  animation: float2 7s ease-in-out infinite;
}
.hero-card-3 {
  bottom: 2%;
  right: 25%;
  z-index: 1;
  animation: float3 8s ease-in-out infinite;
}

/* Right wing cards */
.hero-card-4 {
  top: 5%;
  left: 37%;
  z-index: 3;
  animation: float1 7s ease-in-out infinite;
}
.hero-card-5 {
  top: 33%;
  left: 5%;
  z-index: 2;
  animation: float2 8s ease-in-out infinite;
}
.hero-card-6 {
  bottom: 5%;
  left: 25%;
  z-index: 1;
  animation: float3 6s ease-in-out infinite;
}

/* HOVER GLOW — from the card's --tint (the CATEGORY COLOR of the artwork
   shown, set by main.js; falls back to ember). Old bug: color-mix with
   currentColor — .hero-card never sets color, so it inherited body text
   and EVERY hover glow turned white. var(--tint) is the actual halo hue.
   Fallback for browsers without color-mix: plain tint-colored shadow. */
.hero-card:hover {
  box-shadow: 0 24px 64px rgba(0,0,0,0.8),
              0 0 48px var(--hero-glow, rgba(228,98,44,0.35));
}
@supports (color: color-mix(in srgb, red 10%, blue)) {
  .hero-card:hover {
    box-shadow: 0 24px 64px color-mix(in srgb, var(--hero-tint, #E4622C) 45%, rgba(0,0,0,0.8)),
                0 0 48px color-mix(in srgb, var(--hero-tint, #E4622C) 35%, transparent);
  }
}

/* ===== FLOAT MOTION =====
   Keyframes animate ONLY the independent `translate` property: the static
   `rotate` (base tilt, below) stays applied during the animation — an
   earlier draft set rotate in the keyframes too, which flattened the
   designed tilt while animating. Hover's transform: scale() composes on
   top of both (it never touches translate/rotate), so no snap, no
   !important. Amplitudes match the ORIGINAL design (15/20/12px drift)
   — noticeable, ambient, not distracting. Support floor: Chrome 104+/
   Safari 14.1+/Firefox 72+. */
@keyframes float1 { 0%, 100% { translate: 0 0; } 50% { translate: 0 -15px; } }
@keyframes float2 { 0%, 100% { translate: 0 0; } 50% { translate: 0 -20px; } }
@keyframes float3 { 0%, 100% { translate: 0 -4px; } 50% { translate: 0 -12px; } }

@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }

/* ===== HERO ENTRANCE (cards) =====
   Cards start hidden (opacity/filter only — paint still happens, so LCP
   candidacy is preserved; display/visibility would break that) and reveal
   in a stagger AFTER initHeroShuffle has applied the daily/pinned picks —
   the same gate that also masks the default-art swap flash. main.js adds
   .hero-entered to <section class="hero"> when the picks are in (or the
   fetch failed — the fallback path also enters, so a failed works.json
   never leaves the hero invisible). One-shot: the class is only added by
   the reveal gate, never by re-runs (admin preview pushes re-init the
   shuffle but do NOT re-animate). prefers-reduced-motion gets everything
   visible immediately (block at the bottom of the sheet). */
.hero:not(.hero-entered) .hero-card {
  opacity: 0;
  filter: blur(8px);
}
/* ENTRANCE + FLOAT COMPOSE. A previous draft put the entrance on
   .hero.hero-entered .hero-card with a bare animation shorthand — that
   OVERRODE the per-card float loops (higher specificity), and after the
   one-shot entrance finished the cards sat dead with translate: none.
   Fix: each entered card runs BOTH animations — heroCardIn animates
   opacity/filter, floatN animates translate — disjoint properties, so
   they compose. Float starts immediately (cards drift while fading in,
   which reads as settling into place, not a freeze). */
.hero.hero-entered .hero-card {
  animation: heroCardIn 700ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
.hero.hero-entered .hero-card-1 { animation: heroCardIn 700ms cubic-bezier(0.2, 0.7, 0.3, 1) 300ms both, float1 6s ease-in-out infinite 900ms; }
.hero.hero-entered .hero-card-2 { animation: heroCardIn 700ms cubic-bezier(0.2, 0.7, 0.3, 1) 380ms both, float2 7s ease-in-out infinite 1000ms; }
.hero.hero-entered .hero-card-3 { animation: heroCardIn 700ms cubic-bezier(0.2, 0.7, 0.3, 1) 460ms both, float3 8s ease-in-out infinite 1100ms; }
.hero.hero-entered .hero-card-4 { animation: heroCardIn 700ms cubic-bezier(0.2, 0.7, 0.3, 1) 340ms both, float1 7s ease-in-out infinite 950ms; }
.hero.hero-entered .hero-card-5 { animation: heroCardIn 700ms cubic-bezier(0.2, 0.7, 0.3, 1) 420ms both, float2 8s ease-in-out infinite 1050ms; }
.hero.hero-entered .hero-card-6 { animation: heroCardIn 700ms cubic-bezier(0.2, 0.7, 0.3, 1) 500ms both, float3 6s ease-in-out infinite 1150ms; }
@keyframes heroCardIn {
  from { opacity: 0; filter: blur(8px); }
  to   { opacity: 1; filter: blur(0); }
}
/* Browsers without filter support (or forced-colors) keep opacity-only */
@supports not (filter: blur(1px)) {
  .hero:not(.hero-entered) .hero-card { filter: none; }
  @keyframes heroCardIn { from { opacity: 0; } to { opacity: 1; } }
}

/* Scroll cue: fills the quiet band under the CTAs, matching the nav
   hairline aesthetic (1px ember line + mono chevron). Pure decoration;
   reduced-motion removes the bob. */
.hero-scroll-cue {
  margin-top: var(--spacing-xl);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  color: var(--text-dim);
  animation: fadeInUp 700ms cubic-bezier(0.2,0.7,0.3,1) 360ms both;
}
.hero-scroll-cue .cue-line {
  width: 1px;
  height: 28px;
  background: linear-gradient(to bottom, transparent, rgba(228,98,44,0.55));
}
.hero-scroll-cue .cue-chevron {
  font-family: var(--font-data);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  animation: cueBob 2.4s ease-in-out infinite;
}
@keyframes cueBob {
  0%, 100% { transform: translateY(0); opacity: 0.7; }
  50%      { transform: translateY(4px); opacity: 1; }
}
/* Phone: the strip already fills the space under the CTAs — hide the cue */
@media (max-width: 768px) {
  .hero-scroll-cue { display: none; }
}

/* ======================================================================
   BUTTONS
   ====================================================================== */
.btn {
  font-family: var(--font-data);
  font-size: 0.875rem;
  font-weight: 600;
  padding: 0.875rem 1.75rem;
  border-radius: var(--radius-full);
  text-decoration: none;
  transition: box-shadow 250ms cubic-bezier(0.4,0,0.2,1),
              background-color 250ms cubic-bezier(0.4,0,0.2,1),
              color 250ms cubic-bezier(0.4,0,0.2,1),
              transform 150ms cubic-bezier(0.4,0,0.2,1),
              border-color 250ms cubic-bezier(0.4,0,0.2,1);
  border: 2px solid transparent;
  cursor: pointer;
}
.btn-primary {
  background: var(--ember);
  color: var(--basalt);
  box-shadow: 0 0 24px rgba(228,98,44,0.35), 0 4px 16px rgba(228,98,44,0.25);
}
.btn-primary:hover { box-shadow: 0 0 48px rgba(228,98,44,0.6), 0 8px 32px rgba(228,98,44,0.4); transform: translateY(-2px); }
.btn-primary:active { transform: translateY(0) scale(0.97); box-shadow: 0 0 16px rgba(228,98,44,0.3), 0 2px 8px rgba(228,98,44,0.2); transition-duration: 80ms; }
.btn-secondary {
  background: transparent;
  color: var(--teal);
  border-color: var(--teal);
  box-shadow: 0 0 16px rgba(127,184,179,0.2);
}
.btn-secondary:hover { background: rgba(127,184,179,0.1); box-shadow: 0 0 40px rgba(127,184,179,0.4); transform: translateY(-2px); }
.btn-secondary:active { transform: translateY(0) scale(0.97); background: rgba(127,184,179,0.18); transition-duration: 80ms; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
.btn:disabled:hover { transform: none; box-shadow: none; }

/* ======================================================================
   PORTFOLIO SECTION
   ====================================================================== */
.portfolio-section {
  max-width: 1400px;
  margin: var(--spacing-2xl) auto;
  padding: 0 var(--spacing-lg) var(--spacing-2xl);
  position: relative;
  overflow-x: clip;
  scroll-margin-top: 72px;
}

.portfolio-section::before {
  content: '';
  position: absolute;
  top: calc(-1 * var(--spacing-2xl));
  left: 50%; transform: translateX(-50%);
  width: 80%; height: 1px;
  background: linear-gradient(to right, transparent, rgba(228,98,44,0.3) 30%, rgba(240,169,78,0.4) 50%, rgba(228,98,44,0.3) 70%, transparent);
}

.section-header { margin-bottom: var(--spacing-xl); }

.section-header h2 {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(2rem, 4vw, 3rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-bottom: var(--spacing-sm);
  text-shadow: 0 0 40px rgba(228,98,44,0.3), 0 0 80px rgba(228,98,44,0.15);
}

.section-subtitle {
  font-size: 1rem;
  color: var(--text-dim);
  max-width: min(55ch, 100%);
  margin-bottom: var(--spacing-md);
}

/* Filter buttons */
.filter-buttons {
  display: flex;
  gap: var(--spacing-sm);
  flex-wrap: wrap;
  max-width: 100%;
  width: 100%;
  box-sizing: border-box;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
.filter-btn {
  font-family: var(--font-data);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  padding: 0.45rem 0.8rem;
  background: var(--panel);
  color: var(--text-dim);
  border: 1px solid rgba(232,230,227,0.1);
  border-radius: var(--radius-full);
  cursor: pointer;
  transition: background-color 200ms cubic-bezier(0.4,0,0.2,1),
              color 200ms cubic-bezier(0.4,0,0.2,1),
              border-color 200ms cubic-bezier(0.4,0,0.2,1),
              box-shadow 200ms cubic-bezier(0.4,0,0.2,1);
  display: flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}
.filter-buttons { gap: 0.5rem; }
.filter-btn .count { font-size: 0.7rem; opacity: 0.6; }
.filter-btn:hover { color: var(--text); border-color: rgba(232,230,227,0.2); }
.filter-btn:active { transform: scale(0.96); transition-duration: 80ms; }

/* Chip glyphs: category icons with personality, tinted by the chip color.
   Slightly bigger now (16px) since the drawings have more detail; they
   lift and wiggle on hover; active chips show them in dark. */
.chip-glyph {
  display: inline-flex;
  width: 16px; height: 16px;
  flex-shrink: 0;
  color: var(--chip, var(--ember));
  opacity: 0.8;
  transition: transform 280ms cubic-bezier(0.34,1.56,0.64,1), opacity 200ms ease;
}
.chip-glyph svg { width: 100%; height: 100%; display: block; }
.filter-btn:hover .chip-glyph { transform: translateY(-1.5px) rotate(-8deg) scale(1.18); opacity: 1; }
.filter-btn.active .chip-glyph { color: var(--basalt); opacity: 0.9; }

/* ===== CYBERPUNK CHIP GLOW =====
   Intentional neon bleed: every chip emits a soft halo in its own color.
   The glow is layered (short bright core + long dim spread) so it reads as
   neon-on-dark rather than blur. On hover the halo brightens and the text
   picks up the color; the active chip holds a steady glow. Nothing is
   clipped — neighbors catch a faint wash, which is the point. */
.filter-btn {
  box-shadow:
    0 0 6px  color-mix(in srgb, var(--chip, var(--ember)) 22%, transparent),
    0 0 18px color-mix(in srgb, var(--chip, var(--ember)) 10%, transparent);
}
.filter-btn:hover {
  box-shadow:
    0 0 8px  color-mix(in srgb, var(--chip, var(--ember)) 55%, transparent),
    0 0 26px color-mix(in srgb, var(--chip, var(--ember)) 30%, transparent),
    0 0 48px color-mix(in srgb, var(--chip, var(--ember)) 12%, transparent);
}
.filter-btn.active {
  box-shadow:
    0 0 10px color-mix(in srgb, var(--chip, var(--ember)) 60%, transparent),
    0 0 32px color-mix(in srgb, var(--chip, var(--ember)) 35%, transparent);
}

/* FEATURED — the special one. Gold gradient fill, a warm neon halo, and a
   star that sparkles on hover. Reads as "the picks", not just another filter. */
.chip-featured {
  font-weight: 700;
  border-color: rgba(245,193,92,0.4);
  background: linear-gradient(135deg, rgba(245,193,92,0.14), rgba(228,98,44,0.10));
  box-shadow:
    0 0 8px  rgba(245,193,92,0.30),
    0 0 24px rgba(245,193,92,0.15),
    inset 0 0 0 1px rgba(245,193,92,0.08);
  animation: featuredHum 3.2s ease-in-out infinite; /* faint neon flicker */
}
.chip-featured .chip-glyph { color: #F5C15C; opacity: 1; }
.chip-featured:hover {
  border-color: rgba(245,193,92,0.65);
  box-shadow:
    0 0 12px rgba(245,193,92,0.55),
    0 0 36px rgba(245,193,92,0.30),
    0 0 64px rgba(245,193,92,0.12),
    inset 0 0 0 1px rgba(245,193,92,0.15);
}
.chip-featured:hover .chip-glyph { animation: starSparkle 900ms ease-in-out infinite; }
@keyframes starSparkle {
  0%, 100% { transform: scale(1.18) rotate(-8deg); filter: drop-shadow(0 0 2px rgba(245,193,92,0.8)); }
  50%      { transform: scale(1.32) rotate(-12deg); filter: drop-shadow(0 0 5px rgba(245,193,92,1)); }
}
@keyframes featuredHum {
  0%, 100% { box-shadow: 0 0 8px rgba(245,193,92,0.30), 0 0 24px rgba(245,193,92,0.15), inset 0 0 0 1px rgba(245,193,92,0.08); }
  50%      { box-shadow: 0 0 10px rgba(245,193,92,0.42), 0 0 30px rgba(245,193,92,0.22), inset 0 0 0 1px rgba(245,193,92,0.12); }
}
.chip-featured.active {
  background: linear-gradient(135deg, #F5C15C, #E8A23C);
  border-color: #F5C15C;
  color: var(--basalt);
  box-shadow:
    0 0 14px rgba(245,193,92,0.65),
    0 0 40px rgba(245,193,92,0.38),
    0 0 72px rgba(245,193,92,0.15);
  animation: none;
}
.chip-featured.active .chip-glyph { color: var(--basalt); }

/* Activation pop: the chip does a quick squash-and-settle when clicked. */
.filter-btn.pop { animation: chipPop 420ms cubic-bezier(0.34,1.56,0.64,1); }
@keyframes chipPop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.09); }
  60%  { transform: scale(0.97); }
  100% { transform: scale(1); }
}

/* Active chip takes its color from --chip (set inline from works.json by main.js).
   Text is always near-black, so category colors should be light/mid tones.
   Box-shadow lives in the cyberpunk glow block above — this keeps color only. */
.filter-btn.active {
  color: var(--basalt);
  background: var(--chip, var(--ember));
  border-color: var(--chip, var(--ember));
}

.filter-summary {
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: var(--mono-tracking);
  color: var(--text-dim);
  min-height: 1.2em;
  flex: 0 1 auto; /* size to its text so the shuffle button can share the line */
}

/* ===== SHUFFLE CONTROL (the "All" wall only; site.json sections.work.shuffle) =====
   A quiet mono-text button that sits on the summary line — related to the
   chip bar by texture (font, tracking) but deliberately NOT a chip: no
   neon halo, no count. Ghost style: hairline border, dim text; hover lifts
   the glyph and warms the text. It should read as part of the metadata
   row, not a fifth column of filters. */
.filter-summary-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: var(--spacing-xs);
  min-height: 1.2em; /* same reservation the summary had alone */
}
.shuffle-btn[hidden] { display: none; } /* .shuffle-btn's display would otherwise beat [hidden] */
.shuffle-btn {
  margin-left: auto; /* right side of the row on wide screens; wraps under on narrow */
  position: relative; /* anchor for the notice-shine pseudo-element */
  overflow: hidden;   /* and clip it inside the pill */
  font-family: var(--font-data);
  font-size: 0.7rem;
  letter-spacing: var(--mono-tracking);
  color: var(--text-dim);
  background: transparent;
  border: 1px solid rgba(232,230,227,0.12);
  border-radius: var(--radius-full);
  padding: 0.22rem 0.66rem;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  cursor: pointer;
  white-space: nowrap;
  transition: color 200ms cubic-bezier(0.4,0,0.2,1),
              border-color 200ms cubic-bezier(0.4,0,0.2,1);
}
.shuffle-btn svg {
  width: 13px; height: 13px;
  flex-shrink: 0;
  opacity: 0.7;
  transition: transform 280ms cubic-bezier(0.34,1.56,0.64,1), opacity 200ms ease;
}
.shuffle-btn:hover { color: var(--text); border-color: rgba(232,230,227,0.25); }
.shuffle-btn:hover svg { transform: translateY(-1px) rotate(-8deg) scale(1.12); opacity: 1; }
.shuffle-btn:active { transform: scale(0.95); transition-duration: 80ms; }
.shuffle-btn:focus-visible { outline: 2px solid var(--ember); outline-offset: 2px; }

/* One-time confirmation spin on the first click only (repeats stay calm). */
.shuffle-btn svg.spin-once { animation: shuffle-spin 480ms cubic-bezier(0.4,0,0.2,1); }
@keyframes shuffle-spin {
  0% { transform: rotate(0deg); }
  60% { transform: rotate(200deg) scale(1.15); }
  100% { transform: rotate(360deg) scale(1); }
}

/* ===== NOTICE CUE (first time the button appears, once per visit) =====
   A soft ember-tinted pulse + a diagonal shine sweep across the pill, so
   the quiet control catches the eye exactly once when the visitor first
   lands on "All" — never again on later filter switches. The shine is an
   overflow-clipped gradient pseudo-element, so nothing bleeds onto the
   summary line. */
.shuffle-btn.notice {
  animation: shuffle-notice 1100ms cubic-bezier(0.4,0,0.2,1);
}
.shuffle-btn.notice svg {
  animation: shuffle-notice-glyph 1100ms cubic-bezier(0.34,1.56,0.64,1);
}
.shuffle-btn::after {
  /* the shine: a narrow diagonal light band; invisible (and non-interactive)
     outside the animation */
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(115deg,
    transparent 25%,
    rgba(228,98,44,0.28) 47%,    /* ember core */
    rgba(232,230,227,0.35) 50%,  /* white hot center */
    rgba(228,98,44,0.28) 53%,
    transparent 75%);
  background-size: 260% 100%;
  background-position: 130% 0;   /* parked off to the right */
  opacity: 0;
  pointer-events: none;
}
@keyframes shuffle-notice {
  0%   { border-color: rgba(232,230,227,0.12); }
  18%  { border-color: rgba(228,98,44,0.55); color: var(--text); }
  40%  { border-color: rgba(232,230,227,0.12); }
}
@keyframes shuffle-notice-glyph {
  0%   { transform: rotate(0deg) scale(1); opacity: 0.7; }
  15%  { transform: rotate(-14deg) scale(1.25); opacity: 1; }
  30%  { transform: rotate(10deg) scale(1.15); }
  45%  { transform: rotate(0deg) scale(1); opacity: 0.7; }
}
.shuffle-btn.notice::after {
  animation: shuffle-notice-shine 1100ms cubic-bezier(0.4,0,0.2,1);
}
@keyframes shuffle-notice-shine {
  0%   { background-position: 130% 0; opacity: 0; }
  15%  { opacity: 1; }
  55%  { background-position: -30% 0; opacity: 1; }
  70%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .shuffle-btn svg { transition: none; }
  .shuffle-btn svg.spin-once { animation: none; }
  .shuffle-btn:active { transform: none; }
  .shuffle-btn.notice,
  .shuffle-btn.notice svg,
  .shuffle-btn.notice::after { animation: none; }
}

/* ======================================================================
   PORTFOLIO GRID
   ====================================================================== */
.portfolio-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: clamp(200px, 23vw, 300px);
  gap: clamp(1rem, 2vw, 1.5rem);
}

.portfolio-card {
  position: relative;
  background: var(--panel);
  border-radius: var(--radius-md);
  overflow: hidden;
  transition: box-shadow 400ms cubic-bezier(0.34,1.56,0.64,1),
              transform 400ms cubic-bezier(0.34,1.56,0.64,1);
  cursor: pointer;
  content-visibility: auto;
  contain-intrinsic-size: auto 300px;
  /* tilt is applied by JS as --tilt-x/--tilt-y (see main.js); unset on leave */
  transform: translateY(var(--lift-y, 0)) scale(var(--lift-s, 1)) perspective(900px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
}

@keyframes fadeInGrid {
  from { opacity: 0; transform: translateY(30px); }
  to   { opacity: 1; transform: translateY(0); }
}

.portfolio-card:hover {
  box-shadow: 0 12px 48px color-mix(in srgb, var(--cat) 35%, transparent),
              0 0 32px color-mix(in srgb, var(--cat) 22%, transparent);
  --lift-y: -8px;
  --lift-s: 1.02;
  z-index: 10;
}

/* Entrance animation: plays on first render and restarts whenever .hidden is
   removed (main.js presets animation-delay per visible position). */
.portfolio-card:not(.hidden) {
  /* fill-mode backwards, not both: after the entrance finishes the fill must stop
     overriding :hover lift and the JS chip-hover dim (inline opacity/transform). */
  animation: fadeInGrid 420ms cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
}

.portfolio-card.hidden {
  opacity: 0;
  transform: scale(0.92);
  pointer-events: none;
  transition: opacity 350ms cubic-bezier(0.4,0,1,1),
              transform 350ms cubic-bezier(0.4,0,1,1);
}

/* ===== FEATURED SHOWCASE — grid wears .showcase while the Featured filter is active ===== */
.portfolio-grid.showcase {
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: clamp(300px, 34vw, 460px);
  gap: clamp(1.25rem, 2.5vw, 2rem);
}
.portfolio-grid.showcase .portfolio-card { border-radius: var(--radius-lg); }
/* Gallery-label feel: title/category always visible, not hover-only */
.portfolio-grid.showcase .card-hover-meta { opacity: 1; transform: translateY(0); }
.portfolio-grid.showcase .portfolio-card:hover {
  box-shadow: 0 16px 56px color-mix(in srgb, var(--cat) 40%, transparent),
              0 0 40px color-mix(in srgb, var(--cat) 28%, transparent);
  --lift-y: -8px;
  --lift-s: 1.01;
}

@property --grad-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
/* Featured works wear a slow, subtle rotating gradient ring in every view.
   Kept dim by default (0.45) and brightened on hover so the grid stays calm. */
.portfolio-card.featured::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit; /* tracks the card radius (showcase uses --radius-lg) */
  padding: 1.5px;
  background: conic-gradient(from var(--grad-angle), var(--ember), var(--amber), var(--teal), var(--ember));
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
  animation: rotateGradient 10s linear infinite;
  opacity: 0.45;
  transition: opacity 400ms ease;
  pointer-events: none;
  z-index: 5;
}
.portfolio-card.featured:hover::before { opacity: 0.85; }
@keyframes rotateGradient { to { --grad-angle: 360deg; } }

.portfolio-card button { width: 100%; height: 100%; border: none; background: none; cursor: pointer; padding: 0; display: block; }

.portfolio-card img {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  background: var(--panel);
  opacity: 0;
  transition: opacity 400ms cubic-bezier(0.4,0,0.2,1),
              transform 500ms cubic-bezier(0.34,1.56,0.64,1),
              filter 400ms ease;
}
.portfolio-card img.loaded { opacity: 1; }
.portfolio-card button:hover img { transform: scale(1.08); filter: brightness(1.06); }

.portfolio-card button:hover::after {
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 2px var(--cat, var(--ember)), 0 8px 24px color-mix(in srgb, var(--cat) 30%, transparent);
  border-radius: var(--radius-md);
  pointer-events: none;
}

/* Dribbble CTA — single "want more?" card under the grid */
.dribbble-cta {
  margin-top: var(--spacing-xl);
  background: linear-gradient(135deg, rgba(234,76,137,0.10) 0%, rgba(228,98,44,0.06) 55%, transparent 100%), var(--panel);
  border: 1px solid rgba(232,230,227,0.09);
  border-radius: var(--radius-lg);
  padding: clamp(1rem, 2.2vw, 1.35rem) clamp(1.1rem, 2.6vw, 1.5rem);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-md) var(--spacing-lg);
  flex-wrap: nowrap;
  position: relative;
  overflow: clip;
}
.dribbble-cta::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(135deg, rgba(234,76,137,0.35), rgba(228,98,44,0.20), transparent 65%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
  pointer-events: none;
  opacity: 0.9;
}
.dribbble-cta-text { position: relative; z-index: 1; flex: 1 1 auto; min-width: 0; }
.dribbble-kicker {
  font-family: var(--font-data);
  font-size: 0.70rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #EA4C89;
  font-weight: 600;
  line-height: 1;
  margin: 0;
}
.dribbble-cta-text h3 {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(1.15rem, 2.1vw, 1.45rem);
  letter-spacing: -0.02em;
  line-height: 1.2;
  margin: 0.28rem 0 0;
}
.dribbble-cta-text h3 em { font-style: normal; color: #EA4C89; }
.dribbble-cta-actions { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: flex-end; gap: 0.4rem; flex-shrink: 0; }
.btn-dribbble {
  background: #EA4C89;
  color: #fff;
  border-color: #EA4C89;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  white-space: nowrap;
  box-shadow: 0 0 28px rgba(234,76,137,0.38), 0 4px 16px rgba(234,76,137,0.28);
}
.btn-dribbble:hover { background: #f06297; border-color: #f06297; color: #fff; box-shadow: 0 0 44px rgba(234,76,137,0.55), 0 8px 28px rgba(234,76,137,0.38); transform: translateY(-2px); }
.btn-dribbble:active { transform: translateY(0) scale(0.97); transition-duration: 80ms; }
.btn-dribbble svg { width: 18px; height: 18px; flex-shrink: 0; }
.dribbble-handle {
  font-family: var(--font-data);
  font-size: 0.72rem;
  letter-spacing: var(--mono-tracking);
  color: var(--text-dim);
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  white-space: nowrap;
}
.dribbble-handle a { color: var(--text-dim); text-underline-offset: 3px; }
.dribbble-handle a:hover { color: var(--text); }

.card-hover-meta {
  position: absolute;
  inset: auto 0 0 0;
  padding: 3rem 1rem 0.85rem;
  background: linear-gradient(to bottom, transparent 0%, rgba(13,13,14,0.6) 40%, rgba(13,13,14,0.92) 100%);
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 300ms ease, transform 300ms ease;
  pointer-events: none;
  z-index: 6;
}
@media (hover: hover) and (pointer: fine) {
  .portfolio-card:hover .card-hover-meta { opacity: 1; transform: translateY(0); }
}
/* Focus-visible always; hover guarded above, so focus isn't blocked on touch. */
.portfolio-card button:focus-visible + .card-hover-meta,
.portfolio-card:focus-within .card-hover-meta { opacity: 1; transform: translateY(0); }

.card-title { font-family: var(--font-display); font-size: 0.95rem; color: var(--text); }
.card-category { font-family: var(--font-data); font-size: 0.7rem; letter-spacing: var(--mono-tracking); color: var(--text-dim); text-transform: uppercase; }

.category-tag {
  position: absolute;
  top: var(--spacing-sm); left: var(--spacing-sm);
  font-family: var(--font-data);
  font-size: 0.6875rem;
  letter-spacing: var(--mono-tracking);
  padding: 0.25rem 0.625rem;
  border-radius: var(--radius-full);
  font-weight: 600;
  text-transform: uppercase;
  z-index: 10;
  pointer-events: none;
  box-shadow: 0 2px 12px rgba(0,0,0,0.4);
  /* Color from --cat (set inline from works.json by main.js) */
  background: var(--cat, var(--text-dim));
  color: var(--basalt);
}

/* Show-more pagination control */
.show-more-wrap { display: flex; justify-content: center; margin-top: var(--spacing-lg); }
.show-more[hidden] { display: none; } /* .btn display would otherwise beat [hidden] */
.show-more { font-size: 0.8125rem; }

/* Empty / error / noscript panels — span the full grid */
.grid-empty, .grid-error, .grid-noscript {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-family: var(--font-data);
  font-size: 0.875rem;
  letter-spacing: var(--mono-tracking);
  color: var(--text-dim);
  border: 1px dashed rgba(232,230,227,0.15);
  border-radius: var(--radius-lg);
  padding: var(--spacing-2xl) var(--spacing-lg);
  line-height: 1.6;
}

/* ======================================================================
   STATS
   ====================================================================== */
.stats-section {
  /* the scroll story funnels: gallery (1400) -> stats + about (900) ->
     contact (700). Stats shares About's container, rhythm, and left-
     aligned heading; the stat columns themselves center like the hero's
     symmetric image wings. One editorial pattern end-to-end. */
  max-width: 900px;
  margin: var(--spacing-2xl) auto;
  padding: var(--spacing-2xl) var(--spacing-lg);
  position: relative;
  scroll-margin-top: 72px;
}
/* ember divider at the same scale as the about/contact rules (80% of a
   900px section — identical visual length to About's teal line above) */
.stats-section::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%; transform: translateX(-50%);
  width: 80%; height: 1px;
  background: linear-gradient(to right, transparent, rgba(228,98,44,0.3) 30%, rgba(228,98,44,0.5) 50%, rgba(228,98,44,0.3) 70%, transparent);
}
.stats-section h2 {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(2rem, 4vw, 3rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-bottom: var(--spacing-xl);
  text-shadow: 0 0 40px rgba(228,98,44,0.35), 0 0 80px rgba(228,98,44,0.15);
}

/* Open grid: no boxes, numbers sit directly on basalt like the rest of
   the page. Four equal columns, each unit separated by a hairline top
   rule (a newspaper band — consistent, dense, nothing orphaned). */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}
.stat-card {
  position: relative;
  padding: var(--spacing-md) var(--spacing-sm) var(--spacing-sm);
  border-top: 1px solid rgba(232,230,227,0.12);
  transition: border-color 300ms ease;
}
/* centered funnel: each column centers its number + label, so the band
   reads as one symmetric object under the centered heading */
.stat-card { text-align: center; }
.stat-card::before {
  content: '';
  position: absolute;
  top: -1px; left: 0;
  width: 100%; height: 1px;
  background: linear-gradient(to right, transparent, rgba(232,230,227,0.3) 20%, rgba(232,230,227,0.3) 80%, transparent);
  opacity: 0.6;
  transition: opacity 300ms ease;
}
.stat-card:hover { border-top-color: transparent; }
.stat-card:hover::before { opacity: 1; }
.stat-number {
  font-family: var(--font-data);
  font-size: clamp(2.2rem, 4vw, 3.25rem);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ember);
  line-height: 1;
  margin-bottom: var(--spacing-xs);
  text-shadow: 0 0 32px rgba(228,98,44,0.45), 0 0 64px rgba(228,98,44,0.22);
}
/* Landing flash when a counter finishes */
.stat-number.landed {
  animation: statLand 600ms ease-out;
}
@keyframes statLand {
  0%   { text-shadow: 0 0 32px rgba(228,98,44,0.45), 0 0 64px rgba(228,98,44,0.22); }
  30%  { text-shadow: 0 0 48px rgba(228,98,44,0.9), 0 0 96px rgba(228,98,44,0.45); }
  100% { text-shadow: 0 0 32px rgba(228,98,44,0.45), 0 0 64px rgba(228,98,44,0.22); }
}
.stat-label {
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: var(--mono-tracking);
  color: var(--text-dim);
  text-transform: uppercase;
  line-height: 1.5;
  max-width: 18ch;
  margin: 0 auto;
}
/* 2x2 on tablets and phones — no orphaned half-rows at any width */
@media (max-width: 860px) {
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
  .stat-card { padding: var(--spacing-sm) var(--spacing-md) var(--spacing-xs); }
  .stat-number { font-size: clamp(1.9rem, 7vw, 2.6rem); }
  .stat-label { font-size: 0.68rem; }
}

/* ======================================================================
   TESTIMONIALS v2 — the redesign
   The old one-card carousel was removed. The section is now a
   LAYOUT-DRIVEN design with two looks, picked in the admin:
     • editorial (default) — the first voice runs BIG in Fraunces as a
       magazine lead beside a column of the remaining quotes.
     • grid — the classic quiet wall: equal hairline-topped seats.
   All variants arrive as section classes set by main.js from
   site.json options (tsResolveOptions): .ts-editorial | .ts-grid,
   .ts-cols-2|3|4 (grid), .ts-accent-ember|teal|amber,
   .ts-theme-panel|bare. Every quote is fully visible in its own
   seat — nothing is clipped, dimmed, or off-screen, and there is no
   carousel state, no seam clones, no autoplay, no snap machinery.
   Motion budget: a per-card staggered reveal (reads --ts-i, set by
   main.js) and a hover lift. Reduced-motion zeroes all of it in the
   backstop near file end.
   ====================================================================== */
.testimonials-section {
  max-width: 900px;
  margin: var(--spacing-2xl) auto;
  padding: var(--spacing-2xl) var(--spacing-lg);
  position: relative;
  scroll-margin-top: 72px;
}
.testimonials-section::before {
  content: '';
  position: absolute;
  top: 0; left: 50%; transform: translateX(-50%);
  width: 80%; height: 1px;
  background: linear-gradient(to right, transparent, rgba(228,98,44,0.3) 30%, rgba(228,98,44,0.5) 50%, rgba(228,98,44,0.3) 70%, transparent);
}
.ts-kicker {
  font-family: var(--font-data);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ember);
  margin: 0 0 var(--spacing-xs) 0;
}
.testimonials-section h2 {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(2rem, 4vw, 3rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-bottom: var(--spacing-xl);
  text-align: left;
  text-wrap: balance;
  text-shadow: 0 0 40px rgba(228,98,44,0.35), 0 0 80px rgba(228,98,44,0.15);
}

/* ---- the accent dial: --ts-accent / --ts-glow drive the hairline
   rules, quote mark, and avatar rings. Default = ember (the theme's
   own accent, so a custom theme.accent recolors this section too). */
.testimonials-stage { --ts-accent: var(--ember); --ts-glow: rgba(228,98,44,0.4); --ts-glow-soft: rgba(228,98,44,0.10); }
.testimonials-stage.ts-accent-teal   { --ts-accent: var(--teal);  --ts-glow: rgba(127,184,179,0.45); --ts-glow-soft: rgba(127,184,179,0.10); }
.testimonials-stage.ts-accent-amber  { --ts-accent: var(--amber); --ts-glow: rgba(240,169,78,0.45); --ts-glow-soft: rgba(240,169,78,0.12); }
.ts-accent-teal .ts-kicker  { color: var(--teal); }
.ts-accent-amber .ts-kicker { color: var(--amber); }

/* ---- shared stage base (both layouts) ---- */
#testimonialsItems.testimonials-stage {
  display: grid;
  gap: var(--spacing-md);
}

/* ---- CARD (the atom both layouts share) ----
   The SQUARE voice-card: equal width and height seats, contents
   CENTERED, playful. The quote sits as the visual center in Fraunces
   with WONK on; the attribution anchors bottom-center as a tidy
   centered stack (avatar above name above role). Motion is transform
   + shadow only. The reveal stagger is layered onto the shared
   .reveal transition via --ts-i. */
#testimonialsItems .testimonial {
  position: relative;
  padding: var(--spacing-xl) var(--spacing-lg);
  border-radius: var(--radius-lg);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  justify-content: center;
  /* ALL cards the same size — equal grid seats (1fr columns, equal
     row heights) plus a content-driven min-height so the longest
     quote sets the height and every card matches, with contents
     centered. */
  min-height: 300px;
  transition: transform 350ms cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 350ms ease,
              background-position 700ms cubic-bezier(0.22, 1, 0.36, 1);
  isolation: isolate;
}
/* staggered entrance — delay scales with the card's index, caps at 7 */
#testimonialsItems .testimonial.reveal {
  transition-delay: calc(60ms * var(--ts-i, 0));
}
#testimonialsItems .testimonial.reveal.in { transition-delay: 0s; }

/* the IGNITION top rule — an ember filament, barely warm at rest,
   glowing brighter on hover. A gradient that pools at center like
   a filament heating up. */
.testimonial::before {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 1px;
  background: linear-gradient(to right,
    transparent,
    var(--ts-accent) 12%, var(--ts-glow) 50%, var(--ts-accent) 88%,
    transparent);
  opacity: 0.35;
  transition: opacity 400ms ease, height 400ms ease,
              filter 400ms ease;
  pointer-events: none;
  filter: blur(0.2px);
}
.testimonial:hover::before {
  opacity: 1;
  height: 2px;
  filter: blur(0.4px) drop-shadow(0 0 6px var(--ts-glow));
}

/* the quote: FRAUNCES at its most expressive — SOFT 60, WONK 1,
   optical sizing on, slightly tighter tracking. The quote IS the
   card's visual: big serif warmth against the glass. Measure widened
   to 34ch: Fraunces ch units are wide, 32ch was breaking lines more
   than the rhythm wanted. */
.testimonial-quote {
  font-family: var(--font-quote);
  font-variation-settings: 'SOFT' 60, 'WONK' 1;
  font-optical-sizing: auto;
  font-size: 1.2rem;
  font-weight: 470;
  line-height: 1.55;
  letter-spacing: -0.005em;
  /* compact rhythm: quote centers in the space above the floor-anchored
     attribution — auto top + bottom margins split the breathing room
     evenly, no bottom dead void, attribution pinned to the floor */
  margin: auto 0 var(--spacing-md);
  color: var(--text);
  text-wrap: pretty;
  max-width: 34ch;
  position: relative;
  transition: color 300ms ease;
}
/* hover: quote warms a hair toward the accent — cards feel alive */
.testimonial:hover .testimonial-quote { color: #F5F1EC; }

/* the floating quote mark moved to the CARD (.testimonial::after below):
   it used to live on the quote element, where the long-quote clamp mask
   swallowed it. Card-level = never masked. */
.testimonial-quote::after {
  /* retirement stub: the mark now renders from .testimonial::after so the
     clamp mask can never eat it. This rule keeps the selector harmless. */
  content: none;
}

/* ---- long quotes: clamp + expand ----
   .ts-long (word count > 36, set by JS): the quote clamps to 5 lines
   with a fade mask, and a small accent "Read more" button appears.
   .ts-expanded (button toggle): full quote, no mask. The card grows
   and the grid row follows — every card in that row grows equally. */
.ts-long .testimonial-quote {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 5;
  overflow: hidden;
  /* soft fade at the cut line instead of a hard crop — the fade band is
     kept tight (last ~12%) so the final readable line stays legible */
  mask-image: linear-gradient(to bottom, #000 88%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 88%, transparent 100%);
}
/* the floating quote mark — ONE grammar on every card: a big soft-accent
   glyph centered above the quote, living on the CARD so the long-quote
   clamp mask can never eat it. Fades warm + tilts on hover. Purely
   decorative: real quote marks stay in the text for copy/paste. */
.testimonial::after {
  content: '\201C';
  position: absolute;
  top: calc(var(--spacing-xl) * 0.5);
  left: 50%;
  margin-left: -0.30em;
  font-family: var(--font-quote);
  font-variation-settings: 'SOFT' 100, 'WONK' 1;
  font-size: 2.4em;
  font-weight: 500;
  line-height: 1;
  color: var(--ts-accent);
  opacity: 0.16;
  pointer-events: none;
  z-index: 0;
  transition: opacity 400ms ease, transform 500ms cubic-bezier(0.22, 1, 0.36, 1);
}
.testimonial:hover::after {
  opacity: 0.34;
  transform: translateY(0.04em) rotate(-4deg) scale(1.04);
}

.ts-long.ts-expanded .testimonial-quote {
  display: block;
  overflow: visible;
  mask-image: none;
  -webkit-mask-image: none;
}
.ts-more {
  font-family: var(--font-data);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ts-accent);
  background: transparent;
  border: none;
  border-bottom: 1px solid transparent;
  padding: 2px 0;
  margin: 0 0 var(--spacing-md);
  cursor: pointer;
  transition: color 200ms ease, border-color 200ms ease;
}
.ts-more:hover, .ts-more:focus-visible {
  color: var(--text);
  border-bottom-color: var(--ts-accent);
}
.ts-more:focus-visible { outline: 1px solid var(--ts-glow); outline-offset: 3px; border-radius: 2px; }
/* expanded: the button sits between quote and attribution with room */
.ts-expanded .ts-more { margin-top: var(--spacing-sm); }

/* ---- attribution: a centered stack (avatar / name / role) ----
   margin-top:auto pins the who-block to the card floor; the quote's
   own margin auto centers it in the remaining space. Result: short
   cards breathe in the MIDDLE (between quote and attribution), and
   every avatar/name/role sits on one baseline across the row. */
.testimonial-attr {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--spacing-xs);
  min-width: 0;
  flex: 0 0 auto;
  margin-top: auto;
}
.ts-avatar, .ts-monogram {
  width: 52px; height: 52px;
  border-radius: var(--radius-full);
  flex-shrink: 0;
}
.ts-avatar {
  object-fit: cover;
  border: 1px solid var(--ts-glow);
  background: var(--panel);
  box-shadow: 0 0 0 3px rgba(0,0,0,0.25), 0 0 18px var(--ts-glow);
  transition: box-shadow 350ms ease, transform 350ms cubic-bezier(0.22, 1, 0.36, 1);
}
.ts-monogram {
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-quote);
  font-variation-settings: 'SOFT' 100, 'WONK' 1;
  font-weight: 600;
  font-size: 1.3rem;
  color: var(--ts-accent);
  background:
    radial-gradient(120% 120% at 30% 20%, rgba(255,255,255,0.08), transparent 55%),
    linear-gradient(160deg, rgba(232,230,227,0.10), rgba(232,230,227,0.03));
  border: 1px solid var(--ts-glow);
  box-shadow: 0 0 0 3px rgba(0,0,0,0.25), 0 0 18px var(--ts-glow);
  transition: border-color 250ms ease, transform 250ms cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 350ms ease;
}
.testimonial:hover .ts-monogram,
.testimonial:hover .ts-avatar {
  border-color: var(--ts-accent);
  box-shadow: 0 0 0 3px rgba(0,0,0,0.3), 0 0 30px var(--ts-glow);
}
.testimonial:hover .ts-monogram { transform: rotate(-6deg) scale(1.08); }
.testimonial:hover .ts-avatar { transform: scale(1.06); }
.ts-who { display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 0; }
.ts-name {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 1rem;
  letter-spacing: 0.005em;
  color: var(--text);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  transition: color 300ms ease;
}
a.ts-name { cursor: pointer; }
.ts-arrow { font-size: 0.75em; opacity: 0; transform: translate(-2px, 1px); transition: opacity 200ms ease, transform 200ms ease; }
a.ts-name:hover, a.ts-name:focus-visible { color: var(--ts-accent); }
a.ts-name:hover .ts-arrow, a.ts-name:focus-visible .ts-arrow { opacity: 0.8; transform: translate(0, 1px); }
.ts-role {
  font-family: var(--font-data);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  color: var(--text-dim);
  text-transform: uppercase;
  /* tighter caps rhythm: 1.5 was airy for uppercase; 1.4 keeps
     two-line roles ("FOUNDER, STREETWEAR LABEL") reading as one unit */
  line-height: 1.4;
  /* 22ch was wrapping mid-title ("NAKAMA / STUDIO"); 26ch fits the
     longest role on one line at this size */
  max-width: 26ch;
}
/* the role sits on dark glass now — lift it a notch so it reads */
.testimonials-stage .ts-role { color: var(--text-dim-strong, #A6A6AD); }

/* ---- THEME: panel (default) — ember liquid glass over basalt ----
    Not grey glass: the layers are TINTED with the section accent
    (rgba of --ts-glow) so the whole card reads warm/teal/ambered
    depending on the admin dial, and the dead grey dies. Layer stack:
    top-light pool, diagonal sheen band (drifts on hover), corner
    ember glow, then the panel base. Inner highlight + deep drop
    shadow make it sit like real glass over basalt. */
.testimonials-stage.ts-theme-panel .testimonial {
  background:
    radial-gradient(120% 80% at 30% 0%, rgba(255,255,255,0.05), transparent 55%),
    linear-gradient(115deg,
      transparent 22%,
      rgba(255,255,255,0.03) 38%,
      rgba(255,255,255,0.07) 46%,
      rgba(255,255,255,0.02) 54%,
      transparent 72%),
    radial-gradient(90% 70% at 85% 100%, var(--ts-glow-soft, rgba(228,98,44,0.10)), transparent 60%),
    linear-gradient(180deg, #1B1A1E, #161619);
  background-size: 100% 100%, 220% 100%, 100% 100%, 100% 100%;
  background-position: 0 0, 115% 0, 0 0, 0 0;
  box-shadow: 0 0 0 1px rgba(232,230,227,0.09),
              inset 0 1px 0 rgba(255,255,255,0.06),
              inset 0 -1px 0 rgba(0,0,0,0.35),
              0 14px 40px rgba(0,0,0,0.45);
  overflow: hidden;
}
/* hover: sheen sweeps across, corner glow warms up, card lifts.
   Hover-guarded so touch never triggers the sweep. */
@media (hover: hover) and (pointer: fine) {
  .testimonials-stage.ts-theme-panel .testimonial:hover {
    background-position: 0 0, -15% 0, 0 0, 0 0;
  }
}
/* reduced motion: sheen parked mid-card (visible, static), no sweep */
@media (prefers-reduced-motion: reduce) {
  .testimonials-stage.ts-theme-panel .testimonial {
    background-position: 0 0, 40% 0, 0 0, 0 0;
  }
}
/* ---- THEME: bare — hairline seats straight on basalt (stats grammar) ---- */
.testimonials-stage.ts-theme-bare .testimonial {
  background: transparent;
  box-shadow: none;
}
.testimonials-stage.ts-theme-bare .testimonial::before { opacity: 0.45; }

/* ---- hover lift (guarded; composes with the reveal transform) ---- */
@media (hover: hover) and (pointer: fine) {
  .testimonials-stage.ts-theme-panel .testimonial:hover {
    transform: translateY(-6px) rotate(-0.3deg) scale(1.015);
    box-shadow: 0 0 0 1px var(--ts-glow),
                inset 0 1px 0 rgba(255,255,255,0.09),
                inset 0 -1px 0 rgba(0,0,0,0.35),
                0 22px 60px rgba(0,0,0,0.55),
                0 0 44px var(--ts-glow-soft);
  }
  .testimonials-stage.ts-theme-bare .testimonial:hover {
    transform: translateY(-4px) rotate(-0.4deg);
    box-shadow: none;
  }
  .testimonials-stage.ts-theme-bare .testimonial:hover::before { opacity: 1; }
}

/* ==================================================================
   LAYOUT: editorial (default) — balanced at ANY count, ALL cards
   equal seats. An auto-fit grid of equal voice-cards filling every
   row edge-to-edge — no lead card, no special spans, no asymmetry.
   2 quotes, 3, 6, 15: all organized. The admin can pin the number
   of columns per row (ts-cols-2/3/4); unset = auto-fit.
   At phone width everything becomes the horizontal snap rail.
   ================================================================== */
.testimonials-stage.ts-editorial {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--spacing-md);
}

/* ================= LAYOUT: grid — the quiet wall =================
   Same no-voids rule: auto-fit equal seats filling every row. */
.testimonials-stage.ts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--spacing-md);
}

/* ---- pinned column counts (admin "per row" setting) ----
   Works for BOTH layouts. A REAL fixed count, not an auto-fit nudge:
   repeat(N, 1fr) guarantees exactly N equal seats per row, so two
   reviews sit side by side instead of stretching, and rows stay
   full at any quote count. minmax(0,1fr) stops long quotes from
   blowing the track wider than its share. */
.testimonials-stage.ts-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.testimonials-stage.ts-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.testimonials-stage.ts-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* keep 2-per-row cards comfortably square-ish on huge screens */
.testimonials-stage.ts-cols-2 { max-width: 1280px; margin-inline: auto; }

/* ================= responsive ================= */
@media (max-width: 768px) {
  /* PHONE: the horizontal snap rail — one compact card at a time,
     neighbors peeking. Kills the tall stack that ate the screen. */
  #testimonialsItems.testimonials-stage,
  .testimonials-stage.ts-grid,
  .testimonials-stage.ts-grid.ts-cols-2,
  .testimonials-stage.ts-grid.ts-cols-3,
  .testimonials-stage.ts-grid.ts-cols-4,
  .testimonials-stage.ts-editorial {
    display: flex;
    flex-direction: row;
    gap: var(--spacing-sm);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding: var(--spacing-xs) 4px var(--spacing-sm);
    mask-image: linear-gradient(to right, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%);
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%);
  }
  #testimonialsItems.testimonials-stage::-webkit-scrollbar { display: none; }
  #testimonialsItems .testimonial {
    flex: 0 0 86%;
    scroll-snap-align: center;
    scroll-snap-stop: always;
    padding: var(--spacing-lg) var(--spacing-md);
    display: flex;
    flex-direction: column;
  }
  #testimonialsItems .testimonial .testimonial-quote {
    flex: 1 0 auto;
    margin-bottom: var(--spacing-sm);
    font-size: 1.08rem;
  }
  /* phone: the decorative mark is smaller + higher, and the quote text
     gets a clear runway below it — the 86% card is narrower, so the old
     PC-sized mark crowded the first line here */
  #testimonialsItems .testimonial::after {
    font-size: 1.9em;
    top: calc(var(--spacing-lg) * 0.5);
  }
  #testimonialsItems .testimonial .testimonial-quote {
    padding-top: 1.6em;
  }
  /* phone: the clamp's native ellipsis is readable on its own; the PC
     fade-mask ghosted the 5th line into half-legible text on the
     narrow card. Line-clamp only here. */
  .ts-long .testimonial-quote {
    mask-image: none;
    -webkit-mask-image: none;
  }
  #testimonialsItems .testimonial .testimonial-attr { margin-top: auto; }
}
/* ======================================================================
   ABOUT
   The teal chapter of the funnel — the sober counterweight after the
   ember testimonials. Structure: teal mono kicker under the h2, body
   paragraphs in the proven editorial measure, then a "what you actually
   get" results band (hairline-topped rows, teal check glyphs — the
   stats-section's newspaper grammar, restated vertically), then the
   skills tags under a small mono label. Nothing boxed, nothing loud:
   the section earns trust by rhythm, not decoration.
   ====================================================================== */
.about-section {
  max-width: 900px;
  margin: var(--spacing-2xl) auto;
  padding: var(--spacing-2xl) var(--spacing-lg);
  position: relative;
  scroll-margin-top: 72px;
}
.about-section::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%; transform: translateX(-50%);
  width: 80%; height: 1px;
  background: linear-gradient(to right, transparent, rgba(127,184,179,0.3) 30%, rgba(127,184,179,0.5) 50%, rgba(127,184,179,0.3) 70%, transparent);
}
.about-head { margin-bottom: var(--spacing-lg); }
.about-section h2 {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(2rem, 4vw, 3rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-bottom: var(--spacing-xs);
  text-shadow: 0 0 40px rgba(127,184,179,0.35), 0 0 80px rgba(127,184,179,0.15);
}
/* teal mono kicker under the h2 — the ts-kicker grammar in the about
   chapter's accent. Static markup (index.html): site.json's about
   schema has no kicker field, so this copy never fights the admin. */
.about-kicker {
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: var(--mono-tracking);
  text-transform: uppercase;
  color: var(--teal);
  margin: 0;
  text-shadow: 0 0 18px rgba(127,184,179,0.25);
}
.about-section p {
  font-size: clamp(1.05rem, 1.8vw, 1.2rem);
  line-height: 1.7;
  margin-bottom: var(--spacing-lg);
}
/* ---- results band: what a client actually gets ----
   Three hairline-topped rows (the stat-card grammar turned vertical),
   each a teal check glyph + one bolded promise + its plain-language
   proof. The bold fragment is Sora 600 — the eyebrow weight, not a
   new font — and the whole row sits in a slightly wider measure than
   the paragraphs above so it reads as a distinct block, not a third
   paragraph. */
.about-results {
  list-style: none;
  margin: var(--spacing-lg) 0 0;
  padding: 0;
}
.about-result {
  display: flex;
  align-items: flex-start;
  gap: var(--spacing-sm);
  padding: var(--spacing-sm) 0 var(--spacing-sm) var(--spacing-sm);
  border-top: 1px solid rgba(127,184,179,0.16);
  max-width: 60ch;
}
.about-result-glyph {
  flex-shrink: 0;
  width: 20px; height: 20px;
  margin-top: 0.28em;
  display: flex; align-items: center; justify-content: center;
  color: var(--teal);
  filter: drop-shadow(0 0 6px rgba(127,184,179,0.35));
}
.about-result-glyph svg { width: 100%; height: 100%; display: block; }
.about-result-text {
  font-size: clamp(0.95rem, 1.4vw, 1.05rem);
  line-height: 1.65;
  color: var(--text-dim);
}
.about-result-text strong {
  font-weight: 600;
  color: var(--text);
}
/* ---- skills under a small mono label ---- */
.about-skills { margin-top: var(--spacing-xl); }
.about-skills-label {
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: var(--mono-tracking);
  text-transform: uppercase;
  color: var(--teal-dark);
  margin: 0 0 var(--spacing-sm);
}
.skills-list { display: flex; flex-wrap: wrap; gap: var(--spacing-sm); margin-top: 0; }
/* teal-tinted tags: the about chapter's accent carries the chips, so
   the section reads as one teal object after the ember carousel */
.skill-tag {
  font-family: var(--font-data);
  font-size: 0.8125rem;
  letter-spacing: var(--mono-tracking);
  padding: 0.5rem 1rem;
  background: var(--panel);
  border: 1px solid rgba(127,184,179,0.22);
  border-radius: var(--radius-full);
  color: var(--text-dim);
  transition: border-color 200ms ease, color 200ms ease, box-shadow 200ms ease;
}
.skill-tag:hover {
  border-color: rgba(127,184,179,0.5);
  color: #9FCEC9;
  box-shadow: 0 0 14px rgba(127,184,179,0.12);
}
/* phone: results rows keep their leading glyph, skills stay tags —
   nothing reflows horizontally at 390px */
@media (max-width: 768px) {
  .about-result { padding-left: 0; gap: 0.7rem; }
  .about-result-glyph { width: 18px; height: 18px; margin-top: 0.22em; }
}

/* ======================================================================
   CONTACT
   ====================================================================== */
.contact-section {
  max-width: 700px;
  margin: var(--spacing-2xl) auto;
  padding: var(--spacing-2xl) var(--spacing-lg);
  position: relative;
  scroll-margin-top: 72px;
}
.contact-section::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%; transform: translateX(-50%);
  width: 80%; height: 1px;
  background: linear-gradient(to right, transparent, rgba(228,98,44,0.3) 30%, rgba(228,98,44,0.5) 50%, rgba(228,98,44,0.3) 70%, transparent);
}
.contact-section h2 {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(2rem, 4vw, 3rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-bottom: var(--spacing-sm);
  text-shadow: 0 0 40px rgba(228,98,44,0.3), 0 0 80px rgba(228,98,44,0.15);
}
/* ember mono kicker above the heading — the ts-kicker/about-kicker
   grammar, so the chapter opens like the other three. Static markup;
   the site.json contact schema has no kicker field to fight. */
.contact-kicker {
  font-family: var(--font-data);
  font-size: 0.75rem;
  letter-spacing: var(--mono-tracking);
  text-transform: uppercase;
  color: var(--ember);
  margin: 0 0 var(--spacing-xs) 0;
  text-shadow: 0 0 18px rgba(228,98,44,0.3);
}
.contact-section > p {
  font-size: clamp(1.05rem, 1.8vw, 1.2rem);
  color: var(--text-dim);
  margin-bottom: var(--spacing-xl);
}
.contact-links { display: flex; flex-wrap: wrap; gap: var(--spacing-sm); margin-top: var(--spacing-xl); align-items: stretch; }
.contact-links .btn { font-size: 1rem; padding: 1rem 2rem; }
.contact-label { font-family: var(--font-data); font-size: 0.8125rem; letter-spacing: var(--mono-tracking); color: var(--text-dim); width: 100%; margin-bottom: 0.25rem; text-transform: uppercase; }

/* Reach-out pills: icon chip + stacked label/value, per-channel accent */
.social-link {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  font-family: var(--font-data);
  text-decoration: none;
  background: var(--panel);
  border: 1px solid rgba(232,230,227,0.1);
  border-radius: var(--radius-full);
  padding: 0.7rem 1.35rem 0.7rem 0.75rem;
  position: relative;
  overflow: clip;
  transition: transform 200ms cubic-bezier(0.34,1.56,0.64,1),
              border-color 220ms ease,
              box-shadow 220ms ease,
              background-color 220ms ease;
}
.social-link::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at var(--sl-x, 50%) 50%, color-mix(in srgb, var(--sl-accent) 26%, transparent) 0%, transparent 70%);
  opacity: 0;
  transition: opacity 260ms ease;
  pointer-events: none;
}
.social-link .sl-icon {
  position: relative;
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border-radius: 50%;
  flex-shrink: 0;
  background: color-mix(in srgb, var(--sl-accent) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--sl-accent) 34%, transparent);
  color: var(--sl-accent);
  transition: transform 320ms cubic-bezier(0.34,1.56,0.64,1), background-color 220ms ease;
}
.social-link .sl-icon svg { width: 17px; height: 17px; display: block; }
.social-link .sl-text { position: relative; display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.social-link .sl-kicker {
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--sl-accent);
  opacity: 0.9;
}
.social-link .sl-value {
  font-size: 0.875rem;
  letter-spacing: var(--mono-tracking);
  color: var(--text);
  transition: color 200ms ease;
  white-space: nowrap;
}
.social-link .sl-arrow {
  position: relative;
  margin-left: 0.15rem;
  font-size: 0.85rem;
  color: var(--text-dim);
  transition: transform 240ms cubic-bezier(0.34,1.56,0.64,1), color 200ms ease;
}
.social-link:hover,
.social-link:focus-visible {
  border-color: color-mix(in srgb, var(--sl-accent) 55%, transparent);
  background: color-mix(in srgb, var(--sl-accent) 7%, var(--panel));
  box-shadow: 0 0 28px color-mix(in srgb, var(--sl-accent) 26%, transparent),
              0 6px 18px rgba(0,0,0,0.35);
  transform: translateY(-3px);
}
.social-link:hover::after,
.social-link:focus-visible::after { opacity: 1; }
.social-link:hover .sl-icon,
.social-link:focus-visible .sl-icon { transform: scale(1.12) rotate(-6deg); background: color-mix(in srgb, var(--sl-accent) 30%, transparent); }
.social-link:hover .sl-arrow,
.social-link:focus-visible .sl-arrow { transform: translate(3px, -3px); color: var(--sl-accent); }
.social-link:active { transform: translateY(0) scale(0.97); transition-duration: 80ms; }

.social-email    { --sl-accent: var(--ember); }
.social-discord  { --sl-accent: #5865F2; }

.contact-form {
  display: grid;
  gap: var(--spacing-md);
  margin-top: var(--spacing-lg);
  /* the form sits on the panel with a hairline top rule — the
     testimonial-attr / about-result grammar, so the form reads as the
     chapter's results band rather than a floating applet */
  padding-top: var(--spacing-md);
  border-top: 1px solid rgba(232,230,227,0.14);
}
/* Name/email share one line; collapse on phones (the same stacked layout
   as before — the wrapper carries no hooks, pure presentation). */
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-md); }
@media (max-width: 560px) { .form-row { grid-template-columns: 1fr; } }

.form-group label {
  display: block;
  font-family: var(--font-data);
  font-size: 0.8125rem;
  letter-spacing: var(--mono-tracking);
  color: var(--text-dim);
  margin-bottom: var(--spacing-xs);
  text-transform: uppercase;
  transition: color 200ms ease;
}
.form-group:focus-within label { color: var(--teal); }
.form-group input,
.form-group textarea {
  width: 100%;
  padding: 0.875rem 1rem;
  background: var(--basalt);
  border: 1px solid rgba(232,230,227,0.1);
  border-radius: var(--radius-md);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1rem;
  transition: border-color 200ms ease, box-shadow 200ms ease, background-color 300ms ease;
  /* inset top light: wells read as carved into the page, not floating
     cards — matches how panels float and inputs sink on this site */
  box-shadow: inset 0 1px 0 rgba(232,230,227,0.06), inset 0 8px 20px rgba(0,0,0,0.28);
}
.form-group input:focus,
.form-group textarea:focus {
  outline: 2px solid var(--teal);
  outline-offset: 0;
  border-color: var(--teal);
  box-shadow: inset 0 1px 0 rgba(232,230,227,0.06), inset 0 8px 20px rgba(0,0,0,0.28),
              0 0 24px rgba(127,184,179,0.2), 0 0 4px rgba(127,184,179,0.3);
}
.form-group input::placeholder,
.form-group textarea::placeholder { color: var(--text-dim); opacity: 0.5; }
.form-group textarea { min-height: 140px; resize: vertical; }

.form-group input:-webkit-autofill,
.form-group input:-webkit-autofill:hover,
.form-group input:-webkit-autofill:focus,
.form-group textarea:-webkit-autofill,
.form-group textarea:-webkit-autofill:hover,
.form-group textarea:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: 0 0 0 1000px var(--basalt) inset;
  border-color: rgba(232,230,227,0.1);
  transition: background-color 5000s ease-in-out 0s;
}

.form-group.has-error input,
.form-group.has-error textarea { border-color: var(--rose); box-shadow: 0 0 0 3px rgba(224,122,138,0.15); }
.form-group .field-error { font-family: var(--font-data); font-size: 0.75rem; color: var(--rose); margin-top: 0.35rem; display: none; }
/* Scoping fields: native selects styled in the input vocabulary. The select
   text is the body font at 1rem (no iOS zoom-on-focus); the label keeps the
   mono kicker treatment. Custom caret via SVG data-URI background. */
.form-scoping { display: grid; gap: var(--spacing-md); }
/* [hidden] must win over the grid display above, or the off-state empty
   container still takes a grid row (author rules beat the UA sheet). */
.form-scoping[hidden] { display: none; }
.form-scoping-group select {
  width: 100%;
  padding: 0.875rem 2.4rem 0.875rem 1rem;
  background-color: var(--basalt);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238A8A8D' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 1rem center;
  border: 1px solid rgba(232,230,227,0.1);
  border-radius: var(--radius-md);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1rem;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(232,230,227,0.06), inset 0 8px 20px rgba(0,0,0,0.28);
  transition: border-color 200ms ease, box-shadow 200ms ease;
}
.form-scoping-group select:focus {
  outline: 2px solid var(--teal);
  outline-offset: 0;
  border-color: var(--teal);
  box-shadow: inset 0 1px 0 rgba(232,230,227,0.06), inset 0 8px 20px rgba(0,0,0,0.28),
              0 0 24px rgba(127,184,179,0.2), 0 0 4px rgba(127,184,179,0.3);
}
.form-scoping-group select option { color: var(--text); background: var(--panel); }
/* Unselected state: main.js toggles .placeholder when the value is empty */
.form-scoping-group select.placeholder { color: var(--text-dim); }
@media (forced-colors: active) {
  .form-scoping-group select { background-image: none; }
}

.form-group.has-error .field-error { display: block; }

/* Submit: the site's normal pill button, right where the brief ends. The
   arrow is a CSS pseudo-element (survives the button's textContent swaps
   in main.js) and slides only under a real hover, so it can never stick
   as a ghost state on touch. */
.contact-form .btn-primary::after {
  content: '→';
  margin-left: 0.6em;
  display: inline-block;
  transition: transform 240ms cubic-bezier(0.34,1.56,0.64,1);
}
@media (hover: hover) {
  .contact-form .btn-primary:hover::after { transform: translateX(6px); }
}
/* the submit's warm seat: ember ring + glow on hover — the carousel
   arrow grammar (the ::after slide above keeps its own timing) */
@media (hover: hover) {
  .contact-form .btn-primary:hover {
    box-shadow: 0 0 24px rgba(228,98,44,0.35);
  }
}

.form-confirmation {
  font-family: var(--font-data);
  font-size: 0.875rem;
  color: var(--teal);
  padding: var(--spacing-sm);
  background: rgba(127,184,179,0.1);
  border-radius: var(--radius-md);
  border: 1px solid rgba(127,184,179,0.2);
  display: none;
  margin-top: var(--spacing-md);
  align-items: baseline;
}
/* success row: hairline-topped like testimonial-attr — one grammar
   for "the voice lands" whether it's a client quote or a sent brief */
.form-confirmation.visible {
  display: flex;
  animation: confirm-in 420ms cubic-bezier(0.34,1.56,0.64,1);
}
@keyframes confirm-in {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
}
.confirm-glyph { width: 1em; height: 1em; flex-shrink: 0; margin-right: 0.55em; transform: translateY(0.1em); }

/* Failure region: same panel grammar as the confirmation, in rose. Shown
   only when every delivery channel failed; the anchor offers the mailto
   recovery path with the brief's text still in the form. */
.form-error {
  font-family: var(--font-data);
  font-size: 0.875rem;
  color: var(--rose);
  padding: var(--spacing-sm);
  background: rgba(224,122,138,0.08);
  border-radius: var(--radius-md);
  border: 1px solid rgba(224,122,138,0.22);
  margin-top: var(--spacing-md);
}
.form-error[hidden] { display: none; }
.form-error a { color: var(--rose); text-underline-offset: 3px; }
.form-error a:focus-visible { outline: 2px solid var(--rose); outline-offset: 3px; }

/* Screen-reader-only live region: the sole announcer for send/success/fail
   states. Must stay rendered (not display:none) to announce reliably. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ======================================================================
   FOOTER
   ====================================================================== */
footer {
  max-width: 1400px;
  margin: 0 auto;
  padding: var(--spacing-xl) var(--spacing-lg);
  border-top: 1px solid rgba(232,230,227,0.06);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-md);
}
footer p { font-family: var(--font-data); font-size: 0.8125rem; letter-spacing: var(--mono-tracking); color: var(--text-dim); }
.footer-mark { width: 24px; height: 24px; opacity: 0.4; object-fit: contain; display: block; transition: opacity 300ms ease, transform 300ms cubic-bezier(0.34,1.56,0.64,1); }
footer:hover .footer-mark { opacity: 0.85; transform: rotate(-8deg) scale(1.1); }

/* ======================================================================
   LIGHTBOX
   ====================================================================== */
.lightbox {
  position: fixed;
  inset: 0;
  background: rgba(9,9,10,0.985); /* near-opaque: bright card titles under the modal were faintly ghosting through at 0.97 */
  backdrop-filter: blur(8px);
  z-index: 1000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-lg) calc(var(--spacing-lg) + env(safe-area-inset-right, 0px))
           calc(var(--spacing-lg) + env(safe-area-inset-bottom, 0px))
           calc(var(--spacing-lg) + env(safe-area-inset-left, 0px));
  opacity: 0;
  transition: opacity 300ms ease;
}
.lightbox.visible { display: flex; opacity: 1; }

.lightbox-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* safe center: when the content is taller than the viewport, plain
     centering makes the top overflow unreachable. "safe" falls back to
     top-anchored, and the column scrolls invisibly if it ever must. */
  justify-content: safe center;
  gap: var(--spacing-sm);
  max-width: 90vw;
  max-height: calc(100vh - 2 * var(--spacing-lg));
  max-height: calc(100dvh - 2 * var(--spacing-lg) - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
  position: relative;
  transform: scale(0.95);
  transition: transform 300ms ease, opacity 160ms ease;
  /* invisible scrolling: the scrollbar flash during arrow navigation was the
     visible artifact of the containment fix. Scroll still works (extreme
     tall works + small screens), it just never shows a scrollbar. */
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;           /* Firefox */
  -ms-overflow-style: none;        /* old Edge/IE */
  padding: 2px 6px;
  margin: -2px -6px;               /* padding is scroll gutters, not layout */
}
.lightbox-content::-webkit-scrollbar { display: none; } /* Chrome/Safari */
.lightbox.visible .lightbox-content { transform: scale(1); }

/* The stage wraps the image + all controls (close, prev, next) so they are
   positioned relative to the artwork itself — always inside its bounds and
   vertically centered on it, no matter what caption/related rail sits below.
   It's a grid: the base image and any crossfade overlay share cell 1/1 with
   IDENTICAL sizing rules, so the two layers overlap pixel-perfectly. */
.lightbox-stage {
  position: relative;
  display: grid;
  place-items: center;
  min-width: 0;
  max-width: 100%;
  flex-shrink: 1;
  min-height: 0;
  /* STABLE STAGE BOX — the smoothness keystone. The stage has ONE fixed
     height budget (never resized per work): every image letterboxes inside
     it via object-fit: contain, centered by the grid. Aspect-ratio changes
     between works cause ZERO layout shift — caption, related rail, counter
     and nav arrows never move mid-gallery. The letterbox space reads as
     intentional gallery matting on the near-black overlay.
     Load-state: the reserved box also keeps the spinner centered in open
     space while the first stand-in streams in. */
  height: min(62vh, calc(100dvh - 340px));
}
.lightbox-stage.waiting { height: min(42vh, 320px); }

/* Issue fix (art overflowing the stage top on tall-caption works): the
   column (flex, max-height budget) could squeeze the STAGE below the
   image's own max-height — the img then overflowed its stage box upward
   into the viewport padding, and the nav arrows (positioned relative to
   the stage) drifted off the art. The fixed stage height above makes the
   image fit its box by construction (object-fit), so this guard is now
   belt-and-suspenders for the transition window. */
.lightbox-stage {
  min-height: min(62vh, calc(100dvh - 340px));
}

/* Phone: the exact-fit box (JS-set inline width/height) must not be held
   open by the desktop min-height guard — that re-created the letterbox
   bands. The waiting-state spinner still gets its own reserved height. */
@media (max-width: 768px) {
  .lightbox-stage { min-height: 0; }
  .lightbox-stage.waiting { min-height: min(40dvh, 300px); height: auto; }
}

.lightbox-stage img,
.lightbox-fade {
  grid-area: 1 / 1;
}

/* Quiet loader while the first artwork streams in: a small ember ring,
   centered in the stage. Never replaces a previous image mid-navigation. */
.lightbox-loader {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  z-index: 1; pointer-events: none;
}
.lightbox-loader span {
  width: 34px; height: 34px;
  border: 2px solid rgba(228,98,44,0.18);
  border-top-color: var(--ember);
  border-radius: 50%;
  animation: lightboxSpin 0.8s linear infinite;
}
.lightbox-loader[hidden] { display: none; }
@keyframes lightboxSpin { to { transform: rotate(360deg); } }

.lightbox-stage.waiting img { opacity: 0; }

/* Crossfade overlay + base image share ONE sizing rule so the two layers
   overlap pixel-perfectly during the dissolve. Both occupy grid cell 1/1.
   With the STABLE stage box, the image fills the box via object-fit and
   the letterbox padding lives INSIDE the box — no box resize between works
   of different ratios, ever. The overlay is never removed until it is fully
   painted (decode-gated in JS) — no blank frame, no offset ghosts. */
.lightbox-stage img,
.lightbox-fade {
  grid-area: 1 / 1;
  flex: 0 1 auto;
  min-height: 0;
  object-fit: contain;
  width: 100%;
  max-width: calc(100vw - 110px); /* gutters for the nav arrows */
  height: 100%;
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 48px rgba(0,0,0,0.5), 0 0 80px rgba(228,98,44,0.2), 0 0 0 1px rgba(228,98,44,0.3);
  transition: opacity 260ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 300ms ease;
}

/* Phone: the desktop arrow-gutter cap (100vw - 110px) letterboxed the art
   INSIDE its own exact-fit stage box — the thin black bands above/below the
   artwork. The phone stage is JS-sized to the art's exact rect (arrows sit
   OUTSIDE that budget), so the img must be free to fill the box edge to
   edge. */
@media (max-width: 768px) {
  .lightbox-stage img,
  .lightbox-fade {
    max-width: 100%;
  }
}
.lightbox-stage img:hover { box-shadow: 0 32px 64px rgba(0,0,0,0.6), 0 0 120px rgba(228,98,44,0.35), 0 0 0 1px rgba(228,98,44,0.5); }
/* Old artwork under an in-flight crossfade: fades fully OUT in sync with
   the overlay fading IN (same 260ms). A partial dim (old 0.6) left the
   old art's edges visible past the new art's letterbox during
   different-ratio swaps — mismatched rectangles on screen at once, the
   "gutter" glitch. A true dissolve onto the near-black stage reads as
   one clean motion for every ratio pair. */
.lightbox-stage img.dim { opacity: 0; }
.lightbox-fade {
  opacity: 0;
  pointer-events: none;
  z-index: 2;
}
.lightbox-fade.in { opacity: 1; }

.lightbox-caption { flex: 0 0 auto; text-align: center; max-width: min(32ch, calc(100vw - 96px)); padding-bottom: env(safe-area-inset-bottom, 0px); }

/* Related works rail under the lightbox caption: small same-category thumbs.
   Hidden entirely when a category has fewer than 2 siblings (JS controls).
   Constrained to the caption width so the rail can never widen the column
   and push the nav arrows outside the artwork bounds. */
.lightbox-related { flex: 0 0 auto; text-align: center; margin-top: 6px; max-width: min(560px, 86vw); }
.lightbox-related-label {
  font-family: var(--font-data);
  font-size: 0.65rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #B0B0B3;
  margin-bottom: 7px;
}
.lightbox-related-rail { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; max-width: 100%; }
.related-thumb {
  padding: 0; border: 1px solid rgba(255,255,255,0.14); border-radius: 7px;
  overflow: hidden; background: #1A1A1C; cursor: pointer; line-height: 0;
  transition: border-color 200ms ease, transform 200ms ease;
}
.related-thumb:hover { border-color: var(--ember); transform: translateY(-2px); }
.related-thumb.current { opacity: 0.35; cursor: default; }
.related-thumb.current:hover { border-color: rgba(255,255,255,0.14); transform: none; }
.related-thumb img { display: block; width: 92px; height: 69px; object-fit: cover; opacity: 0; transition: opacity 240ms ease; }
.related-thumb img.loaded { opacity: 1; }
@media (max-width: 599px) {
  .related-thumb img { width: 68px; height: 51px; }
}
.lightbox-title {
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 2.5vw, 1.6rem);
  font-weight: 700;
  margin-bottom: var(--spacing-xs);
  text-wrap: balance;
}
.lightbox-category {
  font-family: var(--font-data);
  font-size: clamp(0.8rem, 1.6vw, 0.95rem);
  letter-spacing: var(--mono-tracking);
  color: #B0B0B3;
  line-height: 1.4;
  text-transform: uppercase;
}

.lightbox-close {
  position: absolute;
  top: var(--spacing-sm); right: var(--spacing-sm);
  background: var(--panel);
  border: none;
  color: var(--text);
  width: 40px; height: 40px;
  border-radius: 50%;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-top: 1px;
  transition: background-color 200ms ease, color 200ms ease, transform 150ms ease;
  z-index: 1010;
}
.lightbox-close:hover { background: var(--ember); color: var(--basalt); }
.lightbox-close:active { transform: scale(0.9); background: rgba(228,98,44,0.6); transition-duration: 80ms; }

.lightbox-nav {
  position: absolute;
  top: 50%; transform: translateY(-50%);
  background: var(--panel);
  border: 1px solid rgba(232,230,227,0.1);
  color: var(--text);
  width: 48px; height: 48px;
  border-radius: 50%;
  font-size: 1.5rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 200ms ease, color 200ms ease, border-color 200ms ease, transform 150ms ease;
  z-index: 1010;
}
.lightbox-nav:hover { background: var(--ember); color: var(--basalt); border-color: var(--ember); }
.lightbox-nav:active { transition-duration: 80ms; }
.lightbox-nav.prev:active { transform: translateY(-50%) translateX(-2px) scale(0.9); }
.lightbox-nav.next:active { transform: translateY(-50%) translateX(2px) scale(0.9); }
.lightbox-nav.prev { left: clamp(6px, 1.5vw, 12px); }
.lightbox-nav.next { right: clamp(6px, 1.5vw, 12px); }

@keyframes lightboxSlideIn     { from { opacity: 0.5; transform: translateX(12px); }  to { opacity: 1; transform: translateX(0); } }
@keyframes lightboxSlideInRev  { from { opacity: 0.5; transform: translateX(-12px); } to { opacity: 1; transform: translateX(0); } }
.lightbox-stage img.slide-next { animation: lightboxSlideIn 300ms cubic-bezier(0.4,0,0.2,1) both; }
.lightbox-stage img.slide-prev { animation: lightboxSlideInRev 300ms cubic-bezier(0.4,0,0.2,1) both; }

@keyframes captionFadeIn { from { opacity: 0.45; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.lightbox-caption.animate { animation: captionFadeIn 180ms ease both; }

.lightbox-counter {
  position: static;
  margin-top: var(--spacing-xs);
  text-align: center;
  font-family: var(--font-data);
  font-size: 0.8rem;
  letter-spacing: var(--mono-tracking);
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

/* ======================================================================
   SCROLL REVEAL — the site's one motion voice.
   Soft overshoot (out of a spring), 18px rise, blurless (paint stays
   cheap). Everything that enters on scroll uses this curve, so the
   whole page feels like one hand. */
.reveal {
  opacity: 0;
  transform: translateY(18px) scale(0.985);
  transition: opacity 650ms cubic-bezier(0.22, 1, 0.36, 1),
              transform 650ms cubic-bezier(0.22, 1, 0.36, 1);
}
.reveal.in { opacity: 1; transform: translateY(0) scale(1); }
.reveal-delay-1 { transition-delay: 80ms; }
.reveal-delay-2 { transition-delay: 160ms; }
.reveal-delay-3 { transition-delay: 240ms; }

/* Phone layout guard: nothing may widen the document. The hero glow
   (::after) and any fit-content child could previously shove the page
   right; clip at every major wrapper so overflow is contained locally. */
@media (max-width: 768px) {
  body, .hero, .portfolio-section, .stats-section, .testimonials-section, .about-section, .contact-section, footer {
    overflow-x: clip;
    max-width: 100vw;
  }
  @supports not (overflow: clip) {
    body, .hero, .portfolio-section, .stats-section, .testimonials-section, .about-section, .contact-section, footer { overflow-x: hidden; }
  }
  .hero::after { pointer-events: none; }
  /* grid/flex children must be allowed to shrink below content size */
  .hero-content-centered > * { min-width: 0; max-width: 100%; }
}

/* ======================================================================
   PHONE HERO ART STRIP — shown only where the hero wings are hidden.
   A seamless marquee: inner track translates slowly leftwards; items wrap
   invisibly (duplicate set + teleport). A press pauses the drift and a
   genuine release resumes it; a cancelled press (scroll gesture) stays
   paused until the strip next re-enters view.
   ====================================================================== */
.hero-strip { display: none; }

@media (max-width: 768px) {
  .hero-strip {
    display: block;
    /* The hero column centers children at fit-content width; pin the strip
       to the full column width so it stays inside the phone viewport. */
    width: 100%;
    max-width: 100%;
    align-self: stretch;
    box-sizing: border-box;
    margin-top: var(--spacing-lg);
    padding: 4px 0 8px;
    overflow: hidden; /* the track does the moving, not the scroll position */
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 12px, #000 calc(100% - 28px), transparent);
    mask-image: linear-gradient(90deg, transparent, #000 12px, #000 calc(100% - 28px), transparent);
  }
  .hero-strip-track {
    display: flex;
    gap: 8px;
    width: max-content;      /* both sets side by side, wider than viewport */
    will-change: transform;  /* compositor-friendly: translate3d only */
    transform: translate3d(0, 0, 0);
  }
  .hero-strip-item {
    flex: 0 0 auto;
    border-radius: 10px;
    overflow: hidden;
    border: 1px solid rgba(255,255,255,0.08);
    /* No :active state on purpose: the strip is decorative (press = pause,
       nothing is tappable), so it must not look interactable under a finger. */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
  }
  .hero-strip-item img {
    display: block;
    width: 132px;
    height: 99px;
    object-fit: cover;
    background: #000;
  }
}

/* ======================================================================
   RESPONSIVE
   ====================================================================== */
@media (max-width: 999px) {
  .portfolio-grid { grid-template-columns: repeat(2, 1fr); }
  .portfolio-grid.showcase { grid-auto-rows: clamp(240px, 30vw, 360px); }
}

@media (max-width: 768px) {
  body { padding-top: 52px; } /* shorter mobile nav */
  .scroll-progress { top: 52px; }
  /* Hero collapses to single column, wings hide */
  .hero { grid-template-columns: 1fr; min-height: auto; padding-top: var(--spacing-xl); }
  .hero-wing-left, .hero-wing-right { display: none; }
  .hero-content-centered { grid-column: 1; min-width: 0; max-width: 100%; }
  .hero::after { width: min(400px, 85vw); height: min(400px, 85vw); }

  /* Nav becomes hamburger */
  .nav-container { padding: var(--spacing-xs) var(--spacing-md); height: 52px; }
  .nav-hamburger { display: flex; }

  .nav-backdrop {
    display: block;
    position: fixed;
    inset: 0; top: 52px;
    background: rgba(0,0,0,0.55);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    z-index: 98;
    opacity: 0;
    pointer-events: none;
    transition: opacity 280ms cubic-bezier(0.4,0,0.2,1);
  }
  .nav-backdrop.visible { opacity: 1; pointer-events: auto; }

  .nav-links {
    position: fixed;
    top: 52px; left: 0; right: 0;
    z-index: 99;
    flex-direction: column;
    background: rgba(13,13,14,0.97);
    -webkit-backdrop-filter: blur(18px) saturate(1.3);
    backdrop-filter: blur(18px) saturate(1.3);
    padding: var(--spacing-md) var(--spacing-lg);
    gap: 0;
    border-bottom: 1px solid rgba(228,98,44,0.18);
    box-shadow: 0 20px 60px rgba(0,0,0,0.6);
    transform: translateY(-8px);
    opacity: 0;
    pointer-events: none;
    transition: transform 280ms cubic-bezier(0.4,0,0.2,1), opacity 240ms cubic-bezier(0.4,0,0.2,1);
  }
  .nav-links.open { transform: translateY(0); opacity: 1; pointer-events: auto; }
  .nav-links.open li { animation: navItemIn 280ms cubic-bezier(0.4,0,0.2,1) both; }
  .nav-links.open li:nth-child(1) { animation-delay: 60ms; }
  .nav-links.open li:nth-child(2) { animation-delay: 120ms; }
  .nav-links.open li:nth-child(3) { animation-delay: 180ms; }
  @keyframes navItemIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
  .nav-links a { display: flex; align-items: center; padding: 0.75rem 0; font-size: 0.9375rem; min-height: 44px; }
  .nav-links a::after { transform-origin: center center; }

  /* Tablet: 2-col portfolio; showcase keeps taller rows */
  .portfolio-grid { grid-template-columns: repeat(2, 1fr); grid-auto-rows: clamp(200px, 22vw, 280px); }
  .portfolio-grid.showcase { grid-auto-rows: clamp(240px, 30vw, 360px); }

  /* Stats 2-col on tablet */
  .stats-grid { grid-template-columns: repeat(2, 1fr); }

  /* Filter pills on touch: a compact wrapping row — chips shrink, glyph and
     count trim down, and they flow side by side, wrapping to a second line
     only if truly needed. Compact, not a wall of buttons. */
  .filter-buttons {
    flex-wrap: wrap;
    gap: 0.4rem;
    overflow-x: visible;
    scroll-snap-type: none;
    padding-bottom: 0;
    overscroll-behavior-x: auto;
    -webkit-overflow-scrolling: auto;
  }
  .filter-btn {
    scroll-snap-align: none;
    flex-shrink: 1;
    justify-content: center;
    font-size: 0.7rem;
    padding: 0.38rem 0.62rem;
    letter-spacing: 0.02em;
    gap: 0.32rem;
    min-height: 40px;
  }
  .filter-btn .count { font-size: 0.62rem; }
  .chip-glyph { width: 13px; height: 13px; }
}

@media (max-width: 599px) {
  /* Mobile: single-col portfolio */
  .portfolio-grid { grid-template-columns: 1fr; grid-auto-rows: auto; }
  .portfolio-card { aspect-ratio: 4/3; }
  .portfolio-grid.showcase { grid-template-columns: 1fr; grid-auto-rows: auto; }

  .hero-ctas { flex-direction: row; flex-wrap: wrap; }
  .hero-ctas .btn { flex: 1 1 auto; min-width: 150px; }
  .btn { text-align: center; }

  .lightbox-nav { width: 44px; height: 44px; font-size: 1.2rem; }
  .lightbox-counter { font-size: 0.7rem; }

  footer {
    padding: var(--spacing-md) var(--spacing-sm);
    padding-bottom: calc(var(--spacing-md) + env(safe-area-inset-bottom, 0px));
  }

  /* Reach-out pills stack full-width on small screens */
  .contact-links .social-link { width: 100%; justify-content: flex-start; }
  .social-link .sl-value { white-space: normal; }
}

@media (max-width: 720px) {
  .dribbble-cta {
    flex-direction: column;
    align-items: stretch;
    gap: var(--spacing-sm);
    padding: clamp(0.95rem, 2.6vw, 1.25rem) clamp(0.95rem, 3vw, 1.2rem);
  }
  .dribbble-cta-actions { align-items: stretch; }
  .btn-dribbble { width: 100%; justify-content: center; }
  .dribbble-handle { justify-content: center; }
}

/* ======================================================================
   TOUCH DEVICES — show hover-only affordances permanently,
   enlarge tap targets to >=44px
   ====================================================================== */
@media (hover: none), (pointer: coarse) {
  /* Card titles/categories are hover-only on desktop; show them on touch */
  .card-hover-meta { opacity: 1; transform: translateY(0); }

  /* Filter pills: tap-target height stays (set via min-height in the mobile
     block); padding no longer inflates the size on touch */
  .filter-btn { padding: 0.38rem 0.62rem; }

  /* Disable button scale-on-tap (feels laggy on touch) */
  .btn:active, .filter-btn:active, .social-link:active { transform: none; }
  /* the chipPop keyframes animation still runs on touch (it's not a :active
     transform), just slower to account for the tap registering later */
  .filter-btn.pop { animation-duration: 500ms; }
  /* Pointer-tracking glow is mouse-only; show a steady tint on touch instead */
  .social-link::after { opacity: 1; opacity: 0.35; }
  .social-link:hover { transform: none; }
  .social-link:hover .sl-icon { transform: none; }
  .btn-primary:active { box-shadow: 0 0 16px rgba(228,98,44,0.3), 0 2px 8px rgba(228,98,44,0.2); }

  /* Touch: disable card hover transforms that stick as ghost state */
  .portfolio-card:hover,
  .portfolio-grid.showcase .portfolio-card:hover { --lift-y: 0px; --lift-s: 1; box-shadow: none; --tilt-x: 0deg; --tilt-y: 0deg; }
  .portfolio-card button:hover img { transform: none; filter: none; }
  .portfolio-card button:hover::after { content: none; }

  /* Enlarge lightbox tap targets */
  .lightbox-close,
  .lightbox-nav { width: 44px; height: 44px; }
}

/* Anchor offset matches the shorter sticky nav on small screens */
@media (max-width: 768px) {
  .hero-card { animation: none !important; pointer-events: none; }
  .hero-card:hover { transform: none; }

  .hero, .portfolio-section, .stats-section, .about-section, .contact-section { scroll-margin-top: 60px; }

  /* Lightbox: full-screen layout with safe-area insets, prevent accidental swipe-vs-scroll */
  .lightbox { padding: 12px calc(12px + env(safe-area-inset-right, 0px)) calc(12px + env(safe-area-inset-bottom, 0px)) calc(12px + env(safe-area-inset-left, 0px)); }
  /* Phone: IMAGE-DRIVEN sizing (the original mobile design). The desktop
     fixed-stage-box + object-fit letterboxing wastes huge black bands on a
     tall narrow phone screen; here the stage hugs the artwork — the image
     sizes itself (width-bound for tall sheets, height-bound for wide
     strips) and the column centers vertically. */
  .lightbox-content {
    max-width: 94vw;
    max-height: calc(100vh - 24px);
    max-height: calc(100dvh - 24px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: safe center;
    touch-action: pan-y;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 10px 0;
  }
  .lightbox-stage {
    max-width: 94vw;
    /* JS sets BOTH width and height to the artwork's exact contain-fit
       rect (px, inline), so the box hugs the art — no letterbox bands.
       Ratio changes glide: width/height transition 400ms. */
    transition: width 400ms cubic-bezier(0.4, 0, 0.2, 1),
                height 400ms cubic-bezier(0.4, 0, 0.2, 1);
    position: relative;
    /* grid cell 1/1 stacking for the crossfade overlay */
    display: grid;
  }
  .lightbox-stage > * { grid-area: 1 / 1; }
  .lightbox-stage.waiting { height: min(40dvh, 300px); }
  .lightbox-stage img,
  .lightbox-stage .lightbox-fade {
    width: 100%;
    height: 100%;
    object-fit: contain;
    border-radius: 12px;
    touch-action: pan-y;
  }
  .lightbox-caption { padding-bottom: env(safe-area-inset-bottom, 0px); }

  /* Nav arrows fully on-canvas (44px + 6px inset, inside the 12px modal
     padding — they were clipping at the viewport edge) */
  .lightbox-nav.prev { left: 14px; }
  .lightbox-nav.next { right: 14px; }
}

/* ======================================================================
   REDUCED MOTION
   ====================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    /* !important beats the inline stagger delays main.js sets on grid cards —
       without this, fill-mode leaves every card blank until its delay elapses. */
    animation-delay: 0s !important;
    transition-duration: 0.01ms !important;
  }
  .hero-content-centered h1, .hero-content-centered p, .hero-ctas { animation: none; opacity: 1; transform: none; }
  .hero-accent { animation: none; }
  .hero-scroll-cue { animation: none; opacity: 1; }
  .hero-scroll-cue .cue-chevron { animation: none; }
  .hero:not(.hero-entered) .hero-card { opacity: 1; filter: none; }
  .hero.hero-entered .hero-card { animation: none; opacity: 1; filter: none; }
  .scroll-progress { display: none; }
  .stat-number.landed { animation: none; }
  .filter-btn.pop { animation: none; }
  .chip-featured { animation: none; }
  .chip-glyph { transition: none; }
  .nav-logo-mark { animation: none; }
  .reveal { opacity: 1; transform: none; transition: none; }
  /* Testimonials: the layout-driven design has no carousel motion; the
     .reveal backstop above already zero-animates every card's entrance. */
  #testimonialsItems .testimonial.reveal { transition-delay: 0s; }
  /* Smooth scrolling is a motion too: nav jumps to #contact still glided
     for exactly the users who asked for none. */
  html { scroll-behavior: auto; }
  .hero::after { transform: translateY(-50%); }
  .portfolio-card img { transition: none; }
  .portfolio-card img.loaded { opacity: 1; }
  .lightbox-stage img.slide-next,
  .lightbox-stage img.slide-prev { animation: none; }
  .lightbox-caption.animate { animation: none; }
  .lightbox-stage img.dim { opacity: 1; }
  body::before { animation: none !important; }
}

/* ======================================================================
   TEXT WRAP
   ====================================================================== */
@supports (text-wrap: balance) {
  .hero-content-centered h1 { text-wrap: balance; }
  .hero-content-centered p { text-wrap: pretty; }
  .section-header h2, .about-section h2, .contact-section h2, .stats-section h2, .testimonials-section h2 { text-wrap: balance; }
  .about-section p, .contact-section > p, .testimonial-quote { text-wrap: pretty; }
}

/* ======================================================================
   CSP-ERA UTILITY CLASSES
   (replaced the last inline style="" attributes, which the site's
   Content-Security-Policy in _headers no longer allows)
   ====================================================================== */
.form-honeypot { display: none; }

/* Windows High Contrast: the teal outline survives; the glows don't. */
@media (forced-colors: active) {
  .form-group input, .form-group textarea { outline: 2px solid HighlightText; box-shadow: none; }
}
.footer-apos { color: var(--ember); }
.footer-studio-suffix {
  font-family: var(--font-data);
  font-size: 0.7em;
  opacity: 0.4;
  margin-left: 0.15em;
}

/* ======================================================================
   TESTIMONIALS — show more (PC): cards past row 3 collapse behind the
   toggle. Collapsed cards keep their grid seat logic but display:none
   (grid auto-reflows, rows stay equal). Phone rail: never collapsed.
   ====================================================================== */
@media (min-width: 769px) {
  .testimonial.ts-extra { display: none; }
  .testimonials-stage.ts-shown .testimonial.ts-extra { display: flex; }
}
.ts-extra { display: none !important; }
.testimonials-stage.ts-shown .testimonial.ts-extra { display: flex; }
.ts-morewrap {
  display: flex;
  justify-content: center;
  margin-top: var(--spacing-md);
}
.ts-showmore {
  font-family: var(--font-data);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ts-accent);
  background: transparent;
  border: 1px solid var(--ts-glow);
  border-radius: var(--radius-full);
  padding: 10px 26px;
  cursor: pointer;
  transition: color 200ms ease, border-color 200ms ease, box-shadow 300ms ease, transform 200ms cubic-bezier(0.22, 1, 0.36, 1);
}
.ts-showmore:hover, .ts-showmore:focus-visible {
  color: var(--text);
  border-color: var(--ts-accent);
  box-shadow: 0 0 24px var(--ts-glow-soft);
  transform: translateY(-1px);
}
.ts-showmore:focus-visible { outline: 1px solid var(--ts-glow); outline-offset: 3px; }
.ts-showmore:active { transform: translateY(0); }

/* ================= typography micro-polish ================= */
/* role breathing room: name/role stack gets a hair more air so the
   attribution reads as one confident unit under the quote */
.ts-who { gap: 4px; }
.ts-role { letter-spacing: 0.14em; }

/* phone: no collapse ever; the toggle hides itself if JS left one */
@media (max-width: 768px) {
  .ts-morewrap { display: none; }
}
