/* =========================================================
   TeakTime — Brand Site
   Minimal white layout. Left rail of navigation, right hero
   illustration. Categorized link list. Sans-serif (Saans).
   ========================================================= */

/* -- FONTS ------------------------------------------------- */
/* Saans is a paid typeface (Displaay). Drop the font files into
   /assets/fonts/ and these @font-face rules will pick them up.
   Until they exist, the system falls back to Inter (Google Fonts)
   loaded in the HTML — visually very close, kerning slightly off. */

@font-face {
  font-family: "Saans";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: local("Saans Regular"),
       url("../assets/fonts/Saans-Regular.otf") format("opentype");
}
@font-face {
  font-family: "Saans";
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  src: local("Saans Medium"),
       url("../assets/fonts/Saans-Medium.otf") format("opentype");
}
@font-face {
  font-family: "Saans";
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  src: local("Saans Italic"),
       url("../assets/fonts/Saans-RegularItalic.otf") format("opentype");
}
@font-face {
  font-family: "Saans";
  font-weight: 500;
  font-style: italic;
  font-display: swap;
  src: local("Saans Medium Italic"),
       url("../assets/fonts/Saans-MediumItalic.otf") format("opentype");
}

/* -- DESIGN TOKENS ----------------------------------------- */
:root {
  --bg:           #FFFFFF;
  --ink:          #0A0A0A;
  --ink-muted:    #9A9A9A;     /* italicized category labels */
  --ink-soft:     #5B5B5B;
  --line:         rgba(10, 10, 10, 0.08);
  --hover:        #2F6BFF;     /* link hover blue, matches mock */

  --sans: "Saans", "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  --rail-w: 320px;
}

/* -- RESET ------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { scrollbar-gutter: stable; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img, svg { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }

/* -- PAGE SHELL -------------------------------------------- */
.page {
  position: relative;
  min-height: 100vh;
  display: grid;
  grid-template-columns: var(--rail-w) 1fr;
  grid-template-rows: 1fr auto;
  /* 64px left (menu group) · 300px right (content block to page edge). */
  padding: 0 300px 0 64px;
  gap: clamp(2rem, 5vw, 4rem);
}

/* Left rail: hosts the menu+logo group. The inner cluster sticks to the
   vertical center of the viewport as the user scrolls the page. */
.rail {
  grid-row: 1 / 2;
  align-self: stretch;
  min-width: 0;
}
.rail-inner {
  position: fixed;
  top: 0;
  left: 64px;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  width: max-content;
}

/* Right stage: hero illustration or page content */
.stage {
  grid-row: 1 / 2;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
}

/* Footer (language picker) — always pinned bottom-left at 20px,
   mirroring the V1/V2 toggle at bottom:20px right:20px. */
.foot {
  position: fixed;
  bottom: 20px;
  left: 20px;
  z-index: 30;
  display: flex;
  align-items: center;
}

/* -- LOGO -------------------------------------------------- */
/* The logo is a "stage" — a clipped box. The img inside starts below it
   (translateY(100%) hides it underneath the floor) and rises up. */
.logo {
  display: block;
  margin-bottom: 12px;
  line-height: 1;
  width: 200px;
  aspect-ratio: 555 / 141;   /* matches the cropped SVG */
  overflow: hidden;
}
.logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: left center;
  display: block;
  transform: translateY(100%);
  animation: logoRise 1.6s cubic-bezier(0.19, 1, 0.22, 1) 0.15s forwards;
}
@keyframes logoRise {
  to { transform: translateY(0); }
}

/* Menu items: start hidden + a touch below, animate up into place.
   The animation-delay is set per-item by main.js so the ladder is dynamic
   regardless of how many categories/items the nav has. */
.nav-cat h2,
.nav-cat li {
  opacity: 0;
  transform: translateX(-12px);
  animation: menuRise 0.6s cubic-bezier(0.19, 1, 0.22, 1) both;
}
@keyframes menuRise {
  to { opacity: 1; transform: translateX(0); }
}

@media (prefers-reduced-motion: reduce) {
  .logo img { animation: none; transform: none; }
  .nav-cat h2, .nav-cat li { animation: none; opacity: 1; transform: none; }
}
.logo-text {
  /* Fallback wordmark — when no logo file exists, render text. */
  font-family: "Fraunces", "Cormorant Garamond", Georgia, serif;
  font-weight: 400;
  font-size: clamp(2.75rem, 5vw, 4rem);
  letter-spacing: -0.03em;
  color: var(--ink);
  font-variation-settings: "opsz" 144, "SOFT" 50;
}

/* -- NAV --------------------------------------------------- */
.nav-cat {
  margin-bottom: 12px;
}
.nav-cat:last-child { margin-bottom: 0; }
.nav-cat h2 {
  font-family: var(--sans);
  font-style: normal;
  font-weight: 400;
  font-size: 0.95rem;
  color: var(--ink);
  margin: 0 0 0.35rem;
}
.nav-cat ul {
  list-style: none;
  padding: 0;
  margin: 0;
}
.nav-cat li { margin: 0; }
.nav-cat a {
  display: inline-block;
  padding: 2px 0;
  color: #9A9A9A;
  font-weight: 400;
  font-size: 1.05rem;
  letter-spacing: -0.005em;
  border-bottom: 1px solid transparent;
  transition: opacity 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
/* Gentle glitch hover — soft RGB split, tiny translate, slow loop */
@keyframes nav-glitch {
  0%   { transform: translate(0, 0);      text-shadow: 0 0 transparent; }
  15%  { transform: translate(-0.5px, 0); text-shadow: -0.6px 0 rgba(0,229,255,0.55), 0.6px 0 rgba(255,0,170,0.55); }
  30%  { transform: translate(0.5px, 0);  text-shadow: 0.6px 0 rgba(0,229,255,0.55), -0.6px 0 rgba(255,0,170,0.55); }
  45%  { transform: translate(0, 0);      text-shadow: 0 0 transparent; }
  60%  { transform: translate(-0.5px, 0.5px); text-shadow: -1px 0 rgba(0,229,255,0.5), 1px 0 rgba(255,0,170,0.5); }
  78%  { transform: translate(0.5px, 0);  text-shadow: 0.5px 0 rgba(0,229,255,0.55), -0.5px 0 rgba(255,0,170,0.55); }
  100% { transform: translate(0, 0);      text-shadow: 0 0 transparent; }
}
.nav-cat a:hover,
.nav-cat a:focus-visible {
  opacity: 1;
  outline: none;
  color: var(--ink);
}
/* CURRENT PAGE: glitch animation runs constantly (no blue, no underline). */
.nav-cat a[aria-current="page"] {
  color: var(--ink);
  opacity: 1;
  font-weight: 500;       /* medium weight — highlight the current page */
}
.nav-cat a[aria-current="page"] .nav-text {
  display: inline-block;
  animation: nav-glitch 0.9s linear infinite;
}
/* When a current page is highlighted, every other nav item dims to the
   eyebrow gray. Hovering any of those restores normal ink + glitch. */
/* Active state inverts the prior behavior — other links keep their default
   soft-gray color (already #9A9A9A); only the active link is ink + glitch. */
nav .nav-cat a:hover,
nav .nav-cat a:focus-visible {
  color: var(--ink);
}

/* -- HERO IMAGE ON HOME ------------------------------------ */
.hero-art {
  width: 100%;
  max-width: 880px;
  display: block;
  margin-top: 24px;            /* drops the hero down by 24px */
  /* Comes in last, after the logo and the full menu ladder have settled. */
  opacity: 0;
  transform: translateY(20px);
  filter: blur(18px);          /* graceful blur-to-clear unveil during entrance */
  will-change: filter, opacity, transform;   /* hint GPU layer so blur transition stays smooth */
  animation: heroIn 1.8s cubic-bezier(0.19, 1, 0.22, 1) 1.75s forwards;   /* starts after logoRise finishes (0.15 + 1.6 = 1.75s); slower duration for a more graceful blur ease */
}
.hero-art img {
  width: 100%;
  height: auto;
}
@keyframes heroIn {
  to { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-art { animation: none; opacity: 1; transform: none; filter: none; }
}

/* -- IN-SITE NAV TRANSITION -------------------------------- */
/* When the user clicks a menu item, the destination page loads with
   .from-nav on <html>: the rail skips its entrance animation (it was
   already on screen) and the right-side content fades up. */
/* On in-site nav to a non-home page, the rail (logo + menu items) was
   already on screen — skip their entrance animations so only the right-side
   content fades up. Hard reload and going to the home page DON'T get
   .from-nav, so they still play the full rail animation. */
.from-nav .logo img {
  animation: none;
  transform: none;
}
.from-nav .nav-cat h2,
.from-nav .nav-cat li {
  animation: none;
  opacity: 1;
  transform: none;
}
.from-nav .prose {
  opacity: 0;
  transform: translateY(24px);
  animation: contentIn 1.6s cubic-bezier(0.22, 0.61, 0.36, 1) 0.05s forwards;
}
.from-nav .hero-art { animation-delay: 0.05s; }
@keyframes contentIn {
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .from-nav .prose { animation: none; opacity: 1; transform: none; }
}

/* -- LANGUAGE PICKER (bottom-left) ------------------------- */
.lang-picker {
  position: relative;
  display: inline-flex;
  background: rgba(10, 10, 10, 0.06);
  border-radius: 999px;
  padding: 4px;
  font: 14px/1 "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  letter-spacing: 0.02em;
}
.lang-picker::before {
  content: '';
  position: absolute;
  bottom: 100%;
  left: 0;
  right: 0;
  height: 14px;
}
.lang-pill {
  display: inline-flex;
  align-items: center;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: inherit;
  font: inherit;
}
.lang-pill .lang-name {
  padding: 8px 16px;
  border-radius: 999px;
  background: #efefef;
  color: #0A0A0A;
}
.lang-pill .lang-flag {
  padding: 8px 14px;
  color: #0A0A0A;
  opacity: 0.6;
  font-size: 14px;
  line-height: 1;
}
.lang-popup {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: rgba(10, 10, 10, 0.06);
  border-radius: 16px;
  padding: 4px;
  min-width: 100%;
  opacity: 0;
  pointer-events: none;
  transform: translateY(6px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.lang-picker:hover .lang-popup,
.lang-picker:focus-within .lang-popup {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}
.lang-option {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  background: transparent;
  border: 0;
  border-radius: 999px;
  padding: 0;
  font: inherit;
  letter-spacing: 0.02em;
  cursor: pointer;
  color: var(--ink);
  text-align: left;
  transition: background 0.15s ease;
}
.lang-option:hover,
.lang-option:focus-visible {
  background: rgba(10, 10, 10, 0.10);
  outline: none;
}
.lang-option .lang-name { padding: 8px 16px; }
.lang-option .lang-flag { padding: 8px 14px; opacity: 0.6; font-size: 14px; line-height: 1; }

/* -- INNER-PAGE LAYOUTS ------------------------------------ */
/* Other pages reuse the rail but the stage becomes prose content. */
/* Inner content pages pin the prose block to the far right of the stage,
   so the .prose's right edge sits 300px from the page edge (page padding). */
.stage--page {
  align-items: flex-start;
  justify-content: flex-end;
  padding-top: 64px;
}
.body--faye .stage--page,
.body--samira .stage--page,
.body--manifesto .stage--page,
.body--story .stage--page { padding-top: 112px; }
.body--faye .prose h2,
.body--samira .prose h2,
.body--manifesto .prose h2,
.body--story .prose h2 { margin: 48px 0 8px; }
.body--faye .prose p,
.body--samira .prose p,
.body--manifesto .prose p,
.body--story .prose p  { margin: 0 0 12px; }

/* Character-page title row: h1 hugs its content; marker sits 8px to its right,
   raised above the centerline, 4px taller than the h1, and runs the same
   glitch loop as the h1 (drop-shadow stand-in for text-shadow).
   The eyebrow-row mirrors the same pattern at eyebrow scale. */
.body--faye .title-row,
.body--samira .title-row,
.body--manifesto .title-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 1.25rem;
}
.body--faye .title-row h1,
.body--samira .title-row h1,
.body--manifesto .title-row h1 { margin: 0; }

.body--faye .title-marker,
.body--samira .title-marker,
.body--manifesto .title-marker {
  /* Both default to ink — override --marker-inner later for a two-tone look. */
  --marker-outer: var(--ink);
  --marker-inner: var(--marker-outer);
  font-size: clamp(1.75rem, 3.5vw, 2.25rem);
  height: calc(1em + 4px);
  width: auto;
  display: block;
  flex-shrink: 0;
  position: relative;
  top: -10px;
  /* Neg margins absorb the marker's overshoot vs the h1 so the row doesn't
     expand — keeps the h1-to-lead gap identical to Samira's h1-to-paragraph gap. */
  margin-block: -2px;
  animation: marker-glitch 0.9s linear infinite;
}
.body--faye .title-marker .marker-outer,
.body--samira .title-marker .marker-outer,
.body--manifesto .title-marker .marker-outer { fill: var(--marker-outer); }
.body--faye .title-marker .marker-inner,
.body--samira .title-marker .marker-inner,
.body--manifesto .title-marker .marker-inner { fill: var(--marker-inner); }
@keyframes marker-glitch {
  0%   { transform: rotate(10deg) translate(0, 0);          filter: none; }
  15%  { transform: rotate(10deg) translate(-0.5px, 0);     filter: drop-shadow(-0.6px 0 rgba(0,229,255,0.55)) drop-shadow(0.6px 0 rgba(255,0,170,0.55)); }
  30%  { transform: rotate(10deg) translate(0.5px, 0);      filter: drop-shadow(0.6px 0 rgba(0,229,255,0.55)) drop-shadow(-0.6px 0 rgba(255,0,170,0.55)); }
  45%  { transform: rotate(10deg) translate(0, 0);          filter: none; }
  60%  { transform: rotate(10deg) translate(-0.5px, 0.5px); filter: drop-shadow(-1px 0 rgba(0,229,255,0.5)) drop-shadow(1px 0 rgba(255,0,170,0.5)); }
  78%  { transform: rotate(10deg) translate(0.5px, 0);      filter: drop-shadow(0.5px 0 rgba(0,229,255,0.55)) drop-shadow(-0.5px 0 rgba(255,0,170,0.55)); }
  100% { transform: rotate(10deg) translate(0, 0);          filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .body--faye .title-marker,
  .body--samira .title-marker,
  .body--manifesto .title-marker { animation: none; }
}

/* Character pages: content eyebrow mirrors the rail's .nav-cat h2 styling. */
.body--faye .prose .eyebrow,
.body--samira .prose .eyebrow,
.body--manifesto .prose .eyebrow,
.body--story .prose .eyebrow {
  font-family: var(--sans);
  font-style: normal;
  font-weight: 400;
  font-size: 0.95rem;
  letter-spacing: normal;
  text-transform: none;
  color: #9A9A9A;
  margin: 0 0 24px;
}
.prose {
  max-width: 640px;
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--ink);
}
.prose .eyebrow {
  /* Global eyebrow style for V1.5 + V2: sentence case, soft gray, upright.
     Mirrors the rail-nav h2 + character-page eyebrow rather than the old
     uppercase + letter-spaced look. Per-body-class rules below still apply
     for Faye/Samira/Manifesto/Story but are now functionally identical. */
  font-family: var(--sans);
  font-size: 0.95rem;
  font-style: normal;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: #9A9A9A;
  margin: 0 0 24px;
}
/* Hide the "← Back" link everywhere — the rail handles navigation; no
   redundant back affordance on content pages. (Standing rule for V1.5 + V2.) */
.prose .back,
.back {
  display: none !important;
}
.prose h1 {
  font-family: var(--sans);
  font-weight: 500;            /* medium, not bold */
  font-size: clamp(1.75rem, 3.5vw, 2.25rem);
  letter-spacing: -0.02em;
  line-height: 1.05;
  margin: 0 0 1.25rem;
  /* Glitch animation is always on — same loop as active menu item */
  animation: nav-glitch 0.9s linear infinite;
}
.prose .lead {
  font-family: var(--sans);
  font-style: normal;
  font-size: 0.9rem;
  line-height: 1.65;
  color: var(--ink-soft);
  margin: 0 0 24px;
}
.prose h2 {
  font-family: var(--sans);
  font-weight: 500;            /* medium — same as h1, gets the same glitch */
  font-size: 1rem;
  letter-spacing: -0.005em;
  margin: 48px 0 12px;         /* site-wide: 48px between sections, 12px h2 → p */
  animation: nav-glitch 0.9s linear infinite;
}
.prose p { margin: 0 0 12px; }
.prose p:last-child { margin-bottom: 0; }

/* Standalone illustrations — used as <img class="illo"> directly,
   12px rounded corners on every illustration. */
img.illo {
  display: block;
  width: 100%;
  height: auto;
  margin: 24px 0;
  border-radius: 12px;
}
/* Visible container for the video. Holds the aspect-ratio, stroke, bg, and
   the overflow:hidden that clips the scaled video inside.
   Lives INSIDE .illo-video-wrap so the wrap's aurora glow (inset:-28px) is
   untouched by this clipping. */
.illo-video-clip {
  display: block;
  width: 100%;
  aspect-ratio: 610 / 160;          /* taller container — +40px height at 610px wide */
  border-radius: 12px;
  border: 1px solid #EFEDF0;
  background: #F9F7FA;
  overflow: hidden;
  position: relative;
  z-index: 1;
}
/* Bio pages: the eye close-up flips to a taller 16:9 aspect EARLY (≤1100px),
   well before the full mobile sweep kicks in at ≤900px. Reason: in the
   desktop layout (.page padding 0 300px 0 64px), the prose column gets
   crowded as the viewport narrows. The 610:160 strip would dip below ~60%
   of its desktop-max size around viewport 1000-1100 — that's when we
   switch to 16:9 so the illustration keeps a substantial height instead
   of becoming a thin sliver. Aspect ratio itself is always honored
   (no stretching); we just swap which ratio is in effect. */
@media (max-width: 1100px) {
  .body--faye .illo-video-clip,
  .body--samira .illo-video-clip {
    aspect-ratio: 16 / 9;
  }
}
.illo--video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  /* Shift the cover-cropped frame down so the eyes sit in view, then zoom
     the CONTENT in 3x. Transform stays inside the clip, container is fixed. */
  object-position: center calc(50% + 40px);
  transform: scale(1.1);
  transform-origin: center;
}

/* Aurora-borealis style halo around the video container.
   Two-tone gold conic-gradient on a blurred pseudo-element behind the video;
   the gradient angle drifts via a @property-driven custom property. */
@property --aurora-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
.illo-video-wrap {
  position: relative;
  margin: 24px 0;
  isolation: isolate;
  z-index: 0;             /* wrap sits below sibling copy so the glow can bleed without covering text */
}
/* Lift all prose children above the wrap so the halo never paints over copy. */
.body--faye .prose > * { position: relative; z-index: 1; }
.body--faye .illo-video-wrap { z-index: 0; }
.illo-video-wrap > video.illo {
  margin: 0;
  position: relative;
  z-index: 1;
}
.illo-video-wrap::before {
  content: '';
  position: absolute;
  inset: -28px;
  border-radius: 28px;
  background: conic-gradient(
    from var(--aurora-angle),
    rgba(212, 175, 55, 0.108) 0%,
    rgba(30,  21,  4, 0.18) 25%,
    rgba(212, 175, 55, 0.108) 50%,
    rgba(30,  21,  4, 0.18) 75%,
    rgba(212, 175, 55, 0.108) 100%
  );
  filter: blur(56px);
  z-index: 0;
  pointer-events: none;
  animation: aurora-shift 4s linear infinite;
}
@keyframes aurora-shift {
  to { --aurora-angle: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
  .illo-video-wrap::before { animation: none; }
}
.illo-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  margin: 24px 0;
}
.illo-row img.illo {
  margin: 0;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border: 1px solid #EFEDF0;
  background: #DAD8E2;          /* fill behind transparent PNG headshots */
  border-radius: 12px;
  overflow: hidden;
}

/* Bio-page side illustration tilt — the .two-col img.illo on Faye + Samira
   gets a subtle 2° "rolled polaroid" tilt for visual character. */
.body--faye .two-col img.illo,
.body--samira .two-col img.illo {
  transform: rotate(2deg);
  transform-origin: center center;
}
.two-col {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: 24px;
  margin: 48px 0 0;                 /* matches section spacing: 48px top, next h2 supplies its own 48 below */
  align-items: start;
}
.two-col > p { margin: 0; }
.two-col > p + p { margin-top: 12px; }
/* When the text column wraps content in a div (e.g. h2 + p), strip the
   first child's top margin so it top-aligns with the image. Body-scoped to
   outrank the .body--faye .prose h2 margin override. */
.body--faye .two-col > div > :first-child,
.body--samira .two-col > div > :first-child,
.body--manifesto .two-col > div > :first-child { margin-top: 0; }
.two-col img.illo {
  margin: 0;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  /* SVG stroke would get cropped on left/right by object-fit: cover, so the
     stroke lives on the img element itself to trace the container exactly. */
  border: 1px solid #EFEDF0;
  background: #F9F7FA;
}
@media (max-width: 720px) {
  .illo-row { grid-template-columns: repeat(2, 1fr); }
  .two-col  { grid-template-columns: 1fr; }
}
.prose p { margin: 0 0 1.05em; }
.prose p:last-child { margin-bottom: 0; }
.prose em { font-style: italic; color: var(--ink-soft); }
.prose hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: 48px 0;             /* 48px above + below the divider, site-wide */
}
.prose ul {
  padding-left: 1.1rem;
  margin: 0 0 1rem;
}
.prose li {
  margin-bottom: 0.4rem;
}
.prose a.inline {
  color: var(--hover);
  border-bottom: 1px solid var(--hover);
}
.prose figure {
  margin: 2rem 0;
}
.prose figure img {
  border-radius: 4px;
}
.prose figcaption {
  font-size: 0.82rem;
  color: var(--ink-muted);
  font-style: italic;
  margin-top: 0.5rem;
}
.back {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 2rem;
  font-size: 0.88rem;
  color: var(--ink-soft);
  border-bottom: 1px solid transparent;
}
.back:hover { color: var(--hover); border-bottom-color: var(--hover); }

/* -- RESPONSIVE -------------------------------------------- */
@media (max-width: 1024px) {
  .page {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto;
    gap: 2rem;
  }
  .rail { grid-row: 1 / 2; justify-content: flex-start; }
  .stage { grid-row: 2 / 3; }
  .foot { grid-column: 1; grid-row: 3 / 4; }
  .logo img { height: 44px; }
}

/* Callout / styled quote box (Squa) — outline only, no fill. */
.callout {
  background: transparent;
  border: 1px solid #EFEDF0;
  border-radius: 12px;
  padding: 24px;
  margin: 48px 0;             /* section rhythm — same as h2 / hr / two-col */
  text-align: left;
  position: relative;
}
/* Decorative close-quote mark in the top-right corner. */
.callout::after {
  content: "";
  position: absolute;
  top: 22px;
  right: 26px;
  width: 32px;
  height: 32px;
  background: url("../quote%201.svg") no-repeat center / contain;
  pointer-events: none;
  animation: quote-glitch 0.9s linear infinite;
}
/* Same glitch loop as the h1 marker, minus the static 10deg tilt. */
@keyframes quote-glitch {
  0%   { transform: translate(0, 0);          filter: none; }
  15%  { transform: translate(-0.5px, 0);     filter: drop-shadow(-0.6px 0 rgba(0,229,255,0.55)) drop-shadow(0.6px 0 rgba(255,0,170,0.55)); }
  30%  { transform: translate(0.5px, 0);      filter: drop-shadow(0.6px 0 rgba(0,229,255,0.55)) drop-shadow(-0.6px 0 rgba(255,0,170,0.55)); }
  45%  { transform: translate(0, 0);          filter: none; }
  60%  { transform: translate(-0.5px, 0.5px); filter: drop-shadow(-1px 0 rgba(0,229,255,0.5)) drop-shadow(1px 0 rgba(255,0,170,0.5)); }
  78%  { transform: translate(0.5px, 0);      filter: drop-shadow(0.5px 0 rgba(0,229,255,0.55)) drop-shadow(-0.5px 0 rgba(255,0,170,0.55)); }
  100% { transform: translate(0, 0);          filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .callout::after { animation: none; }
}
.callout p {
  margin: 0 0 8px;
  color: var(--ink-soft);
  font-style: normal;
  animation: nav-glitch-soft 2.6s ease-in-out infinite;
}
/* Softer glitch — gentler offsets, eased timing, longer cycle. Used on the
   callout follow-up lines so they shimmer instead of stutter. */
@keyframes nav-glitch-soft {
  0%   { transform: translate(0, 0);          text-shadow: 0 0 transparent; }
  20%  { transform: translate(-0.3px, 0);     text-shadow: -0.3px 0 rgba(0,229,255,0.30), 0.3px 0 rgba(255,0,170,0.30); }
  40%  { transform: translate(0.3px, 0);      text-shadow: 0.3px 0 rgba(0,229,255,0.30), -0.3px 0 rgba(255,0,170,0.30); }
  60%  { transform: translate(0, 0.3px);      text-shadow: 0 -0.3px rgba(0,229,255,0.25), 0 0.3px rgba(255,0,170,0.25); }
  80%  { transform: translate(0.2px, 0);      text-shadow: 0.2px 0 rgba(0,229,255,0.20), -0.2px 0 rgba(255,0,170,0.20); }
  100% { transform: translate(0, 0);          text-shadow: 0 0 transparent; }
}
.callout p:last-child { margin-bottom: 0; }

/* ====================================================================
   VHS-glitch text effect (Netflix-intro style)
   ====================================================================
   Combines:
     1. SVG fractal-noise displacement filter (via #vhs-distort defined
        inline on the page) — gives each letter a chewed-up,
        scratchy edge as if it were filmed off a worn tape.
     2. Subtle constant chromatic split (text-shadow cyan + magenta).
     3. Periodic intensification flashes — `vhs-pulse` every 5s spikes
        the chromatic split + nudges the text horizontally a few px,
        mimicking the dropout flicker in the source.
     4. Inline SVG filter animates its turbulence seed via JS (see
        manifestoTitleGlitch IIFE in main.js) so the distortion pattern
        keeps shifting — without that, the edges would be static.
   Optional letter-scramble (a glyph briefly replaced with a corrupted
   character) is JS-driven from the same IIFE.

   Scoped to .vhs-glitch so we can opt-in per-title. */
.vhs-glitch {
  position: relative;
  display: block;             /* block so margin-bottom collapses with the hr below — matches the Manifesto title-row's spacing on every page */
  width: max-content;         /* keeps the filter region tight to the text (was the reason inline-block was used) */
  max-width: 100%;
  filter: url(#vhs-distort);
  text-shadow:
    -1px 0 rgba(0, 229, 255, 0.35),
     1px 0 rgba(255, 0, 170, 0.35);
  animation: vhs-pulse 5s steps(1, end) infinite;
  will-change: transform, text-shadow, filter;
}
/* The SVG filter container itself takes no layout space. */
.vhs-glitch-svg { position: absolute; width: 0; height: 0; pointer-events: none; }
@keyframes vhs-pulse {
  /* Calm baseline */
  0%, 65% {
    transform: translate(0, 0);
    text-shadow: -1px 0 rgba(0, 229, 255, 0.35), 1px 0 rgba(255, 0, 170, 0.35);
  }
  /* First spike — sudden hard split + L offset */
  68% {
    transform: translate(-3px, 0);
    text-shadow: -4px 0 rgba(0, 229, 255, 0.85), 4px 0 rgba(255, 0, 170, 0.85);
  }
  70% {
    transform: translate(3px, 0);
    text-shadow: 4px 0 rgba(0, 229, 255, 0.85), -4px 0 rgba(255, 0, 170, 0.85);
  }
  72% {
    transform: translate(0, 1px);
    text-shadow: -1px 0 rgba(0, 229, 255, 0.35), 1px 0 rgba(255, 0, 170, 0.35);
  }
  /* Brief settle */
  73%, 88% {
    transform: translate(0, 0);
    text-shadow: -1px 0 rgba(0, 229, 255, 0.35), 1px 0 rgba(255, 0, 170, 0.35);
  }
  /* Second smaller spike — vertical drop-frame */
  90% {
    transform: translate(0, -2px);
    text-shadow: -2px 0 rgba(0, 229, 255, 0.65), 2px 0 rgba(255, 0, 170, 0.65);
  }
  92% {
    transform: translate(0, 2px);
  }
  100% {
    transform: translate(0, 0);
    text-shadow: -1px 0 rgba(0, 229, 255, 0.35), 1px 0 rgba(255, 0, 170, 0.35);
  }
}
@media (prefers-reduced-motion: reduce) {
  .vhs-glitch {
    animation: none;
    filter: none;
    text-shadow: none;
  }
}

/* Lead question — italic + medium-weight + the same glitch loop as h2s. */
.callout .callout-question {
  font-family: var(--sans);
  font-weight: 500;
  font-style: normal;
  font-size: 1rem;
  letter-spacing: -0.005em;
  color: var(--ink);
  margin-bottom: 16px;
  animation: nav-glitch 0.9s linear infinite;
}

/* Aside punchline — a short h2 pulled out of section-heading flow, centered
   with breathing room so it reads as an interjection, not a section title. */
.prose h2.aside {
  text-align: center;
  font-style: italic;
  font-weight: 400;
  color: var(--ink-soft);
  margin: 40px 0 40px;
  animation: none;
}

/* h2.balanced — equal vertical breathing room above and below (overrides
   the default .prose h2 24/8 split) for standalone punchline-style lines. */
.prose h2.balanced { margin: 48px 0 48px; }

/* Universal rule — raster assets (JPG/PNG) flowing into content containers
   inherit the same stroke + fill as the SVG placeholders, so swapping a
   placeholder for a photo Just Works without per-spot CSS. */
img.illo:not([src$=".svg"]) {
  border: 1px solid #EFEDF0;
  background: #F9F7FA;
}

/* Nav-link arrow — sits 4px to the left of each menu label. Rendered via
   mask-image on a ::before so the fill inherits the link's color via
   `background-color: currentColor`. When the link is active (or hovered),
   the arrow runs its own glitch loop using filter: drop-shadow (since
   text-shadow doesn't apply to masked pseudo-elements). */
.nav-cat a::before {
  content: "/";
  display: inline-block;
  margin-right: 6px;
}
/* Backslash never animates — color inherits from the link automatically. */

/* ====== BENCHIES ROW 1 (5-image strip under the h1) ======
   Floating squares — NO stroke, NO fill, NO radius. The padding lives inside each asset.
   Question marks are inlined SVGs using fill="currentColor", so the color is just
   `color:` on the element — flip --qmark-color to recolor all three at once. */
.benchies-row-1 {
  /* default question-mark color — change this one line to recolor all qmarks */
  --qmark-color: var(--ink);
  /* Auto-spaced row: items keep their square size, the leftover space
     auto-distributes between them via space-between. Row fills the prose width. */
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  margin: 24px 0 32px;
  gap: 0;
}
.benchies-row-1 .brow-item {
  display: block;
  /* V1.5: scaled up 25% (24% → 30%). 3 items × 30% = 90%, remaining 10%
     becomes 2 auto gaps (~5% each). Row still fills the prose container. */
  flex: 0 0 30%;
  width: 30%;
  min-width: 0;
  height: auto;
  aspect-ratio: 1 / 1;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: 0;
  box-shadow: none;
  outline: 0;
}
/* Photo tile wrapper — holds the img + the hover-pill tooltip (desktop only). */
.benchies-row-1 .brow-tile {
  position: relative;
  overflow: visible;
}
.benchies-row-1 .brow-tile img.brow-photo,
.benchies-row-1 img.brow-photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* The tooltip — black bg, white text, sits BELOW the tile (never overlaps the image).
   Slight fade-up on hover. */
.benchies-row-1 .brow-tile::after {
  content: attr(data-tooltip);
  position: absolute;
  top: calc(100% + 10px);          /* 10px gap between bottom of tile and top of pill */
  left: 50%;
  transform: translate(-50%, -4px);
  background: #0A0A0A;
  color: #fff;
  padding: 8px 14px;
  border-radius: 8px;
  font: 500 13px/1 ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  letter-spacing: 0.01em;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .18s ease, transform .18s ease;
  z-index: 2;
  box-shadow: 0 4px 16px rgba(10, 10, 10, 0.18);
}
/* Desktop with a real pointer only — no hover tooltips on touch devices. */
@media (hover: hover) and (pointer: fine) {
  .benchies-row-1 .brow-tile:hover::after {
    opacity: 1;
    transform: translate(-50%, 0);
  }
}
.benchies-row-1 svg.brow-qmark {
  color: var(--qmark-color);
  overflow: visible;
  /* Container — stroke only (same 1px #EFEDF0 as other detail-page illos),
     no background fill, 20px corner radius. */
  border: 1px solid #EFEDF0;
  background: transparent;
  border-radius: 20px;
}
/* per-item color overrides — uncomment / edit anytime
.benchies-row-1 .brow-qmark--c { color: #ff3b30; }
.benchies-row-1 .brow-qmark--d { color: #2F6BFF; }
.benchies-row-1 .brow-qmark--e { color: #9A9A9A; }
*/

/* ====== FAYE TILE POPUPS (Benchies row) ======
   JS-driven via .popup-in / .popup-out classes (see main.js → fayePopups IIFE).
   Pure CSS :hover can't do asymmetric in/out animations, so JS toggles classes:
     - .popup-in  → fade up + spread + tilt (both opacity AND transform animate)
     - .popup-out → fade in place (ONLY opacity animates; transform stays put)
   After .popup-out completes, JS removes the class so transform silently snaps
   back to the resting position (invisible at opacity 0). Next hover starts fresh. */
/* Faye + Samira Benchies — no animation. Tile is a static photo + link to
   the bio page. Popups + sparkles are hidden; the JS skips these tiles, so
   spotlight/portal never fires either. */
.benchies-row-1 .brow-tile--faye .brow-popup,
.benchies-row-1 .brow-tile--samira .brow-popup,
.benchies-row-1 .brow-tile--faye .brow-tile-sparkle,
.benchies-row-1 .brow-tile--samira .brow-tile-sparkle {
  display: none !important;
}

.benchies-row-1 .brow-popup {
  position: absolute;
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  height: auto;
  object-fit: contain;
  border: 1px solid #EFEDF0;
  background: var(--bg);
  border-radius: 16px;
  opacity: 0;
  pointer-events: none;
  top: calc(-100% - 20px);
  left: 0;
  transform-origin: center center;
  /* Promote to its own GPU layer so opacity + transform animate at 60fps
     without re-rasterising the (still-large) PNG every frame. translateZ(0)
     forces layer creation in older browsers; will-change tells the modern
     compositor to keep the layer warm during hover. */
  will-change: transform, opacity;
  transform: translateZ(0);
  backface-visibility: hidden;
  /* No transition on the base rule — when .popup-out is removed at the end of
     the fade, transform snaps from hover-pos → resting silently while
     opacity is already 0. Transitions live on the .popup-in/.popup-out rules. */
}

/* Resting transforms — each popup starts 40px below final position. */
.benchies-row-1 .brow-popup--1 {
  z-index: 5;
  transform: translateY(40px);
}
.benchies-row-1 .brow-popup--2,
.benchies-row-1 .brow-popup--3 {
  z-index: 3;
  transform: translate(0, 40px) rotate(0deg);
}

/* Hover-in: animate UP + spread + fade. */
.benchies-row-1 .brow-popup.popup-in {
  opacity: 1;
  transition:
    opacity 800ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 800ms cubic-bezier(0.22, 1, 0.36, 1);
}
.benchies-row-1 .brow-popup--1.popup-in { transform: translateY(0); }
.benchies-row-1 .brow-popup--2.popup-in { transform: translate(-90%, 14%) rotate(-10deg); }
.benchies-row-1 .brow-popup--3.popup-in { transform: translate(90%, 14%) rotate(10deg); }

/* Hover-out: fade in PLACE — transform stays at hover position, only opacity animates. */
.benchies-row-1 .brow-popup.popup-out {
  opacity: 0;
  transition: opacity 800ms cubic-bezier(0.22, 1, 0.36, 1);
}
.benchies-row-1 .brow-popup--1.popup-out { transform: translateY(0); }
.benchies-row-1 .brow-popup--2.popup-out { transform: translate(-90%, 14%) rotate(-10deg); }
.benchies-row-1 .brow-popup--3.popup-out { transform: translate(90%, 14%) rotate(10deg); }

/* ====== BASE-IMG NAVIGATION LINK (Benchies row) ======
   Wraps the Faye/Samira photo in an <a> so clicking the artwork takes the
   visitor to that character's detail page. Sized to fill the tile so the
   whole base image area is the click target. Popups (sibling of the link
   inside the same .brow-tile) keep pointer-events: none so clicks on them
   pass through to empty space, not this link. */
.benchies-row-1 .brow-photo-link {
  display: block;
  width: 100%;
  height: 100%;
  text-decoration: none;
  color: inherit;
  /* The base img sits below the popups in z-stack — popups have z-index 3+;
     the link has the default static stacking, so it's behind them. Good. */
}
.benchies-row-1 .brow-photo-link:focus-visible {
  outline: 2px solid var(--hover);
  outline-offset: 4px;
  border-radius: 6px;
}

/* qmark SVG fills its .brow-tile wrapper */
.benchies-row-1 .brow-tile--qmark > svg.brow-qmark {
  display: block;
  width: 100%;
  height: 100%;
}

/* ====== PAGE BLUR (character-tile hover spotlight) ======
   When the visitor hovers a character tile that has popups (Faye, Samira),
   a translucent blurred sheet drops over the entire page. The hovered tile
   gets raised to z-index 1100 so it + its popups + its tooltip pop above the
   sheet, while the rail, paragraphs, other tiles, and everything else fades
   behind a soft white blur. The sheet fades in/out over 800ms — matched to
   the popup rise duration so they appear together. Driven by characterPopups
   in main.js: it adds .is-active to .page-blur and .is-spotlit to the tile. */
.page-blur {
  position: fixed;
  inset: 0;
  background: rgba(255, 255, 255, 0.4);
  -webkit-backdrop-filter: blur(16px);
          backdrop-filter: blur(16px);
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* Base = used when going INACTIVE: opacity fades, then visibility flips at the end. */
  transition: opacity 800ms cubic-bezier(0.22, 1, 0.36, 1), visibility 0s 800ms;
}
.page-blur.is-active {
  opacity: 1;
  visibility: visible;
  /* Going ACTIVE: visibility flips immediately so the fade-in is visible. */
  transition: opacity 800ms cubic-bezier(0.22, 1, 0.36, 1), visibility 0s;
}
.benchies-row-1 .brow-tile.is-spotlit {
  z-index: 1100;
}

/* tile placeholder (created by JS while a tile is portalled to body)
   Contains a clone of the character's base img so the row position keeps
   showing the character — blurred under the page-blur layer. No "reappear"
   flash when the portal unwinds; the blurred clone has been visible the
   whole time and seamlessly takes over when the original returns. */
.benchies-row-1 .brow-tile-placeholder {
  display: block;
  position: relative;
  /* Inherits .brow-item flex sizing so the row layout stays untouched. */
}
.benchies-row-1 .brow-tile-placeholder > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Quick-fade an outgoing tile when the user switches characters — added by JS
   on the *previous* tile when mouseenter fires on a *different* tile. Fades the
   whole tile (base img + popups + tooltip) over 300ms, then unportals silently.
   Faster than the normal 800ms fade-out so the new character can take focus
   without the old one lingering above the blur. */
.brow-tile.is-leaving {
  transition: opacity 300ms ease;
  opacity: 0;
  pointer-events: none;
}

/* ====== CHARACTER-TILE SPARKLES (Benchies row) ======
   Two decorative sparkles per character tile, animated in a loop while the
   tile is .is-spotlit. Each runs: slow grow + start of spin → fast spin at
   full size → slow shrink back to 0. Sparkle B is staggered 0.6s after A
   for a laddered feel. Color via currentColor (path fill = currentColor). */
.brow-tile-sparkle {
  position: absolute;
  display: block;
  pointer-events: none;
  color: var(--ink);            /* swap to recolor */
  opacity: 0;
  z-index: 6;                   /* above popups (z 3-5) and tooltip (z 2) */
  transform-origin: center center;
}
.brow-tile-sparkle svg {
  /* Only applies to a *nested* SVG inside a wrapper — does NOT match the
     sparkle SVG itself (which has .brow-tile-sparkle directly on it).
     Removed `svg.brow-tile-sparkle` because element-type specificity was
     overriding the per-sparkle px width/height. */
  width: 100%;
  height: 100%;
  display: block;
}

/* ----- SPARKLE SIZE + POSITION (tweak these to relocate/resize) -----
   apex size: width + height (also update the two negative margins to half
   of that, so the sparkle stays centered on its anchor point).
   position: left/top % within the tile container.                         */
.brow-tile-sparkle--a {
  width: 24px;                  /* APEX SIZE A */
  height: 24px;
  left: 3px;                    /* shifted right 3px from the tile's left edge */
  top: 40%;
  margin-left: -12px;           /* = -½ × width */
  margin-top: -12px;
}
.brow-tile-sparkle--b {
  width: 38px;                  /* APEX SIZE B */
  height: 38px;
  left: calc(100% - 2px);       /* shifted left 2px from the right edge */
  top: 35%;
  margin-left: -19px;           /* = -½ × width */
  margin-top: -19px;
}

/* Two SEPARATE animations so the spin runs CONTINUOUSLY without pausing
   at peak (the old single-animation approach decelerated at the scale
   peak because ease-in-out slows velocity at keyframe boundaries).
   Scale loop (slow ease-in-out) handles the fade-in / fade-out;
   spin loop (fast linear) handles the constant rotation. They combine
   via separate `scale` and `rotate` properties (modern CSS). */
.brow-tile.is-spotlit .brow-tile-sparkle {
  opacity: 1;
  /* Two animations, SYNCED to the same 2.5s cycle so the spin's slow-fast-slow
     rhythm lines up with the scale loop's appear-hold-shrink rhythm.
     Scale uses LINEAR easing so the grow phase appears crisply without a slow
     ramp; spin uses EASE-IN-OUT so rotation accelerates into a fast middle
     and decelerates at the edges — the "ease into the spin, whoosh whoosh
     real quick, ease back out" feel. */
  animation:
    sparkleScale 2.5s linear infinite both,
    sparkleSpin 2.5s cubic-bezier(0.4, 0, 0.6, 1) infinite both;
}
/* Delays — A fires the instant you hover, B follows 0.2s later.
   Both values must match per animation since both animations share the timing. */
.brow-tile.is-spotlit .brow-tile-sparkle--a {
  animation-delay: 0s, 0s;
}
.brow-tile.is-spotlit .brow-tile-sparkle--b {
  animation-delay: 0.2s, 0.2s;
}

/* Scale loop — fast grow (0→8%), long hold (8%→85%), gentle shrink (85%→100%). */
@keyframes sparkleScale {
  0%   { scale: 0; }
  8%   { scale: 1; }
  85%  { scale: 1; }
  100% { scale: 0; }
}
/* Spin — 720° (2 revolutions) per cycle with ease-in-out. Rotation is slow
   at the iteration edges (when the sparkle is invisible or just appearing/
   disappearing), and fastest right in the middle when the sparkle is at
   full size and most visible. */
@keyframes sparkleSpin {
  to { rotate: 720deg; }
}

/* Samira sparkle nudges (V1.5): A nudged 4px right, B nudged 3px left into
   the container. Px values scaled up from V2's 3/2 to match V1.5's larger tile. */
.brow-tile--samira .brow-tile-sparkle--a {
  left: 7px;        /* shifted right +3px (was 4px) */
}
.brow-tile--samira .brow-tile-sparkle--b {
  left: calc(100% - 5px);   /* base shifted -2px, Samira had -3px → total -5px */
}

/* ====== QMARK GLITCH (Benchies row hover) ======
   When the visitor hovers a qmark tile in benchies-row-1, the inner SVG
   does a marker-glitch-style shake: tiny pixel jitter + RGB-split
   drop-shadow. Same vibe as the title-marker glitch on Faye/Samira pages,
   but without the baked-in 10deg rotation since the qmark sits upright. */
@media (hover: hover) and (pointer: fine) {
  .brow-tile--qmark:hover > svg.brow-qmark {
    animation: qmark-glitch 0.9s linear infinite;
  }
}
@keyframes qmark-glitch {
  0%   { transform: translate(0, 0);          filter: none; }
  15%  { transform: translate(-0.5px, 0);     filter: drop-shadow(-0.6px 0 rgba(0,229,255,0.55)) drop-shadow(0.6px 0 rgba(255,0,170,0.55)); }
  30%  { transform: translate(0.5px, 0);      filter: drop-shadow(0.6px 0 rgba(0,229,255,0.55)) drop-shadow(-0.6px 0 rgba(255,0,170,0.55)); }
  45%  { transform: translate(0, 0);          filter: none; }
  60%  { transform: translate(-0.5px, 0.5px); filter: drop-shadow(-1px 0 rgba(0,229,255,0.5)) drop-shadow(1px 0 rgba(255,0,170,0.5)); }
  78%  { transform: translate(0.5px, 0);      filter: drop-shadow(0.5px 0 rgba(0,229,255,0.55)) drop-shadow(-0.5px 0 rgba(255,0,170,0.55)); }
  100% { transform: translate(0, 0);          filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .brow-tile--qmark:hover > svg.brow-qmark { animation: none; }
}

/* ====== LOCK ICON + "Locked" TOOLTIP (rail nav, 'coming soon' items) ======
   Adds a 16x16 lock icon (::after) to the right of nav items not shippable
   yet (Carries, Job Board, Business Center) AND a small "Locked" tooltip
   pill (::before) that appears on hover — same monospace style as the
   brow-tile tooltips on Benchies. On link hover, the lock recolors to
   ink to match the link text's hover color. */
.nav-cat a[data-i18n="page.carries"],
.nav-cat a[data-i18n="page.jobBoard"],
.nav-cat a[data-i18n="page.business"] {
  position: relative;            /* anchor for the absolutely-positioned tooltip */
}
/* Lock icon */
.nav-cat a[data-i18n="page.carries"]::after,
.nav-cat a[data-i18n="page.jobBoard"]::after,
.nav-cat a[data-i18n="page.business"]::after {
  content: '';
  display: inline-block;
  vertical-align: -3px;          /* visually align with the text x-height */
  width: 16px;
  height: 16px;
  margin-left: 4px;              /* gap between line item text and lock */
  background-color: #9A9A9A;     /* same gray as inactive .nav-cat a links */
  opacity: 0.4;                  /* lighter still — softer than the lang-picker dim */
  -webkit-mask: url("../lock.svg") center / contain no-repeat;
          mask: url("../lock.svg") center / contain no-repeat;
  transition: background-color .15s ease, opacity .15s ease;
}
/* On hover, lock matches the link text's hover color (var(--ink)) + full opacity. */
.nav-cat a[data-i18n="page.carries"]:hover::after,
.nav-cat a[data-i18n="page.jobBoard"]:hover::after,
.nav-cat a[data-i18n="page.business"]:hover::after {
  background-color: var(--ink);
  opacity: 1;
}
/* "Locked" tooltip — same monospace pill as .brow-tile::after on Benchies.
   Lives as a JS-injected <span class="lock-tooltip"> child inside each locked
   nav link (so we don't clobber the / prefix that ::before paints on every
   nav link). Positioned to the right of the lock icon. Desktop hover only. */
.nav-cat a .lock-tooltip {
  position: absolute;
  left: calc(100% + 10px);       /* sit 10px to the right of the lock */
  top: 50%;
  transform: translateY(-50%) translateX(-4px);
  background: #0A0A0A;
  color: #fff;
  padding: 8px 14px;
  border-radius: 8px;
  font: 500 13px/1 ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  letter-spacing: 0.01em;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .18s ease, transform .18s ease;
  z-index: 200;
  box-shadow: 0 4px 16px rgba(10, 10, 10, 0.18);
}
@media (hover: hover) and (pointer: fine) {
  .nav-cat a:hover > .lock-tooltip {
    opacity: 1;
    transform: translateY(-50%) translateX(0);
  }
}

/* ====== JOB BOARD — job-list cards (Figma 553:1699 port) ======
   Each .job-item is a row of: text column (title + subtitle) | 54x54 icon box.
   A divider hr lives right below each item. Hover state surfaces an
   external-link arrow next to the title. */
.prose .job-list {
  display: flex;
  flex-direction: column;
  gap: 24px;
  margin: 48px 0 32px;          /* 48px gap above the listings (after intro paragraph) */
}
.prose .job-item {
  display: flex;
  flex-direction: column;
  gap: 16px;
  text-decoration: none;
  color: inherit;
  position: relative;          /* anchor for the floating preview tooltip */
  padding-bottom: 24px;
  border-bottom: 1px solid rgba(10, 10, 10, 0.08);
}
.prose .job-item-row {
  display: flex;
  gap: 24px;
  align-items: flex-start;
  width: 100%;
}
/* Hover-tooltip preview — floats absolutely below the job-item row, fades in
   on hover. 20% smaller than the prior always-visible version (max-width 444
   → 356), still 16:9. 1px #EFEDF0 stroke + 12px radius matches the rest of
   the site's image containers. Soft drop shadow for the floating feel. */
.prose .job-preview {
  position: absolute;
  top: calc(100% - 16px);      /* float just below the row, overlapping the gap */
  right: 0;
  z-index: 10;
  width: 100%;
  max-width: 356px;            /* 444 × 0.8 = 355.2, rounded to 356 */
  aspect-ratio: 16 / 9;        /* height ≈ 200px */
  border-radius: 12px;
  border: 1px solid #EFEDF0;
  overflow: hidden;
  background: #F9F7FA;
  box-shadow: 0 12px 32px rgba(10, 10, 10, 0.16);
  opacity: 0;
  pointer-events: none;
  transform: translateX(-16px);
  /* Slides in horizontally from the LEFT, settling at translateX(0).
     600ms ease-out-quart — slower than before but still graceful. */
  transition: opacity 600ms cubic-bezier(0.22, 1, 0.36, 1),
              transform 600ms cubic-bezier(0.22, 1, 0.36, 1);
}
.prose .job-preview img,
.prose .job-preview video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
@media (hover: hover) and (pointer: fine) {
  .prose .job-item:hover .job-preview {
    opacity: 1;
    transform: translateX(0);
  }
}
.prose .job-item:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}
.prose .job-item-text {
  flex: 1 0 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.prose .job-item-title {
  font-family: var(--sans);
  font-weight: 500;
  font-size: 20px;
  line-height: 1.216;
  color: #2D2D2D;
  margin: 0;
  display: flex;
  gap: 8px;
  align-items: center;
}
.job-arrow-svg {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  color: #2D2D2D;
  opacity: 0;
  transform: translate(-4px, 4px);
  transition: opacity .18s ease, transform .18s ease;
}
@media (hover: hover) and (pointer: fine) {
  .prose .job-item:hover .job-arrow-svg {
    opacity: 1;
    transform: translate(0, 0);
  }
}
.prose .job-item-subtitle {
  font-family: var(--sans);
  font-weight: 400;
  font-size: 14px;
  line-height: 1.737;
  color: #9A9A9A;
  margin: 0;
  display: flex;
  align-items: center;
  gap: 8px;                        /* Figma 8px between position-type / dot / desc */
  flex-wrap: nowrap;               /* single line — desc truncates with ellipsis */
  min-width: 0;                    /* allow shrinking inside .job-item-text */
}
/* Position-type label and the dot separator are intentionally hidden at
   every breakpoint — the subtitle reads as the role description only.
   Markup stays in place for i18n / future re-enable. */
.prose .job-position-type,
.prose .job-subtitle-dot {
  display: none;
}
.prose .job-subtitle-desc {
  flex: 1 1 0;                     /* shrinkable — paired with min-width:0 */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.prose .job-item-icon {
  flex-shrink: 0;
  width: 48px;                  /* Figma updated 54 → 48 */
  height: 48px;
  background: #ffffff;
  border: 1px solid #EFEDF0;
  border-radius: 11px;          /* Figma 10.667 — auto-scaled with container */
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
}
.job-icon-svg {
  width: 30.75px;               /* Figma 30.748 */
  height: 28.22px;              /* Figma 28.222 */
  display: block;
}

/* On hover, job title text glitches — same nav-glitch keyframes used by the
   active nav link and h1/h2 heads. display: inline-block lets the transform
   apply to the inline span. Scoped to .job-item:hover so it only fires when
   you actually hover the card (desktop only). */
@media (hover: hover) and (pointer: fine) {
  .prose .job-item .job-title-text {
    display: inline-block;
  }
  .prose .job-item:hover .job-title-text {
    animation: nav-glitch 0.9s linear infinite;
  }
}

/* Job Board pagination (Figma node 554:2021) — page-number indicators below
   the listings. Active page in title-dark (#2D2D2D), inactive in subtext
   gray (#9A9A9A) with hover restoring dark. 12px gap, Saans Medium 16px. */
.prose .job-pagination {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: flex-end;       /* align numbers to the right edge */
  font-family: var(--sans);
  font-weight: 500;
  font-size: 16px;
  line-height: 1.52;
  margin-top: 32px;
}
.prose .job-pagination-page {
  color: #9A9A9A;
  text-decoration: none;
  transition: color 0.15s ease;
}
.prose .job-pagination-page[aria-current="page"] {
  color: #2D2D2D;
}
@media (hover: hover) and (pointer: fine) {
  .prose .job-pagination-page:hover {
    color: #2D2D2D;
  }
}

/* Job Board filter section (Figma 558:2200) ==============================
   Outer container: holds [filter-bar + hr divider]. 40px breathing room
   above + below; 12px between the pill row and the divider line. */
.prose .job-filter-section {
  display: flex;
  flex-direction: column;
  gap: 12px;                       /* between pill row + divider */
  margin: 40px 0;                  /* 40px to intro above, 40px to listings below */
}
.prose .job-filter-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}
.prose .job-filter-divider {
  margin: 0;
  border: 0;
  border-top: 1px solid rgba(10, 10, 10, 0.08);
  width: 100%;
}
/* Listings sit flush against the section's 40px bottom margin — no extra
   top margin needed (was 40px before the section wrapper existed). */
.prose .job-list {
  margin: 0 0 32px;
}

.job-filter {
  position: relative;
  display: inline-flex;
}
.job-filter-pill {
  background: #F9F9F9;
  border: 1px solid #EFEDF0;
  border-radius: 24px;
  padding: 4px 12px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--sans);
  font-weight: 400;
  font-size: 14px;
  line-height: 1.737;
  color: #0A0A0A;
  cursor: pointer;
  transition: background .15s ease;
}
.job-filter-pill:hover {
  background: #F0F0F0;
}
.job-filter-chevron {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: #0A0A0A;
  transition: transform .25s cubic-bezier(0.22, 1, 0.36, 1);
}
.job-filter-pill[aria-expanded="true"] .job-filter-chevron {
  transform: rotate(180deg);
}

/* Smooth dropdown open/close — opacity + transform + delayed visibility,
   instead of the instant [hidden] attribute toggle. */
.job-filter-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 100;
  background: #ffffff;
  border: 1px solid #EFEDF0;
  border-radius: 12px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-width: 204px;
  box-shadow: 0 8px 24px rgba(10, 10, 10, 0.10);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px) scale(0.98);
  transform-origin: top left;
  transition:
    opacity .25s cubic-bezier(0.22, 1, 0.36, 1),
    transform .25s cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear .25s;
}
.job-filter-dropdown.is-open {
  opacity: 1;
  visibility: visible;
  transform: translateY(0) scale(1);
  transition:
    opacity .25s cubic-bezier(0.22, 1, 0.36, 1),
    transform .25s cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear 0s;
}
.job-filter-option {
  background: transparent;
  border: 0;
  padding: 6px 48px 6px 12px;
  text-align: left;
  font-family: var(--sans);
  font-weight: 400;
  font-size: 14px;                 /* matches pill text size */
  line-height: 1.737;
  color: #0A0A0A;
  cursor: pointer;
  transition: background .12s ease;
  white-space: nowrap;
}
.job-filter-option:hover,
.job-filter-option.is-selected {
  background: #F9F9F9;
}
.job-filter-count {
  font-family: var(--sans);
  font-weight: 400;
  font-size: 12px;
  line-height: 2.027;
  color: #0A0A0A;
  white-space: nowrap;
  margin-top: 4px;                 /* lowered 4px per design feedback */
}

/* Smooth filter swap — fade .job-list briefly during the transition so the
   show/hide of items doesn't feel like a hard cut. JS adds .is-filtering
   for ~120ms while it toggles [hidden] on the items underneath. */
.prose .job-list {
  transition: opacity .18s ease;
}
.prose .job-list.is-filtering {
  opacity: 0.35;
}

/* Trailing-border cleanup when filter hides the actual last-child. */
.prose .job-item.is-last-visible {
  border-bottom: 0;
  padding-bottom: 0;
}

/* Job Board empty state (Figma 558:2209) ================================
   Shown when the active filter has 0 matching jobs. Centered character
   illustration + swoosh + quoted line. Fade-up in/out animation; the
   text glitches with the same nav-glitch keyframes used elsewhere. */
.prose .job-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  padding: 32px 0 40px;
  text-align: center;
  opacity: 0;
  transform: translateY(18px);          /* shallower resting offset — less overlap */
}
.prose .job-empty-state[hidden] {
  display: none;
}
/* Entrance — 550ms, shallower 18px rise so it doesn't overshoot the layout
   bounds during empty → empty swaps. The keyframes are re-fired on every
   filter switch via JS so the entrance reads as fresh each time. `both`
   fill-mode locks the end state in place after the animation finishes. */
@keyframes job-empty-state-in {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: translateY(0); }
}
.prose .job-empty-state.is-visible {
  animation: job-empty-state-in 550ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
/* Exit — used ONLY for empty → empty transitions. Snappier (280ms) so the
   handoff to the next entrance doesn't overlap awkwardly. .is-fading-out
   comes AFTER .is-visible in the cascade so its animation wins when both
   classes are present. */
@keyframes job-empty-state-out {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(10px); }
}
.prose .job-empty-state.is-fading-out {
  animation: job-empty-state-out 280ms cubic-bezier(0.7, 0, 0.84, 0) both;
}
.prose .job-empty-text {
  font-family: var(--sans);
  font-weight: 500;
  font-size: 15.2px;
  line-height: 1.6;
  color: #0A0A0A;
  margin: 0;
  display: inline-block;       /* lets nav-glitch transform apply */
  animation: nav-glitch 0.9s linear infinite;
}
.prose .job-empty-illo {
  position: relative;
  display: inline-block;
  margin-top: 8px;
}
.prose .job-empty-character {
  display: block;
  width: 170px;
  height: auto;
}
.prose .job-empty-swoosh {
  position: absolute;
  right: -32px;
  top: 12px;
  width: 44px;
  height: 44px;
  transform: rotate(21deg);
  color: #0A0A0A;
}

/* Job-item / job-list hidden override =====================================
   .prose .job-item and .job-list both set display: flex, which beats the
   UA [hidden] { display: none } rule. Without this, the filter logic
   marks items hidden but they keep rendering — and the empty state never
   appears. Restore the expected behaviour. */
.prose .job-item[hidden],
.prose .job-list[hidden] {
  display: none !important;
}

/* Pill label must NEVER wrap, even mid-animation. The JS locks pill.width
   to startWidth and transitions to endWidth, but if endWidth is narrower
   than the label's natural width, the inner span would wrap to 2 lines for
   the duration of the transition. nowrap forces a single-line layout; the
   parent's overflow:hidden (set by JS during the animation) clips the
   excess, so you only see a clean horizontal squeeze. */
.job-filter-pill,
.job-filter-current {
  white-space: nowrap;
}

/* Job ladder-in animation =================================================
   Used when switching from the empty state to a populated category. Each
   visible .job-item gets an inline animation-delay (set by JS, bottom row
   first) and then runs this keyframe once: fade up from 18px below to its
   resting position. `both` fill-mode keeps the start state during the
   pre-animation delay and the end state after, so there's no flicker. */
@keyframes job-ladder-in {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: translateY(0); }
}
.prose .job-item.is-laddering {
  animation: job-ladder-in 700ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

/* Job preview — flip above row when JS detects there's not enough space
   below the hovered row to keep the tooltip clear of the viewport bottom.
   JS adds .preview-above on mouseenter based on geometry; CSS overrides
   the default below-the-row positioning. Horizontal slide-in stays the
   same in both directions for visual consistency. */
.prose .job-item.preview-above .job-preview {
  top: auto;
  bottom: calc(100% - 16px);
}

/* Job-item icon GIF (per-category animated icons) =========================
   Replaces the .job-icon-svg in jobs that carry an animated GIF icon for
   their category. Sized to roughly match the original SVG footprint inside
   the 48px tile. Renders pixel-art-style icons crisply on retina screens. */
.job-icon-img {
  width: 36px;
  height: 36px;
  display: block;
  object-fit: contain;
  image-rendering: -webkit-optimize-contrast;
  transform: translateX(2px);    /* nudge 2px right inside the 48px tile */
}

/* Illo-row hover tilt =====================================================
   On Faye / Samira bio pages, each .illo-row tile (sketch face) lifts on
   hover: scales 1.15x, rotates 10deg in-plane, pitches back 10deg in 3D.
   Pivot point is the top-right corner. The parent grid carries perspective
   so the rotateX reads as depth instead of a flat squash. */
.illo-row {
  perspective: 1200px;
}
.illo-row img.illo {
  transform-origin: top right;
  transition: transform 500ms cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 500ms cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
  position: relative;
  z-index: 1;
}
@media (hover: hover) and (pointer: fine) {
  .illo-row img.illo:hover {
    transform: scale(1.3) rotate(7deg) rotateX(-7deg);
    z-index: 2;
    box-shadow: 0 12px 28px rgba(10, 10, 10, 0.16);
  }
}
/* 2x2 grid breakpoint (≤720px) — tile hover simplifies:
   - Scale-up reduced 40% (1.3 → 1.18; scale-up of 0.3 × 0.6 = 0.18)
   - Left-column tiles (odd children in 2-col grid) tilt the OPPOSITE way:
     mirror the rotation + flip the pivot from top-right to top-left
   - Box-shadow drops to match the gentler lift
   - Sparkle animation (.illo-tile:hover .brow-tile-sparkle) is unaffected */
@media (max-width: 720px) and (hover: hover) and (pointer: fine) {
  .illo-row img.illo:hover {
    transform: scale(1.18) rotate(7deg) rotateX(-7deg);
    box-shadow: 0 8px 18px rgba(10, 10, 10, 0.12);
  }
  /* Left column = .illo-tile:nth-child(odd) once the grid is 2-col.
     Flip the pivot to top-left + invert the 2D rotation so the tile
     mirrors the right-column tilt. rotateX stays — back-pitch reads
     the same regardless of L/R. */
  .illo-row .illo-tile:nth-child(odd) img.illo {
    transform-origin: top left;
  }
  .illo-row .illo-tile:nth-child(odd) img.illo:hover {
    transform: scale(1.18) rotate(-7deg) rotateX(-7deg);
  }
}
/* Sparkle nudge for left-column tiles in the 2x2 grid — shift +30px right
   so the sparkle reads correctly on the mirrored tilt. */
@media (max-width: 720px) {
  .illo-row .illo-tile:nth-child(odd) .brow-tile-sparkle--b {
    left: calc(100% - 6px + 30px);
  }
}
@media (prefers-reduced-motion: reduce) {
  .illo-row img.illo,
  .illo-row img.illo:hover {
    transform: none;
    transition: none;
  }
}

/* Illo-tile sparkle (Faye / Samira bio rows) =============================
   Wraps each .illo-row img.illo so we can position the two .brow-tile-sparkle
   SVGs at the tile's left + right edges and animate them on hover. Reuses
   the existing sparkleScale + sparkleSpin keyframes verbatim — same 2.5s
   cycle, same easing curves, same 0.2s stagger between A and B. */
.illo-row {
  /* Override the existing grid template — the wrapper span is now the grid
     child; it must still fill its column. */
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
}
.illo-tile {
  position: relative;
  display: block;
  /* perspective inherited from .illo-row applies through to the img tilt. */
}
/* Sparkles spill outside the tile bounds by design — keep overflow visible. */
.illo-tile {
  overflow: visible;
}
@media (hover: hover) and (pointer: fine) {
  .illo-tile:hover .brow-tile-sparkle {
    opacity: 1;
    animation:
      sparkleScale 2.5s linear infinite both,
      sparkleSpin 2.5s cubic-bezier(0.4, 0, 0.6, 1) infinite both;
  }
  .illo-tile:hover .brow-tile-sparkle--a { animation-delay: 0s, 0s; }
  .illo-tile:hover .brow-tile-sparkle--b { animation-delay: 0s, 0s; }
}

/* Illo-tile sparkle overrides ============================================
   On the bio-page sketch tiles (Faye/Samira), drop the left sparkle and
   keep only the right one — bumped 25% larger and nudged 3px inward from
   the right edge. Benchies row keeps its two-sparkle layout (scoped to
   .illo-tile only). */
.illo-tile .brow-tile-sparkle--a {
  display: none;
}
.illo-tile .brow-tile-sparkle--b {
  width: 45px;                  /* 36 × 1.25 = 45 */
  height: 45px;
  left: calc(100% - 6px);       /* 6px in from the right edge */
  margin-left: -22.5px;         /* = -½ × width */
  margin-top: -22.5px;
}

/* Homepage Benchies Release promo (Figma 702:278) ========================
   Two-column hero: illustration on the left, headline + body + signup on
   the right. Sits inside the existing .hero-art so the entrance animation
   (heroIn, 1.4s ease-out 2.6s delay) still plays per the rail-stagger
   timing. */
.hero-art {
  max-width: 998px;             /* 415 + 76 + 507 — illustration 15%% bigger, content fits exactly */
}
.hero-grid {
  display: flex;
  align-items: center;
  gap: 76px;
}
.hero-illo {
  flex-shrink: 0;                 /* keep illustration at full size — never shrinks */
  position: relative;             /* positioning context for .bubble-hover */
  width: 415px;                   /* 361 × 1.15 = 415 — 15%% bigger */
}
.hero-illo img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 12px;            /* moved from parent so .has-tooltip ::after isn't clipped */
}
.hero-promo {
  width: 507px;
  flex: 0 1 auto;                 /* can shrink when parent runs short */
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.promo-title {
  font-family: var(--sans);
  font-weight: 500;
  font-size: 36px;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: #0A0A0A;
  margin: 0;
  /* Same glitch keyframes used elsewhere for the section heads. */
  animation: nav-glitch 0.9s linear infinite;
}
.promo-body {
  font-family: var(--sans);
  font-weight: 400;
  font-size: 14.4px;
  line-height: 1.65;
  color: #5B5B5B;
  margin: 0;
}
.promo-form {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
}
.promo-input {
  flex: 1 1 0;
  min-width: 0;
  display: block;
}
.promo-input input {
  width: 100%;
  font-family: var(--sans);
  font-size: 15.2px;
  line-height: 1.6;
  color: #2D2D2D;
  background: #FCFCFC;
  border: 1px solid #EFEDF0;
  border-radius: 8px;
  padding: 12px 24px;
  outline: none;
  transition: border-color .15s ease;
}
.promo-input input::placeholder {
  color: #2D2D2D;
  opacity: 1;
}
.promo-input input:focus {
  border-color: #0A0A0A;
}
.promo-submit {
  flex-shrink: 0;
  width: 48px;
  height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: #2D2D2D;
  border: 1px solid #EFEDF0;
  border-radius: 11px;
  cursor: pointer;
  color: #FFFFFF;
  padding: 0;
  transition: background .15s ease;
}
.promo-submit:hover {
  background: #0A0A0A;
}
.promo-send-svg {
  width: 22px;
  height: 22px;
  display: block;
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Mobile / narrow viewport — stack columns, image shrinks to row width. */
@media (max-width: 1500px) {
  /* Stack — illustration stays 361px and never shrinks, so once the promo
     column would compress below ~400px we flip to a single column.
     Everything middle-aligned in stacked layout: illo + promo centered,
     headline + body text centered, email row centered. */
  .hero-grid {
    flex-direction: column;
    gap: 32px;
    align-items: center;
  }
  .hero-illo {
    width: 100%;
    max-width: 415px;
  }
  .hero-promo {
    width: 100%;
    max-width: 507px;
    align-items: center;
  }
  .promo-title,
  .promo-body {
    text-align: center;
    width: 100%;
  }
  .promo-form {
    width: 100%;
    justify-content: center;
  }
}

/* Generic .has-tooltip — hover tooltip pill ============================
   Adopts the same visual spec used by .benchies-row-1 .brow-tile (black
   pill, white mono caption, fades up below the element on hover). Apply
   .has-tooltip + data-tooltip='...' to any element. Desktop only — touch
   devices don't get sticky hover states. */
.has-tooltip {
  position: relative;
}
.has-tooltip::after {
  content: attr(data-tooltip);
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  transform: translate(-50%, -4px);
  background: #0A0A0A;
  color: #fff;
  padding: 8px 14px;
  border-radius: 8px;
  font: 500 13px/1 ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  letter-spacing: 0.01em;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .18s ease, transform .18s ease;
  z-index: 20;
  box-shadow: 0 4px 16px rgba(10, 10, 10, 0.18);
}
@media (hover: hover) and (pointer: fine) {
  .has-tooltip:hover::after,
  .has-tooltip:focus-within::after {
    opacity: 1;
    transform: translate(-50%, 0);
  }
}

/* Hero-art centering — keep equal whitespace on left + right of the
   illustration/promo block within .stage at every viewport size. The
   parent .stage already uses flex + justify-content:center, but the
   explicit margin-inline:auto guarantees centering even if a later layout
   tweak changes .stage's flex behavior. */
.hero-art {
  margin-inline: auto;
}

/* Hero-illo link reset — the illustration wrapper is now an <a>; strip
   default link styling so the visual is unchanged. */
a.hero-illo {
  display: block;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}
a.hero-illo:focus-visible {
  outline: 2px solid var(--hover, #0A0A0A);
  outline-offset: 4px;
  border-radius: 12px;
}

/* Hero illustration hover bubble + dizzy SVG ============================
   Layered on top-left of the .hero-illo as an absolutely-positioned wrapper.
   Wrapper visibility controlled by .hero-illo:hover; entrance animations
   on the children play once per hover. On hover end, the wrapper opacity
   transitions to 0 and the children fade out together as a single unit
   (their animation `forwards` fill keeps them at the final transform, so
   no snap-to-initial occurs mid-fade-out). */
.bubble-hover {
  position: absolute;
  top: -18px;                /* moved down another 24px → -42 + 24 = -18 */
  left: -10px;               /* moved right another 24px → -34 + 24 = -10 */
  width: 95px;               /* 112 × 0.85 ≈ 95 — shrunk another 15%% */
  height: 85px;              /* 100 × 0.85 = 85 */
  pointer-events: none;
  opacity: 0;
  transition: opacity 280ms cubic-bezier(0.22, 1, 0.36, 1);
  z-index: 3;
}
.bubble-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: auto;
  display: block;
  /* Base = end state of the entrance — when not hovered, wrapper opacity
     is 0 so this is invisible anyway. On hover-leave, animation property
     is removed and child stays at this state while the wrapper fades.
     scale(-1, 1) flips the SVG horizontally — bubble faces the other way. */
  opacity: 1;
  transform: scale(1);                          /* natural orientation — no flip, no rotation */
  transform-origin: center;
}

@media (hover: hover) and (pointer: fine) {
  .hero-illo:hover .bubble-hover {
    opacity: 1;
  }
  /* Bubble — fades in + scales up + rotates into the -10deg tilt. The
     ease-out-back curve gives the slight overshoot that makes the bubble
     "land" with a touch of physicality. 400ms total. */
  .hero-illo:hover .bubble-svg {
    /* `both` fill = `backwards` (apply `from` state during pre-delay) + `forwards`
       (hold `to` state after). The `backwards` half is what kills the
       'double animation' — the child stays at `from` (invisible) until the
       animation actually runs. */
    animation: bubble-hover-in 400ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }
}
@keyframes bubble-hover-in {
  from { opacity: 0; transform: scale(0.6); }
  to   { opacity: 1; transform: scale(1); }
}

/* Scroll-driven row-reaction bubble (e.g., Samira's sketch-face .illo-row gets
   a heart-bubble icon that pops in when the row enters the active range and
   fades out as it scrolls past). Same visual size as the hero-illo text bubble
   (95×85) and the same ease-back entrance feel. The .is-visible class is
   toggled by the scrollRowBubble IIFE in main.js. */
.row-bubble {
  position: absolute;
  /* Sits to the FAR LEFT of the first tile with a 12px gap between the
     bubble's right edge and the row's left edge. Vertically centered then
     biased 32px UPWARD so it reads as a reaction floating beside the
     upper-left portion of the sketch row (not centered on the midline). */
  top: 50%;
  left: calc(-95px - 12px);    /* bubble width (95) + 12px gap */
  width: 95px;                 /* same size as homepage text bubble */
  height: auto;
  display: block;
  opacity: 0;
  /* translate(0, calc(-50% - 36px)) centers it on the row's midline then
     biases it 32px upward (was 8px). scale(0.55) is the resting starting
     state for the ease-back overshoot when .is-visible toggles on. */
  transform: translate(0, calc(-50% - 36px)) scale(0.55);
  transform-origin: right center;
  pointer-events: none;
  z-index: 6;
  transition:
    opacity 320ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 420ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.row-bubble.is-visible {
  opacity: 1;
  transform: translate(0, calc(-50% - 36px)) scale(1);
}
/* Any element that hosts a .row-bubble needs to be its positioning anchor
   (the .illo-row on bio pages, the "She doesn't know" h2 on the story page,
   any future host element added to the scrollRowBubble config). */
.has-row-bubble {
  position: relative;
}
@media (max-width: 720px) {
  .row-bubble {
    width: 72px;
    left: calc(-72px - 12px);
  }
}
@media (max-width: 480px) {
  /* Tightest tier — drop the bubble; the row hugs the viewport edge and
     there's no room for a left-floated reaction without going off-screen. */
  .row-bubble {
    display: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .row-bubble { transition: opacity 120ms ease; transform: none; }
  .row-bubble.is-visible { transform: none; }
}

/* Story-page cat icon — sits with 16px gap (vs the 12px the heart bubbles
   use) and stays centered on the h2's vertical midline (no upward bias).
   Per-page override applied via body class. */
.body--story .row-bubble {
  left: calc(-95px - 16px);
  transform: translate(0, -50%) scale(0.55);
}
.body--story .row-bubble.is-visible {
  transform: translate(0, -50%) scale(1);
}
/* Exclaim variant — −30% size (95px × 0.7 ≈ 67px). Tall narrow aspect
   (120:194 native) means the exclaim was rendering ~154px tall at 95px wide,
   which dominated the headline; the smaller width brings it in line with
   the cat icon visually. left offset recomputed so the 16px gap holds. */
.body--story .row-bubble--exclaim {
  width: 67px;
  left: calc(-67px - 16px);
}
@media (max-width: 720px) {
  .body--story .row-bubble {
    left: calc(-72px - 16px);
  }
  .body--story .row-bubble--exclaim {
    width: 50px;                       /* 72px × 0.7 ≈ 50 */
    left: calc(-50px - 16px);
  }
}
@media (prefers-reduced-motion: reduce) {
  .hero-illo:hover .bubble-svg,
}

/* Homepage — symmetric padding so the hero-art block sits with equal
   whitespace between the rail's right edge and the viewport's right edge.
   Default .page padding is 0 300px 0 64px (asymmetric — extra 300px on the
   right) which pushes the centered .hero-art left of the visual center.
   Override on the home page only; other pages keep the asymmetric padding
   their layouts were designed for. */
.body--home .page {
  /* Symmetric horizontal layout — equal whitespace on both sides of the
     visible content (rail's left edge sits 64px from viewport left; hero's
     right edge sits 64px from viewport right; rail-to-hero gap fixed at
     64px so the page padding and the column gap match exactly). */
  padding: 0 64px;
  gap: 64px;
}

/* Benchies-page inline hero copy =========================================
   Mirrors the homepage hero block but sized to fit inside the .prose 640px
   column. Always stacks vertically (illustration on top, promo below) and
   shrinks the illustration so it never feels oversized within text flow.
   Reuses the original .hero-grid/.hero-illo/.hero-promo classes so the
   bubble hover + tooltips + form styling all come along automatically. */
.hero-art--inline {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 24px;
  margin: 24px 0 0;           /* email row sits 24px below the last paragraph */
  max-width: 100%;
  /* Override the parent .hero-art entrance animation — the inline copy
     on Benchies should be present immediately, not blurred or fading. */
  opacity: 1;
  transform: none;
  filter: none;
  animation: none;
}
.hero-art--inline .hero-illo {
  width: 320px;
  max-width: 100%;
  align-self: flex-start;     /* sits left-aligned with the rest of the text */
  flex-shrink: 1;
  cursor: default;            /* it's a <div>, not a <a>, since self-link is a no-op */
}
.hero-art--inline .promo-form {
  width: 100%;
  /* default justify-content (flex-start) leaves the row at the column's
     left edge; the input field flex-grows to fill the available width */
}

/* Benchies standalone hero-illo (between sections) ======================
   The .hero-illo on the Benchies page sits as a direct child of .prose,
   between the 'That's what a Benchie is for.' and 'Benchies Release. 2027'
   sections. Center it horizontally and shrink 30% from the homepage default
   (415 × 0.7 ≈ 290). Scoped to body--benchies so the homepage illo is
   unaffected. */
.body--benchies .prose > .hero-illo {
  width: 290px;
  max-width: 100%;
  margin: 48px auto 0;        /* 48px above for more separation from the paragraph */
}

/* Benchies-page bubble-hover overrides ====================================
   The standalone illustration on Benchies is smaller (290px vs homepage 415)
   AND repositioned, so the bubble needs its own offsets + size.
   - Moved left 32px: -10 → -42
   - Moved down 4px: -18 → -14
   - Shrunk 18%: 95×85 → 78×70 */
.body--benchies .bubble-hover {
  top: -14px;
  left: -24px;
  width: 78px;
  height: 70px;
}

/* Bottom breathing room — 260px under every content section ==============
   Stops the content from feeling like it ends with a hard cut at the bottom
   of the viewport. Scoped to .prose so it lands on every text-based page
   (Faye, Samira, Benchies, Manifesto, Story, Carries, Job Board, Business
   Center). Homepage uses .hero-art which is vertically centered in .stage,
   so it doesn't need this. */
.prose {
  padding-bottom: 260px;
}

/* Cursor-following tooltip ================================================
   Marker: .cursor-tooltip on a hover target. JS creates a single global
   pill element (#cursor-tooltip) pinned with position:fixed, then updates
   its top/left on every mousemove inside the target — 12px right + 12px
   below the cursor (slight down offset keeps the pill out of the cursor
   sprite). Visual matches .has-tooltip::after exactly (black pill, white
   SF Mono caption, soft drop shadow). */
#cursor-tooltip {
  position: fixed;
  top: 0;
  left: 0;
  background: #0A0A0A;
  color: #fff;
  padding: 8px 14px;
  border-radius: 8px;
  font: 500 13px/1 ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  letter-spacing: 0.01em;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  z-index: 9999;
  box-shadow: 0 4px 16px rgba(10, 10, 10, 0.18);
  transition: opacity .15s ease;
  will-change: transform, opacity;
}
#cursor-tooltip.is-visible {
  opacity: 1;
}
.cursor-tooltip {
  /* The trigger element itself just needs a position context — the actual
     tooltip lives in a fixed-position element pinned to <body>. */
}

/* Illo handler label (bio pages — vertical illustration in .two-col) ====
   The .illo-frame wraps the existing img.illo so a positioned span can
   sit in its bottom-left corner. Color matches unselected rail nav items
   (#9A9A9A), 10pt sans uppercase, 16px padding from the bottom + left
   edges (and a matching right padding so longer artist names wrap cleanly
   rather than overflow the frame). */
.two-col .illo-frame {
  position: relative;
  display: block;
}
.two-col .illo-frame img.illo {
  display: block;
  margin: 0;
}
.illo-handler {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 16px;
  font-family: var(--sans);
  font-size: 6pt;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: #9A9A9A;
  /* text-transform removed — sentence case now comes from the markup */
  pointer-events: none;
  /* The .illo image has overflow:hidden via its border-radius + object-fit;
     the label sits ABOVE it via z-index since both share the same stacking
     context inside .illo-frame. */
  z-index: 1;
}

/* Mobile nav (≤ 900px) =====================================================
   Portfolio-style menu: 3-bar hamburger morphs into X on open, full-viewport
   blur backdrop fades in via opacity+visibility, menu items cascade up with
   staggered nth-child delays. Items remain bottom-left positioned at 24px
   padding (per the TeakTime layout spec). */

/* Hide on desktop. */
.nav-hamburger,
.nav-backdrop {
  display: none;
}

@media (max-width: 1024px) {
  /* --- Page shell becomes a single column --- */
  .page,
  .body--home .page {            /* override the desktop .body--home padding: 0 64px so home tracks the same single-col mobile shell as every other page */
    grid-template-columns: 1fr;
    padding: 0;
    gap: 0;
    min-height: 100vh;
  }

  /* --- Rail becomes a fixed top bar (centered logo only) --- */
  .rail {
    grid-row: auto;
    align-self: auto;
    width: 100%;
  }
  .rail-inner {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    width: 100%;
    height: auto;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    padding: 24px 24px;          /* symmetric vertical padding — logo + hamburger sit dead-center in the bar */
    /* IMPORTANT: keep .rail-inner free of backdrop-filter / non-none transform /
       will-change so it does NOT become a containing block for the fixed nav
       inside it. The frosted-glass effect lives on the ::before pseudo, which
       can carry filter without affecting the parent's containing-block status. */
    background: transparent;
    z-index: 100;
    transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  .rail-inner::before {
    content: '';
    position: absolute;
    inset: 0;
    background: rgba(255, 255, 255, 0.65);
    -webkit-backdrop-filter: blur(20px) saturate(160%);
            backdrop-filter: blur(20px) saturate(160%);
    z-index: -1;            /* sits BEHIND the logo + hamburger */
    pointer-events: none;
  }
  .rail-inner.rail-hidden {
    transform: translateY(-100%);
  }
  /* When the nav is open: drop the rail's transform so it stays in the
     containing-block-free state and the fixed nav inside positions
     relative to the viewport. Also force the pseudo's blur off so the
     fullscreen menu reads cleanly over a solid backdrop. */
  body.nav-open .rail-inner {
    transform: none !important;   /* override .rail-hidden if it was scrolled away */
  }
  body.nav-open .rail-inner::before {
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
    background: rgba(255, 255, 255, 0.92);
  }
  .rail-inner .logo {
    margin-bottom: 0;
    width: 140px;
    aspect-ratio: auto;          /* drop the desktop 555/141 constraint — let the SVG size itself naturally from width */
    height: auto;
    overflow: visible;
  }
  .rail-inner .logo img {
    width: 100%;
    height: auto;                /* SVG draws at its natural aspect — no descender clipping */
    object-fit: initial;
    animation: none;
    transform: none;
  }
  .rail nav {
    display: none;       /* hidden until .nav-open */
  }

  /* --- Hamburger (3 bars → X morph), 18×18, vertically centered with the logo ---
     Rail padding is 24px top + 24px bottom; the 140px-wide logo renders ~36px
     tall (SVG aspect 555:141), so the logo's vertical center sits at 24 + 18 = 42px
     from viewport top. Half of 18px hamburger is 9, so top: 33px puts the
     hamburger's center on the same 42px line. */
  .nav-hamburger {
    display: flex;
    position: fixed;
    top: 33px;
    right: 24px;
    width: 18px;
    height: 18px;
    padding: 3px 0;              /* keeps the 3 bars centered inside the 18×18 hit box */
    background: transparent;
    border: 0;
    cursor: pointer;
    flex-direction: column;
    justify-content: space-between;
    align-items: stretch;
    box-sizing: border-box;
    z-index: 200;
    -webkit-tap-highlight-color: transparent;
  }
  /* Expanded hit area so touch targets don't feel pixel-perfect */
  .nav-hamburger::before {
    content: '';
    position: absolute;
    inset: -16px;                /* slightly more padding now that the visible target shrank */
  }
  .nav-hamburger .bar {
    display: block;
    width: 100%;
    height: 2px;
    background: #0A0A0A;
    border-radius: 1px;
    transition: transform 280ms ease, opacity 200ms ease;
    transform-origin: center;
  }
  body.nav-open .nav-hamburger .bar:nth-child(1) {
    transform: translateY(5px) rotate(45deg);     /* slides to vertical center of 18×18 (12px usable, ~5px to center) */
  }
  body.nav-open .nav-hamburger .bar:nth-child(2) {
    opacity: 0;
  }
  body.nav-open .nav-hamburger .bar:nth-child(3) {
    transform: translateY(-5px) rotate(-45deg);
  }

  /* --- Backdrop (blurs main content) --- */
  .nav-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(255, 255, 255, 0.55);
    backdrop-filter: blur(24px);
    -webkit-backdrop-filter: blur(24px);
    opacity: 0;
    visibility: hidden;    /* iOS Safari: makes the overlay truly uninteractive */
    pointer-events: none;
    z-index: 90;
    /* Quick fade so the blur "sets" almost immediately — the nav-cat cascade
       below fires at 0ms delay, so the menu reads as rolling in right after. */
    transition: opacity 120ms ease, visibility 0ms linear 120ms;
  }
  body.nav-open .nav-backdrop {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 120ms ease, visibility 0ms linear 0ms;
  }

  /* Push main content below the top bar */
  .stage,
  .stage--page {
    padding-top: 88px;
  }

  /* --- Open menu: centered in viewport, items cascade up --- */
  body.nav-open .rail nav {
    display: flex;
    flex-direction: column;
    gap: 16px;
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    z-index: 95;
    max-width: calc(100vw - 48px);
  }
  body.nav-open .rail nav .nav-cat {
    margin: 0;
    text-align: center;
  }
  body.nav-open .rail nav .nav-cat h2 {
    font-size: 0.9rem;
  }
  body.nav-open .rail nav .nav-cat ul {
    padding-left: 0;
    list-style: none;
    text-align: center;
  }
  body.nav-open .rail nav .nav-cat li {
    line-height: 1.8;
    text-align: center;
  }
  body.nav-open .rail nav a {
    display: inline-block;
  }

  /* Cascade animation pattern (lifted from portfolio site).
     The first item fires at 0ms delay so the menu starts animating the
     instant the hamburger is tapped — the per-item duration (220ms) and
     the 40ms stagger between items are the original "ladder" timing,
     left alone. */
  .rail nav .nav-cat {
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 120ms ease, transform 220ms ease;
  }
  body.nav-open .rail nav .nav-cat {
    opacity: 1;
    transform: translateY(0);
    animation: nav-cascade 220ms ease backwards;
  }
  body.nav-open .rail nav .nav-cat:nth-child(1) { animation-delay: 0ms; }
  body.nav-open .rail nav .nav-cat:nth-child(2) { animation-delay: 40ms; }
  body.nav-open .rail nav .nav-cat:nth-child(3) { animation-delay: 80ms; }
  body.nav-open .rail nav .nav-cat:nth-child(4) { animation-delay: 120ms; }
  body.nav-open .rail nav .nav-cat:nth-child(5) { animation-delay: 160ms; }

  @keyframes nav-cascade {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .rail nav .nav-cat {
      transition: opacity 120ms ease;
      transform: none;
    }
    body.nav-open .rail nav .nav-cat {
      animation: none;
      transition-delay: 0ms;
    }
  }

  /* Lock body scroll when menu is open */
  body.nav-open {
    overflow: hidden;
  }

  /* Inline hero on Benchies wraps a bit tighter on small screens too */
  .hero-art--inline .hero-illo {
    width: 100%;
    max-width: 320px;
  }
}

/* Mobile responsiveness sweep (≤ 900px) ==================================
   Clean-sweep tightening across the site. Each rule below addresses a
   pattern that worked at desktop widths but read cramped or overflowed on
   phone-sized viewports. Lives as an additive media block — desktop CSS
   above is untouched. */
/* Component breakpoints — fire EARLIER than the full layout flip (≤1024px)
   so individual grids respond at the widths where they actually need to. */

/* Bio-page sketch-tile row: 4 columns → 2×2 grid at ≤1260px */
@media (max-width: 1260px) {
  .illo-row {
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
  }
}

/* Two-col text+illo blocks: stack vertically at ≤1200px (was 1024) */
@media (max-width: 1200px) {
  .two-col {
    grid-template-columns: 1fr;
    gap: 24px;
  }
}

/* Benchies character row: 4 flex items → 2×2 grid at ≤1024px (full mobile sweep
   threshold — the row needs the rail-collapsed layout to have room for tiles). */
@media (max-width: 1024px) {
  .benchies-row-1 {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
    justify-content: stretch;
  }
  .benchies-row-1 .brow-item {
    flex: initial;
    width: 100%;
  }
}

/* Benchies row collapses to a single column at ≤700px. Each character
   illustration is shrunk 20% and centered in the column; row gap is
   doubled (12 → 24) so the stacked illustrations breathe. */
@media (max-width: 700px) {
  .benchies-row-1 {
    grid-template-columns: 1fr;
    gap: 24px;                /* doubled from 12px */
  }
  .benchies-row-1 .brow-item {
    width: 80%;               /* −20% from full column width */
    justify-self: center;     /* keep centered on the page */
  }
}

@media (max-width: 1024px) {

  /* 4. Page title fluid scaling — tightened ceiling so h1 shrinks
        more in proportion with the illustration + quote below it. */
  .prose h1 {
    font-size: clamp(1.75rem, 6.5vw, 2.25rem);   /* was 2rem / 8vw / 3rem */
    line-height: 1.1;
  }
  .prose h2 {
    font-size: 1rem;
  }

  /* 5. Content stage padding — side breathing room + top buffer for the
        mobile top bar (logo + hamburger). Default 24px each side; bio /
        manifesto / story pages already use padding-top: 112px on desktop,
        so use a smaller fixed value tuned for the top bar.
        Also reset justify-content from the desktop's flex-end (which
        hugs prose against the page's 300px right padding on wide screens)
        to flex-start so the prose sits naturally at the left of the
        single-column shell, matching the rest of the mobile layout. */
  .stage--page {
    padding: 96px 24px 0;
    justify-content: center;
  }
  .body--faye .stage--page,
  .body--samira .stage--page,
  .body--manifesto .stage--page,
  .body--story .stage--page {
    padding-top: 96px;
  }

  /* 6. Hero video on bio pages: taller aspect so the character reads
        clearly instead of being a horizontal sliver */
  .illo-video-clip {
    aspect-ratio: 16 / 9;
  }

  /* 7. Callout (Story page quote block) — tighten the padding so it
        doesn't dominate a small viewport */
  .callout {
    padding: 20px;
    margin: 32px 0;
  }
  .callout::after {
    width: 24px;
    height: 24px;
    top: 16px;
    right: 18px;
  }

  /* 8. Job board: tighter row padding; filter always left-aligned so the
        pill anchors to the start of the row even when there's space for
        the count beside it. The bar wraps the count to its own line below
        the pill once the row is too narrow. */
  .prose .job-item {
    padding-bottom: 16px;
  }
  .prose .job-item-row {
    gap: 16px;
    align-items: flex-start;          /* icon hugs the top — extra subtitle lines push down only the text column */
  }
  /* Filter bar stays horizontal: pill on the left, "X Available Commissions"
     count anchored to the right — same x-axis as desktop, no wrap-to-next-line. */
  .prose .job-filter-bar {
    justify-content: space-between;
    flex-wrap: nowrap;
    gap: 12px;
  }
  .prose .job-filter {
    flex-shrink: 0;
  }
  .prose .job-filter-count {
    margin-top: 0;
    text-align: right;
    flex-shrink: 1;
    min-width: 0;
  }
  .prose .job-filter-divider {
    width: 100%;                       /* always visible across the column */
  }
  .prose .job-item-title {
    font-size: 1rem;
  }
  /* Subtitle — allow the desc text to wrap to 2 lines instead of being
     truncated to 1 with an ellipsis. Position-type + dot align with the
     FIRST line of desc (baseline), so the dot doesn't float orphaned
     above multi-line desc. Icon column (.job-item-icon) stays put at the
     top via align-items: flex-start on the row. */
  .prose .job-item-subtitle {
    align-items: baseline;             /* dot/position-type sit on desc's first-line baseline */
    flex-wrap: nowrap;                 /* keep pos-type + dot + desc on the same "row" — desc grows downward via -webkit-box */
  }
  .prose .job-subtitle-desc {
    white-space: normal;               /* allow line wrapping */
    overflow: hidden;
    text-overflow: clip;
    display: -webkit-box;
    -webkit-line-clamp: 2;             /* cap at 2 lines */
            line-clamp: 2;
    -webkit-box-orient: vertical;
    line-height: 1.45;
  }
  /* Hide the floating job preview tooltip on mobile (no hover) */
  .prose .job-preview {
    display: none;
  }

  /* 9. illo-handler credit ('Handled by Sepo') — bump from 6pt to 8pt
        so the line reads on a phone screen */
  .illo-handler {
    font-size: 8pt;
    left: 12px;
    right: 12px;
    bottom: 12px;
  }

  /* 10. Lang picker — keep it tucked but not on top of menu content */
  .foot {
    bottom: 16px;
    left: 16px;
  }

  /* 11. Stage homepage centering — when stacked, hero-art already responds
         to the .hero-art--inline / hero-grid stack-at-1500 breakpoint, but
         on a phone we also want side padding on the .hero-art container */
  .hero-art {
    padding-left: 24px;
    padding-right: 24px;
    max-width: 100%;
  }

  /* 12. Job board page hero — same edge padding as other content */
  .body--job-board .stage--page {
    padding-left: 24px;
    padding-right: 24px;
  }

  /* 13. .prose has padding-bottom: 260px (page-end breathing room set
         site-wide). At mobile that's overkill; halve it. */
  .prose {
    padding-bottom: 120px;
  }
}

/* Mobile sweep — round 2 ==================================================
   Refinements layered on top of the first mobile pass. Adds:
   - text/input safety (no iOS auto-zoom, no horizontal overflow)
   - section rhythm tightening at mobile (48px → 32px)
   - touch-target sizing for taps
   - a small-phone tier (≤ 480px) for the tightest viewports */

@media (max-width: 1024px) {

  /* ---- A. Text safety ------------------------------------------------- */
  /* Long words / URLs shouldn't blow out the column. */
  .prose,
  .prose p,
  .prose h1,
  .prose h2,
  .prose li {
    overflow-wrap: break-word;
    word-wrap: break-word;
  }
  /* Inline links in prose: keep the underline but allow breaking on long URLs */
  .prose a.inline,
  .prose a[href*="://"] {
    overflow-wrap: anywhere;
  }

  /* ---- B. Form inputs ------------------------------------------------- */
  /* iOS Safari auto-zooms when tapping <input> with font-size < 16px.
     Bump every form input + textarea to at least 16px on mobile. */
  .promo-input input,
  input[type="email"],
  input[type="text"],
  input[type="search"],
  textarea {
    font-size: 16px;
  }

  /* ---- C. Section rhythm tightened ------------------------------------ */
  /* Desktop uses 48px between sections — on small viewports that's a lot
     of dead space. Pull it back to ~32px so more content fits on screen. */
  .prose h2 {
    margin: 32px 0 12px;
  }
  .body--faye .prose h2,
  .body--samira .prose h2,
  .body--manifesto .prose h2,
  .body--story .prose h2 {
    margin: 32px 0 8px;
  }
  .prose hr {
    margin: 32px 0;
  }
  .two-col {
    margin: 32px 0 0;
  }
  .prose h2.balanced {
    margin: 32px 0 32px;
  }

  /* ---- D. Tap targets ------------------------------------------------- */
  /* Anchor links should be tall enough to tap reliably (Apple HIG says 44px;
     Material Design says 48dp). Add a touch-fudge on small links without
     visually changing them. */
  .prose a,
  .rail nav a {
    /* Prevent rapid double-tap zoom on iOS */
    touch-action: manipulation;
  }
  /* Email submit button — bump touch area */
  .promo-submit {
    width: 48px;
    height: 48px;
    flex-shrink: 0;
  }
  /* Job board: each row already has enough vertical padding for tapping. */

  /* ---- E. Tile sparkles ------------------------------------------------ */
  /* The decorative sparkle SVG positions are absolute % of the tile. On
     smaller tiles (2×2 grid), the % math still works — but the dizzy/sparkle
     can look oversized. Scale down slightly. */
  .illo-tile .brow-tile-sparkle--b {
    width: 32px;                /* was 45 at desktop */
    height: 32px;
    margin-left: -16px;
    margin-top: -16px;
  }

  /* ---- F. Hero illustration on bio pages ------------------------------ */
  /* The illo-video-wrap is centered. On mobile it's full width by default,
     which is fine, but add a bit of margin breathing room. */
  .illo-video-wrap {
    margin: 16px 0;
  }

  /* ---- G. Title row (h1 + marker) ------------------------------------- */
  /* Keep the marker SVG from dominating at small viewport. The marker SVG
     is normally inline; make sure it shrinks alongside the h1. */
  .body--faye .title-row,
  .body--samira .title-row,
  .body--manifesto .title-row {
    flex-wrap: wrap;
    gap: 8px;
  }
  .body--faye .title-marker,
  .body--samira .title-marker,
  .body--manifesto .title-marker,
  .body--story .title-marker {
    width: 32px;
    height: auto;
  }

  /* ---- H. Lead paragraph (subtitle on bio pages) ---------------------- */
  .prose .lead {
    font-size: 0.95rem;
    margin: 12px 0 16px;
  }

  /* ---- I. Job board filter dropdown ---------------------------------- */
  /* Dropdown opens below pill — on mobile the dropdown can extend past the
     viewport horizontally. Constrain it. */
  .job-filter-dropdown {
    max-width: calc(100vw - 48px);
  }
  /* Pill text should be readable */
  .job-filter-pill {
    padding: 8px 14px;
    font-size: 14px;
  }

  /* ---- J. Job board empty state -------------------------------------- */
  .job-empty-state {
    padding: 24px 0 32px;
  }
  .job-empty-character {
    width: 140px;       /* was 170 at desktop */
  }

  /* ---- K. Story page callout — tighter ------------------------------- */
  /* Already tightened in round 1, but the callout-question h2-like line
     could use its own scaling. */
  .callout .callout-question {
    font-size: 1rem;
  }

  /* ---- L. Sparkle for illo-row tiles on mobile ----------------------- */
  /* The hover-tilt on .illo-row tiles uses transform-origin: top-right.
     On mobile (no hover), the tilt never fires, but the perspective on
     .illo-row stays. Disable to keep things clean. */
  .illo-row {
    perspective: none;
  }

  /* ---- M. Benchies pop-up illustrations ------------------------------ */
  /* The .brow-popup hover overlays only fire on (hover: hover), so they're
     auto-disabled on touch. Make sure the popup z-index doesn't intercept
     touches anyway. */
  .benchies-row-1 .brow-popup {
    pointer-events: none;
  }
}

/* ---- N. Small-phone tier (≤ 480px) — most aggressive ------------------ */
@media (max-width: 480px) {
  /* Page padding squeezed down */
  .stage--page {
    padding: 88px 16px 0;
  }
  .body--faye .stage--page,
  .body--samira .stage--page,
  .body--manifesto .stage--page,
  .body--story .stage--page {
    padding-top: 88px;
  }
  .hero-art {
    padding-left: 16px;
    padding-right: 16px;
  }

  /* Section rhythm even tighter on tiny screens */
  .prose h2 {
    margin: 28px 0 10px;
  }
  .prose hr {
    margin: 28px 0;
  }
  .prose {
    padding-bottom: 80px;
  }

  /* h1 shrinks further at the tightest tier — proportional to the
     reduced illustration + quote sizes here. */
  .prose h1 {
    font-size: clamp(1.4rem, 5.5vw, 1.75rem);   /* was 1.75rem / 8vw / 2.5rem */
  }

  /* Two-col gap shrinks */
  .two-col {
    gap: 16px;
  }

  /* Bio illo-row could go single column on the tiniest screens since
     2 across at 320px is only ~140px per tile — readable but tight.
     Keep 2×2 but reduce gap. */
  .illo-row {
    gap: 8px;
  }
  .benchies-row-1 {
    gap: 10px;
  }

  /* Logo in top bar can shrink slightly */
  .rail-inner .logo {
    width: 120px;
  }

  /* Job listings — title size + truncate */
  .prose .job-item-title {
    font-size: 0.95rem;
  }
  .prose .job-item-icon {
    width: 40px;
    height: 40px;
  }
  .job-icon-img {
    width: 30px;
    height: 30px;
  }
  /* Position-type + dot are hidden globally; subtitle gap zero-ed everywhere
     via the base rule, no per-tier override needed. */

  /* Callout tighter still */
  .callout {
    padding: 16px;
    margin: 24px 0;
  }
  .callout::after {
    width: 20px;
    height: 20px;
    top: 12px;
    right: 14px;
  }

  /* Inline hero (Benchies bottom) — illo shrinks */
  .hero-art--inline .hero-illo {
    width: 100%;
    max-width: 280px;
  }

  /* Email signup form — stack input + button vertically if needed.
     Keep them side-by-side since the input flexes; button stays 48px. */

  /* Language picker takes less space */
  .lang-picker .lang-name {
    font-size: 12px;
  }

  /* Hide the back link on tiny screens (rare appearance now anyway) */
  .prose .back {
    margin-bottom: 16px;
  }
}

/* ---- O. Global mobile-safety: prevent any horizontal scroll bleed -------- */
@media (max-width: 1024px) {
  body, html {
    overflow-x: hidden;
  }
  img, video, iframe, svg {
    max-width: 100%;
    height: auto;
  }
}

/* Versioned bio header layouts (V1 + V2) ================================
   V1: lead caption left + quote-mark right (flex row)
   V2: centered quote-mark above centered lead caption (stacked column)
   Both replace the old <p class="lead"> direct child + title-marker SVG.
   The .quote-mark inherits the existing quote-glitch keyframe (originally
   built for .callout::after) so the shake reads consistently across the site.
*/

.bio-caption {
  margin: 24px 0 32px;
}

/* V1 — side-by-side */
.bio-caption--v1 {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
}
.bio-caption--v1 .lead {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
}
.bio-caption--v1 .quote-mark {
  flex-shrink: 0;
  width: 140px;                      /* asset is 355×218 — wider crop so the mist reads in the row */
  height: auto;
  align-self: center;
}

/* V2 — centered stacked: quote-mark overlaps bottom-center of the illo,
   with a soft white glow so the dark glyph reads cleanly over the artwork.
   Wrap structure: .caption-illo wraps the illo-video-wrap + quote-mark.
   The quote is position-absolute over the bottom edge of the illo container. */
.bio-caption--v2 {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 60px;            /* illustration → subtitle: 36 + 24 (extra below to clear the mist halo over the subtitle) */
  margin: 36px 0 32px;  /* title → illustration: 36px */
}
.bio-caption--v2 .caption-illo {
  position: relative;
  width: 100%;
}
.bio-caption--v2 .caption-illo .illo-video-wrap {
  position: relative;
  margin: 0;
}
.bio-caption--v2 .quote-mark {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translate(-50%, 50%);
  width: clamp(160px, 48%, 288px);  /* desktop max 288, smoothly tracks 48% of container on smaller screens, floored at 160 so it never goes tiny */
  height: auto;
  z-index: 2;
  pointer-events: none;
  overflow: visible;    /* let the mist halo bleed past the SVG viewBox — no visible clip rectangle */
}
.bio-caption--v2 .lead {
  width: 100%;          /* fill the bio-caption container */
  max-width: none;      /* drop the 520px cap */
  margin: 0;
  text-align: center;
  position: relative;   /* establish stacking */
  z-index: 3;           /* sits ABOVE the quote-mark (z-index 2) so the mist halo doesn't cover the words */
}

/* Quote-mark — glitch keyframe + base setup (entrance is staged by the
   bio-page sequence block further down). */
.quote-mark {
  display: block;
}
.quote-mark .quote-glyph {
  transform-origin: center;
  transform-box: fill-box;
  will-change: transform, filter;
}
@keyframes quote-rise {
  from { opacity: 0; transform: translateY(28px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ============================================================
   BIO-PAGE STAGED REVEAL (Faye, Samira)
   Sequence (cumulative timeline from page load):
     1. Leading illustration: opacity 0 + blur(16px) → 100% + blur(0)
        over 1.6s starting at 0.05s. Slow, no translate.
     2. Title (h1), subtitle (.lead), mist halo (.quote-mist) AND the
        rest of the prose (hr divider, h2s, body paragraphs, illo rows,
        two-col blocks, closing illos) all fade in together — 0.8s start,
        0.7s duration → lands at 1.5s (just before the blur fade-in
        finishes at 1.65s, so the page settles just ahead of the
        illustration sharpening).
     3. Quote glyph (LAST): rises + glitches. Rise 2.35s → 3.1s; glitch
        begins at 3.15s and loops forever. (Page reads as fully at rest
        before the glyph announces itself.)
   The global `.from-nav .prose` contentIn is overridden for these
   pages so each piece can stage independently regardless of how the
   visitor arrived. */

/* Step 0: kill the page-wide contentIn so individual children stage on their own */
.from-nav .body--faye .prose,
.from-nav .body--samira .prose {
  opacity: 1;
  transform: none;
  animation: none;
}

/* Step 1: leading illustration — slow blur + opacity */
.body--faye .illo-video-clip,
.body--samira .illo-video-clip {
  opacity: 0;
  filter: blur(16px);
  animation: bio-illo-in 1.6s cubic-bezier(0.22, 1, 0.36, 1) 0.05s forwards;
  will-change: opacity, filter;
}
@keyframes bio-illo-in {
  to { opacity: 1; filter: blur(0); }
}
/* Bio-page lead-illustration zoom-in — the inner <img> starts at 80% scale
   and eases back to 100% over the SAME 1.6s window as the container's blur
   fade. So the photo "settles into" the frame in lockstep with the blur
   resolving. Container size stays fixed; only the image content scales.
   Also resets the default .illo--video scale(1.1) + 40px-down position
   (which were tuned for the original eye-close-up video) so the new
   1920×504 images render flush against the 610:160 frame. */
.body--faye .illo--video,
.body--samira .illo--video {
  transform: scale(1);
  object-position: center;
  animation: bio-lead-zoom-in 1.6s cubic-bezier(0.22, 1, 0.36, 1) 0.05s both;
  transform-origin: center;
  will-change: transform;
}
@keyframes bio-lead-zoom-in {
  from { transform: scale(0.8); }
  to   { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .body--faye .illo--video,
  .body--samira .illo--video { animation: none; transform: scale(1); }
}

/* Step 2: title + subtitle + mist + the rest of the prose (divider, h2s,
   body paragraphs, illo rows, two-col blocks, closing illos) all fade in
   together, just before the illustration lands */
.body--faye .prose > h1,
.body--samira .prose > h1,
.body--faye .bio-caption--v2 .lead,
.body--samira .bio-caption--v2 .lead,
.body--faye .quote-mark .quote-mist,
.body--samira .quote-mark .quote-mist,
.body--faye .prose > hr,
.body--samira .prose > hr,
.body--faye .prose > h2,
.body--samira .prose > h2,
.body--faye .prose > p,
.body--samira .prose > p,
.body--faye .prose > .illo-row,
.body--samira .prose > .illo-row,
/* scrollRowBubble JS wraps .illo-row in a .has-row-bubble div on Faye + Samira,
   so the direct-child selectors above miss it once that wrap is in place.
   Match the wrap too so the staged reveal still catches the row. */
.body--faye .prose > .has-row-bubble,
.body--samira .prose > .has-row-bubble,
.body--faye .prose > .two-col,
.body--samira .prose > .two-col,
.body--faye .prose > img.illo,
.body--samira .prose > img.illo {
  opacity: 0;
  animation: bio-fade-step 0.7s ease-out 0.8s forwards;
}

/* Step 3 (LAST): quote glyph — rises into position, then glitches forever,
   after the rest of the page is settled. Rise lands at 3.1s
   (0.7s earlier than before). */
.body--faye .quote-mark .quote-glyph,
.body--samira .quote-mark .quote-glyph {
  opacity: 0;
  transform: translateY(28px);
  animation:
    quote-rise 0.75s cubic-bezier(0.22, 1, 0.36, 1) 2.35s forwards,
    quote-glitch 0.9s linear 3.15s infinite;
}

@keyframes bio-fade-step {
  to { opacity: 1; }
}

/* Accessibility — skip the whole staged sequence when reduced motion is on */
@media (prefers-reduced-motion: reduce) {
  .body--faye .illo-video-clip,
  .body--samira .illo-video-clip,
  .body--faye .prose > h1,
  .body--samira .prose > h1,
  .body--faye .bio-caption--v2 .lead,
  .body--samira .bio-caption--v2 .lead,
  .body--faye .quote-mark .quote-mist,
  .body--samira .quote-mark .quote-mist,
  .body--faye .quote-mark .quote-glyph,
  .body--samira .quote-mark .quote-glyph,
  .body--faye .prose > hr,
  .body--samira .prose > hr,
  .body--faye .prose > h2,
  .body--samira .prose > h2,
  .body--faye .prose > p,
  .body--samira .prose > p,
  .body--faye .prose > .illo-row,
  .body--samira .prose > .illo-row,
  .body--faye .prose > .has-row-bubble,
  .body--samira .prose > .has-row-bubble,
  .body--faye .prose > .two-col,
  .body--samira .prose > .two-col,
  .body--faye .prose > img.illo,
  .body--samira .prose > img.illo {
    opacity: 1;
    transform: none;
    filter: none;
    animation: none;
  }
}

/* V2 title centering — h1 sits dead-center above the eye illustration */
.body--faye .stage--page .prose .title-row,
.body--samira .stage--page .prose .title-row {
  justify-content: center;
}
.body--faye .stage--page .prose h1,
.body--samira .stage--page .prose h1 {
  text-align: center;
  width: 100%;
}

/* Mobile: V1's side-by-side stacks below 640px so the quote doesn't squash
   the caption. V2 stays centered (it already is). */
@media (max-width: 640px) {
  .bio-caption--v1 {
    flex-direction: column;
    align-items: flex-start;
    gap: 16px;
  }
  .bio-caption--v1 .quote-mark {
    align-self: flex-end;
  }
  /* V2 quote-mark uses clamp() at the top — no abrupt mobile width override needed; it scales smoothly with the illustration. */
}

/* Tightest-tier (≤480px) — disable hover illos + stretch the email form ===
   On the smallest phone screens, hover-driven illustration effects are
   awkward (touch users can trigger them by accident, they get stuck open,
   and they fight finger-scrolling). Disable them. Also force the Benchies
   email signup row to fill the column width, since it's the primary CTA. */
@media (max-width: 480px) {
  /* --- Disable hover effects on illustrations ----------------------- */

  /* Bio-page .illo-tile sketch row — tilt + shadow + sparkles */
  .illo-row img.illo:hover {
    transform: none !important;
    box-shadow: none !important;
    z-index: 1 !important;
  }
  .illo-tile:hover .brow-tile-sparkle {
    opacity: 0 !important;
    animation: none !important;
  }

  /* Benchies character tiles — popup illustrations + spotlight blur */
  .benchies-row-1 .brow-tile .brow-popup,
  .benchies-row-1 .brow-tile .brow-popup--1,
  .benchies-row-1 .brow-tile .brow-popup--2,
  .benchies-row-1 .brow-tile .brow-popup--3 {
    display: none !important;
  }
  .benchies-row-1 .brow-tile:hover {
    transform: none !important;
  }
  .benchies-row-1 .brow-tile.is-spotlit .brow-tile-sparkle {
    opacity: 0 !important;
    animation: none !important;
  }
  .page-blur {
    display: none !important;
  }

  /* Homepage + Benchies hero-illo bubble */
  .hero-illo:hover .bubble-hover,
  .hero-illo:hover .bubble-svg,
  .hero-illo:hover .dizzy-svg {
    opacity: 0 !important;
    animation: none !important;
  }

  /* --- Benchies email form: full container width -------------------- */
  .hero-art--inline .promo-form {
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
  }
  .hero-art--inline .promo-input {
    flex: 1 1 0;
    min-width: 0;          /* lets the flex item actually shrink/grow */
    width: 100%;
  }
  .hero-art--inline .promo-input input {
    width: 100%;
    box-sizing: border-box;
  }
  /* Send button stays its 48×48 — input flex-grows to fill the rest */
  .hero-art--inline .promo-submit {
    flex-shrink: 0;
  }
}

/* Bio-page hero illo — no frame ==========================================
   The 1px stroke + light-fill background that wraps standard detail-page
   imagery reads as a 'frame' / drop-shadow around the eye close-up.
   Bio pages drop both so the artwork sits flush against the page. */
.body--faye .illo-video-clip,
.body--samira .illo-video-clip {
  border: 0;
  background: #DAD8E2;          /* match the headshot tile fill color */
}
/* Samira: drop the aurora-glow on the main illustration. The conic-gradient
   blur + animation on .illo-video-wrap::before is the only "drop shadow /
   glow" around the eye close-up; hiding the pseudo on Samira removes it
   cleanly without touching any other illo-video-wrap consumer. */
.body--samira .illo-video-wrap::before {
  display: none;
}
