/* ============================================================
   Homepage v2 — "keep it snappy" direction, from Figma
   (Portfolio 3 – Simplified, frame "MacBook Pro 16" - 13",
   node 1629:17200 for the work-samples row — pulled via
   Figma MCP design context, so colors/sizes/copy below are
   taken directly from the file, not eyeballed.)
   Isolated from style.css so the case-study pages are untouched
   while we iterate on just the homepage.
   ============================================================ */

:root {
  --v2-peach: #fbe8cf;
  --v2-black: #111110;
  --v2-gray: #8a8a86;
  --v2-gray-light: #b6b6b2;
  --v2-border: #ececea;
  /* The site's one accent color lives in style.css as --accent (loaded
     on every page) and is used directly here, not re-aliased — an
     alias declared on :root would freeze at :root's own --accent and
     never see the body[data-mood="zen"] override in style.css, since
     custom-property resolution happens per declaring element. */

  /* exact card background colors from Figma */
  --v2-remitly: #242620;
  --v2-newsfeed: #cc348d;
  --v2-paylink-bg: #ebe6e1;
  --v2-streak: #5f7d8b;
}

/* Mode type: Snappy tightens, Zen loosens, same typeface either way.
   NOT applied right now: the hero headline and the card teasers use
   Snappy's values in both moods (see .d-headline and .v2-card-desc), so
   the two moods look identical apart from color and the circle. Point
   those rules back at these variables to bring the difference back.
   Weight is applied to the teasers only — the headline's Crimson Text
   only ships 400 and 600 here, and 600 at headline size is a much
   heavier jump than "tighten" is asking for. */
body[data-mood="snappy"] {
  --mode-line-height: 1.28;
  --mode-letter-spacing: -0.015em;
  --mode-weight: 500;
}
body[data-mood="zen"] {
  --mode-line-height: 1.5;
  --mode-letter-spacing: 0.005em;
  --mode-weight: 400;
}

body.home-v2 { font-family: 'Inclusive Sans', -apple-system, BlinkMacSystemFont, sans-serif; }

/* ---------- NAV ---------- */
.v2-nav {
  display: flex; align-items: center; justify-content: space-between;
  /* Same max-width + centering + padding recipe as .v2-hero /
     .v2-section-head / .v2-scroll-wrap below — without it, on any
     viewport wider than 1600px + padding, the nav's flat 96px padding
     sits left of where those max-width:1600px boxes actually start
     (they center with extra margin beyond their own padding), so the
     logo reads as misaligned against everything below it. */
  max-width: 1600px; margin: 0 auto; padding: 28px 96px; box-sizing: border-box;
  position: sticky; top: 0; background: var(--white); z-index: 100;
}
/* Logo is an inline SVG (fill="currentColor" on its paths) so it always
   tracks the one accent color via this element's own `color`. */
.v2-logo { display: inline-flex; text-decoration: none; color: var(--accent); }
.v2-logo-img { height: 20px; width: auto; display: block; }
.v2-nav-links { display: flex; gap: 28px; list-style: none; }
.v2-nav-links a { text-decoration: none; }
/* Single mood control now — lowercase, in brackets like a live variable
   value, rather than a two-button tablist. Click cycles to the next
   mood (see the JS MOOD_ORDER list). */
.v2-mood-cycle {
  font-size: 11px; font-weight: 700; letter-spacing: 0.04em;
  color: var(--v2-black); transition: color 0.15s;
  background: none; border: none; padding: 0; cursor: pointer; font-family: inherit;
}
.v2-mood-cycle:hover { color: var(--accent); }

/* ---------- HERO ---------- */
.v2-hero { text-align: left; padding: 56px 96px 0; max-width: 1600px; margin: 0 auto; box-sizing: border-box; }
.v2-hero h1 {
  font-family: 'Inclusive Sans', sans-serif; font-weight: 400; font-size: clamp(22px, 2.6vw, 30px);
  color: var(--v2-black); margin-bottom: 16px; letter-spacing: -0.01em;
}
.v2-hero p.v2-sub { font-size: 14px; line-height: 1.55; color: #505050; margin-bottom: 6px; width: 30%; min-width: 280px; }
.v2-hero p.v2-detail { font-size: 13px; color: #8c8c8c; margin-top: 10px; }
.v2-accent-divider { color: var(--accent); font-weight: 700; }

/* ---------- CALLOUT CIRCLE(S) ---------- */
.v2-callout {
  /* 142px top/bottom — this and .v2-section-head's margin-top are set to
     the same value so the circle sits centered in the whitespace between
     the hero text and "Some project samples", instead of hugging the
     hero (64px above it, 220px below it, before this fix). */
  max-width: 1600px; margin: 142px auto 0; padding: 0 96px;
  display: flex; justify-content: center; position: relative;
  box-sizing: border-box;
  /* Dragging the dot circle was catching nearby text as a selection,
     which shows a blinking text caret/highlight — this area isn't
     meant to be selectable text anyway. */
  -webkit-user-select: none; user-select: none; cursor: default;
}
/* Both graphics stack in the same spot; only the active one is visible —
   toggled from the Zen/Snappy buttons up in the nav. */
.v2-circle-stage { position: relative; width: 260px; height: 260px; }
.v2-circle-stage .v2-ring3d,
.v2-circle-stage .v2-callout-circle {
  position: absolute; top: 0; left: 0;
  opacity: 0; pointer-events: none;
  transition: opacity 0.4s ease;
}
.v2-circle-stage .v2-ring3d.is-active,
.v2-circle-stage .v2-callout-circle.is-active {
  opacity: 1; pointer-events: auto;
}
.v2-callout-circle {
  /* No fill/gradient behind it on purpose — just the dots themselves.
     `color` isn't used for rendering here — the zen canvas reads it back
     via getComputedStyle so its grains always track --accent, even
     though canvas fillStyle can't reference a CSS variable directly. */
  width: 260px; height: 260px; border-radius: 50%; overflow: hidden;
  background: transparent; color: var(--accent);
  display: block; cursor: pointer;
  /* Without this, a touch-drag on the circle also pans the page underneath
     it — the browser's default touch behavior competes with the canvas's
     own touchmove handling for the same gesture. This tells the browser
     not to reserve the gesture for scrolling here at all. */
  touch-action: none;
}

/* --- 3D tilt ring graphic. A cluster of thin circles that spins slowly
   on its own (CSS keyframe on the inner .v2-ring3d-spin layer) while the
   outer .v2-ring3d-stage tilts in 3D toward the cursor (JS-set
   rotateX/rotateY) — two independent transforms on two different
   elements, so they compose instead of fighting each other. */
.v2-ring3d { width: 260px; height: 260px; perspective: 700px; }
.v2-ring3d-stage {
  width: 100%; height: 100%; position: relative;
  transform-style: preserve-3d;
  transition: transform 0.5s var(--liquid-ease, cubic-bezier(.22,1.12,.36,1));
}
.v2-ring3d-spin {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  animation: v2-ring-spin 16s linear infinite;
}
@keyframes v2-ring-spin {
  from { transform: rotateZ(0deg); }
  to { transform: rotateZ(360deg); }
}
.v2-ring {
  position: absolute; inset: 0; border-radius: 50%;
  border: 1.5px solid var(--accent);
}
.v2-ring-a { transform: translate3d(-4%, -3%, 30px) scale(0.9); opacity: 0.9; }
.v2-ring-b { transform: translate3d(3%, 5%, 0) scale(1); opacity: 0.65; }
.v2-ring-c { transform: translate3d(7%, -6%, -30px) scale(0.78); opacity: 0.45; }

/* ---------- SECTION LABEL ROW ---------- */
/* .v2-section-head and .v2-scroll-wrap share the identical max-width +
   centering + padding recipe below, so their left edges always line up —
   and on screens wider than the max-width, the whole block centers as one
   unit (whitespace grows symmetrically) instead of either one staying
   pinned to the raw viewport edge. */
.v2-section-head {
  max-width: 1600px; margin: 142px auto 24px; padding: 0 96px; font-size: 17px; box-sizing: border-box;
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
}
.v2-section-head p { margin: 0; }
.v2-section-head strong { font-weight: 700; color: var(--v2-black); }
.v2-section-head span { color: var(--v2-gray); font-weight: 400; }
/* Prev/next controls for the row, pushed to the row's right edge by the
   space-between above — static (no animation), same quiet glyph-button
   treatment as .v2-card-close: no fill/border, just the character
   itself, dimmed until hover. */
.v2-scroll-nav { display: flex; flex: 0 0 auto; }
.v2-scroll-nav-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; margin-left: 4px;
  background: none; border: none; padding: 0;
  font-size: 20px; line-height: 1; color: var(--v2-black); opacity: 0.5; cursor: pointer;
  transition: opacity 0.2s ease;
}
.v2-scroll-nav-btn:hover { opacity: 1; color: var(--accent); }

/* ---------- WORK SAMPLES: horizontal scroll row ---------- */
.v2-scroll-wrap { max-width: 1600px; margin: 0 auto; padding: 0 0 24px; }
.v2-card-row {
  display: flex; gap: 32px; align-items: flex-start;
  overflow-x: auto;
  /* Explicit, not left to default — with only overflow-x set, the spec
     computes overflow-y as auto too, quietly making this a *vertical*
     scroll container as well. On touch that let a finger swipe meant for
     the page get captured as an internal scroll of the row instead,
     cropping an open card's top edge against its own clipped box. This
     row only ever scrolls horizontally (via the drag/touch handling in
     home-v2.js) — vertical gestures should always fall through to the
     page. */
  overflow-y: hidden;
  /* No scroll-snap: combined with padding on a scroll container, Chrome's
     snap-on-load can set the initial scrollLeft to the padding amount,
     which visually eats the left inset. Dragging works fine without it. */
  padding: 4px 96px 20px;
  -webkit-overflow-scrolling: touch;
  cursor: grab;
  /* No visible scrollbar, ever — this row scrolls by dragging (see home-v2.js)
     or touch/trackpad, never a scrollbar the user drags. */
  scrollbar-width: none;
}
.v2-card-row::-webkit-scrollbar { display: none; }
.v2-card-row.dragging { cursor: grabbing; user-select: none; }
.v2-card-row.dragging .v2-card { pointer-events: none; }

/* Shared "liquid" easing: fast start, long satisfying settle with a
   whisper of overshoot at the end — used everywhere something opens,
   grows, or rises, so the whole card feels like one coherent material. */
:root {
  --liquid-ease: cubic-bezier(.22, 1.12, .36, 1);
  /* Same family, but never exceeds 1 — for the bottom-anchored, masked
     image reveal specifically. The overshoot above is great for width/
     panel motion, but on a clipped bottom-anchored image it means the
     image briefly rises past its resting spot and reveals card
     background beneath it. This curve settles at exactly translateY(0),
     never past it. */
  --liquid-ease-image: cubic-bezier(.16, 1, .3, 1);
  --liquid-duration: 0.7s;
  /* One source of truth for a resting card's footprint — the mobile
     block further down re-points these, and home-v2.js reads
     --v2-card-h back to know what height to collapse an accordion card
     to, so the two can't drift apart. */
  --v2-card-w: 268px;
  --v2-card-h: 383px;
}

.v2-card {
  flex: 0 0 auto; width: var(--v2-card-w); height: var(--v2-card-h);
  border-radius: 15px 15px 0 0; overflow: hidden; position: relative;
  text-decoration: none; color: inherit; cursor: pointer;
  display: flex; flex-direction: row; align-items: stretch;
  box-shadow: 0 0 0 rgba(0,0,0,0);
  transition: width var(--liquid-duration) var(--liquid-ease), box-shadow var(--liquid-duration) ease;
  /* Clicking to toggle expand was starting a text selection on the title/
     description, which some browsers render as a blinking text caret.
     These cards aren't text content to select, so disable it outright. */
  -webkit-user-select: none; user-select: none;
}
.v2-card:focus { outline: none; }
/* Same color as the card's own divider line (see .v2-card-expand's
   border-left and .v2-card-divider) rather than a bright white ring,
   which read as an off-palette border on the light cards. */
.v2-card:focus-visible { outline: 2px solid rgba(255,255,255,0.18); outline-offset: -2px; }
.v2-card.on-light:focus-visible,
.v2-card.duocon:focus-visible { outline-color: rgba(0,0,0,0.12); }
/* A soft lift once open — reads as the card actually rising off the row
   rather than just changing size, on top of whichever direction (wider,
   or taller for the mobile accordion) it's actually growing in. */
.v2-card.is-open { box-shadow: 0 18px 40px rgba(0,0,0,0.16); }
.v2-card.is-open { width: 688px; }

.v2-card-main {
  flex: 0 0 268px; width: 268px; height: 100%;
  display: flex; flex-direction: column;
  position: relative;
}

.v2-card-title {
  font-weight: 700; font-size: 17px; color: #fff;
  padding: 18px 21px 0;
  position: relative; z-index: 3;
}
.v2-card-desc {
  /* The screenshot sits on top of this at rest and fully hides it — not
     dimmed-through, actually covered — so it doesn't need its own
     position in the flex column; it's overlaid absolutely, starting
     right under the title. On hover the image slides down (see
     .v2-card-shot below) and this simply brightens to full opacity in
     the gap it leaves, rather than moving itself. */
  /* Weight 500, not 400 — plain regular white-on-color was hard to read;
     600 turned out too heavy, especially on the on-light (dark-text)
     cards. 500 needs Inclusive Sans' medium weight loaded in index.html. */
  /* Same in every mood for now (Snappy's values); the per-mode variables
     up top can drive these again later. */
  font-weight: 500; font-size: 13px;
  line-height: calc(1.28 * 1.42);
  letter-spacing: -0.015em;
  color: #fff;
  position: absolute; top: 60px; left: 0; right: 0; z-index: 1;
  padding: 0 23px;
  opacity: 0.4;
  transition: opacity 1s var(--liquid-ease-image);
}
.v2-card.on-light .v2-card-title,
.v2-card.on-light .v2-card-desc { color: var(--v2-black); }
/* Trailing arrow — a dedicated element (not literal ">" text baked into
   the teaser) so it can be independently styled/rotated for the mobile
   accordion below, without the mood cycle's text-fade update ever
   touching or clobbering it. A proper arrow (shaft + head), not a bare
   chevron, which still read as the ">" character it replaced. Drawn as
   a mask filled with currentColor so it recolors itself correctly on
   the on-light cards. */
.v2-card-arrow {
  display: inline-block; width: 13px; height: 13px; margin-left: 5px;
  vertical-align: -2px;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12h15M12.5 5.5L19 12l-6.5 6.5'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12h15M12.5 5.5L19 12l-6.5 6.5'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* Holds the teaser's last word *and* the arrow, so a line break can't
   leave the arrow stranded alone on a line of its own — an atomic
   inline box like it allows a break immediately before it even with no
   whitespace there. (A word joiner between the two, the usual trick,
   doesn't stop it; only keeping them inside one nowrap box does.)
   home-v2.js writes the split, and only ever rewrites this span's text
   node, so the arrow element itself survives every mood swap. */
.v2-card-desc-tail { white-space: nowrap; }

.v2-card:hover .v2-card-desc,
.v2-card.is-open .v2-card-desc,
.v2-card.is-touched .v2-card-desc {
  opacity: 1;
}

.v2-card-shot {
  /* Full width (auto height, true aspect ratio, never cropped) rather
     than object-fit'd into a box — letterboxing an image to fit a fixed
     box left gaps on the sides that the dimmed text behind it peeked
     through. The reveal is done purely by sliding this whole block down
     on hover, so the image itself never grows or shrinks. */
  position: absolute; left: 8px; right: 8px; top: 58px; z-index: 2;
  padding: 0;
  transform: translateY(0);
  transition: transform 1s var(--liquid-ease-image);
}
.v2-card:hover .v2-card-shot,
.v2-card.is-open .v2-card-shot,
.v2-card.is-touched .v2-card-shot {
  /* Set per-card by JS (measured from the actual rendered teaser height,
     so the gap below the text matches the gap above it regardless of
     how many lines that card's teaser wraps to) — 120px is just the
     fallback before the first measurement runs. */
  transform: translateY(var(--shot-open-y, 120px));
}
.v2-card-shot img {
  width: 100%; height: auto; max-width: none; max-height: none;
  display: block;
  /* Never shorter than the space from the shot's top (58px) to the
     card's bottom edge: a screenshot with a wider aspect ratio would
     otherwise stop short and leave a strip of card color under it.
     Taller images are unaffected; a shorter one fills the gap by
     cropping a little off its sides, anchored to the top. */
  min-height: calc(var(--v2-card-h) - 58px);
  object-fit: cover; object-position: top center;
  -webkit-user-drag: none; user-select: none;
  /* Uniform rounded top on every card's image — some source screenshots
     (remitly, pay with a link) already have a phone bezel baked in, but
     others (news feed, duocon) don't, which reads as misaligned even
     when the Y position is identical. This makes all five consistent
     regardless of what's in the source file. */
  border-radius: 14px 14px 0 0;
  /* One shared shadow on every card image. The source files that had a
     shadow baked in (and the per-card side offsets that compensated
     for it) were swapped for trimmed *-clean.png copies, so they don't
     double up and every image sits on its card the same way. */
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.18);
  transform: none; filter: none; transition: none;
}

/* ---------- EXPAND PANEL ---------- */
.v2-card-expand {
  flex: 0 0 0; width: 0; overflow: hidden; position: relative;
  border-left: 1px solid rgba(255,255,255,0.18);
  transition: width var(--liquid-duration) var(--liquid-ease), opacity 0.5s ease;
  opacity: 0; padding: 0; color: #fff;
  display: flex; flex-direction: column; gap: 18px; justify-content: center;
}
.v2-card.is-open .v2-card-expand {
  flex-basis: 420px; width: 420px; opacity: 1; padding: 32px 32px;
}
.v2-card.on-light .v2-card-expand { border-left-color: rgba(0,0,0,0.12); color: var(--v2-black); }
.v2-card.duocon .v2-card-expand { color: var(--v2-black); }

.v2-card-close {
  position: absolute; top: 24px; right: 28px;
  background: none; border: none; padding: 4px; margin: 0; font-family: inherit;
  font-size: 24px; color: inherit; opacity: 0.6; cursor: pointer; line-height: 1;
  transition: opacity 0.2s ease, transform 0.2s var(--liquid-ease);
}
.v2-card-close:hover { opacity: 1; transform: scale(1.1); }

/* Number + label sit on one row, like the reference — the label given
   generous room (420px panel) and white-space:nowrap so it never wraps
   to a second line. Each stat cascades in with a short stagger once the
   card is open, rather than appearing all at once. */
.v2-stat {
  color: #fff; display: flex; align-items: baseline; gap: 10px; white-space: nowrap;
  opacity: 0; transform: translateY(10px);
  transition: opacity 0.5s ease, transform 0.5s var(--liquid-ease);
  transition-delay: 0s;
}
.v2-card.is-open .v2-stat { opacity: 1; transform: translateY(0); }
.v2-card.is-open .v2-stat:nth-child(2) { transition-delay: 0.06s; }
.v2-card.is-open .v2-stat:nth-child(3) { transition-delay: 0.12s; }
.v2-card.is-open .v2-stat:nth-child(4) { transition-delay: 0.18s; }
.v2-card.is-open .v2-stat:nth-child(5) { transition-delay: 0.24s; }
.v2-card.on-light .v2-stat { color: var(--v2-black); }
.v2-stat-num {
  /* Inclusive Sans throughout — this used to be set to Karla by mistake. */
  font-weight: 700; font-size: 32px; line-height: 1.1;
  flex: 0 0 auto;
}
.v2-stat-label { font-size: 13px; opacity: 0.75; line-height: 1.4; flex: 0 0 auto; }

.v2-card-soon { color: #fff; font-size: 15px; opacity: 0.8; }
.v2-card.on-light .v2-card-soon { color: var(--v2-black); }

/* ---------- ROW 2: SYSTEM SNAPSHOTS ----------
   Same card component as Row 1. Colors are pulled from Row 1's own
   palette rather than new ones being invented — placeholder pending
   Vivian. These cards have no screenshot and (until their stat panels
   are unblocked) no stats, so the expand panel carries a plain
   mechanism description instead. */
.v2-card.sys-alloc { background: var(--v2-remitly); }
.v2-card.sys-gate { background: var(--v2-streak); }
.v2-card.sys-patterns { background: var(--v2-paylink-bg); }
.v2-card-note { color: #fff; font-size: 14px; line-height: 1.6; opacity: 0.85; }
.v2-card.on-light .v2-card-note { color: var(--v2-black); }

/* With no screenshot sliding down over it, the teaser has nothing to be
   revealed from behind — so it sits at full strength from the start
   instead of the dimmed resting state the other cards use, and the
   hover/peek brightening has nothing left to do. */
.v2-card.no-shot .v2-card-desc { opacity: 1; }

/* ---------- CARD DEPTH TIERS ----------
   Case study: a link at the bottom of the open panel. Overview: the
   panel's notes/stats. Locked (.is-locked): a blurred screenshot with a
   lock baked in. .is-static cards don't open at all (they lose the
   pointer cursor), but still slide their screenshot down on hover to
   show their one line, like every other card. */
.v2-card.is-static { cursor: default; }
.v2-card-link {
  /* "dive deeper" + a diving-mask icon. Takes the card's text color. */
  align-self: flex-start; display: inline-flex; align-items: center; gap: 6px;
  color: #fff; line-height: 1; font-size: 13px; text-decoration: none;
  opacity: 0.85; transition: opacity 0.15s ease, transform 0.15s ease;
}
.v2-card-link:hover { opacity: 1; transform: translateY(-1px); }
/* In the open side panel it pins to the bottom-right corner: the icon
   sits centered under the close X (whose glyph centers ~39px from the
   right edge), the words run off to its left, and it's as far from the
   bottom as the X is from the top. */
.v2-card-expand .v2-card-link { position: absolute; right: 31px; bottom: 24px; }
.v2-card.on-light .v2-card-link,
.v2-card.duocon .v2-card-link { color: var(--v2-black); }
/* Two short notes read as one list, not two separate paragraphs. */
.v2-card-note + .v2-card-note { margin-top: -10px; }

/* card 1 — remitly business */
.v2-card.remitly { background: var(--v2-remitly); }


/* duolingo stories */
.v2-card.stories { background: #226dc7; }

/* card 2 — duolingo news feed */
.v2-card.newsfeed { background: var(--v2-newsfeed); }

/* card 3 — pay with a link */
.v2-card.paylink { background: var(--v2-paylink-bg); }

/* card 4 — duocon: frosted dragon (Daenerys/High Valyrian nod) as a
   full-card background — scoping it to just the shot rectangle made it
   crop in too tight and stop reading as a background image, so it spans
   the whole card again. The sharp badge photo sits on top of it inside
   .v2-card-shot, using the exact same alignment/rounding treatment as
   every other card's peek image. */
.v2-card.duocon { background: #f4f2ee; }
.v2-card.duocon::before {
  content: ''; position: absolute; inset: -12px; z-index: 0;
  background-image: url('/assets/images/duocon/07-danydraca.png');
  background-size: cover; background-position: center 48%;
  filter: blur(4px) saturate(0.9) brightness(1.25);
}
.v2-card.duocon::after {
  content: ''; position: absolute; inset: 0; z-index: 0;
  /* #EAEAEA wash, not white — gives it a touch of contrast against the
     page background instead of washing out to pure white. */
  background: rgba(234,234,234,0.55);
  backdrop-filter: blur(1px);
}
.v2-card.duocon .v2-card-title,
.v2-card.duocon .v2-card-desc,
.v2-card.duocon .v2-stat { color: var(--v2-black); }
.v2-card.duocon .v2-card-expand { border-left-color: rgba(0,0,0,0.12); }
/* .v2-card-title/-desc/-shot already carry their own z-index (3/1/2) from
   the shared rules above, which is enough to sit above this card's own
   ::before/::after background layers (z-index 0) — only .v2-card-expand
   needs the explicit bump, since it's otherwise unpositioned. */
.v2-card.duocon .v2-card-expand { position: relative; z-index: 1; }

/* card 5 — duolingo streak society */
.v2-card.streak { background: var(--v2-streak); }

/* ---------- NUGGETS (placeholder) ---------- */
/* Same max-width + centering + padding recipe as .v2-section-head /
   .v2-scroll-wrap above, so this row's left edge always lines up with the
   "Random nuggets" label. The tiles themselves stay a fixed compact size
   (not stretched full-width) via minmax + justify-content: start. */
.v2-nugget-row {
  display: grid; grid-template-columns: repeat(4, minmax(0, 160px)); justify-content: start; gap: 16px;
  max-width: 1600px; margin: 0 auto; padding: 0 96px; box-sizing: border-box;
}
.v2-nugget { aspect-ratio: 3/4; border-radius: 10px; background: #ececea; }
.v2-nugget.dark { background: #d8d8d4; }

.v2-footer-spacer { height: 120px; }

/* ---------- RESPONSIVE ---------- */
@media (max-width: 640px) {
  .v2-nav, .v2-section-head, .v2-hero { padding-left: 24px; padding-right: 24px; }
  .v2-hero p.v2-sub { width: 100%; min-width: 0; }
  .v2-callout { padding: 0 24px; }
  .v2-card-row { padding-left: 24px; padding-right: 24px; }
  /* Roughly 80% of the screen, so a card reads as the main thing on it
     while the next one still peeks in at the right to show the row
     scrolls. 80vw plus the row's own 24px inset leaves that peek a bit
     under 40px on a typical phone, which the tighter gap below keeps
     from being swallowed. Capped so it doesn't balloon on a tablet
     that's still inside this breakpoint. Height stays proportional to
     the desktop card's 268x383 so the screenshot inside crops the same
     way it always has. */
  :root {
    --v2-card-w: min(80vw, 340px);
    --v2-card-h: calc(min(80vw, 340px) * 1.429);
  }
  .v2-card-row { gap: 16px; }
  /* The inner column is a fixed 268px by default (it's the left half of
     the desktop side-panel split) — on mobile the card is one column,
     so it should just fill whatever width the card now has. */
  .v2-card-main { flex-basis: 100%; width: 100%; }
  .v2-nugget-row { grid-template-columns: repeat(2, 1fr); padding: 0 24px; }
  /* Set by home-v2.js from the row's actual width, so the card never
     opens wider than the screen — see syncOpenCardSizing there for why.
     The px fallbacks only matter if JS hasn't run yet. */
  .v2-card.is-open { width: var(--card-open-w, 460px); }
  .v2-card.is-open .v2-card-main { flex-basis: var(--card-open-main-w, 268px); width: var(--card-open-main-w, 268px); }
  .v2-card.is-open .v2-card-expand { flex-basis: var(--card-open-expand-w, 192px); width: var(--card-open-expand-w, 192px); padding: 36px 18px; }
  /* The expand panel's own floor (130px) is narrower than "100M DAUs" fits
     on one line at the stat's normal 32px/nowrap sizing — stack the
     number over its label instead of clipping the line against the
     card's overflow:hidden. Font size trimmed too, so it takes less
     width at this column size. */
  .v2-card.is-open .v2-stat { flex-direction: column; align-items: flex-start; gap: 2px; white-space: normal; }
  .v2-card.is-open .v2-stat-num { font-size: 22px; }
  /* .v2-card-expand centers its stats vertically (justify-content:center
     on the base rule) — how close that centered block's top edge lands
     to the close button varies by card (more stats = taller block = less
     top clearance), so a fixed padding tuned for one card either wastes
     space on short ones or still isn't enough for the tallest. A small
     backdrop behind the button instead makes any close overlap read as
     an intentional layered control rather than clipped/broken text. */
  .v2-card.is-open .v2-card-close {
    background: rgba(0,0,0,0.45); border-radius: 50%;
    width: 26px; height: 26px; padding: 0;
    display: flex; align-items: center; justify-content: center;
  }
  .v2-card.on-light.is-open .v2-card-close,
  .v2-card.paylink.is-open .v2-card-close { background: rgba(255,255,255,0.65); }
}

/* ---------- MOBILE ACCORDION (test: remitly only) ----------
   An alternative to the side-panel reveal above, opted into per-card via
   .v-accordion — expands straight down in place instead of widening
   sideways: Header > Subheader (with a down-chevron instead of the ">"
   every other card uses) > divider > stats > screenshot. Collapsed state
   is untouched (same markup, same hover/rest styling) — only .is-open is
   redefined here, and with three classes' worth of specificity it wins
   over the two-class .v2-card.is-open rules above without editing them. */
@media (max-width: 640px) {
  /* Touch devices can leave :hover "stuck" active after a tap (there's
     no real pointer to leave), which would otherwise trigger the
     side-panel cards' own hover slide-reveal on this card too, right
     before the actual open/close handler takes over — the same
     conflicting-motion problem the touch-drag skip in home-v2.js's
     cardAtTouch fixes, just via :hover instead of .is-touched. This
     card's own reveal is entirely click/is-open driven. */
  .v2-card.v-accordion:hover .v2-card-desc,
  .v2-card.is-static:hover .v2-card-desc { opacity: 0.4; }
  .v2-card.v-accordion:hover .v2-card-shot,
  .v2-card.is-static:hover .v2-card-shot { transform: none; }
  /* "First level" — a tap's worth of reveal (text readable, image nudged
     down over it) short of the full stats expand, using the same
     translateY slide/opacity brighten every other card gets from
     :hover. Two more taps from here (either the arrow, or dragging the
     image down — see home-v2.js) open the stats; is-open's own rules
     below take over from these once that happens. */
  .v2-card.v-accordion.is-peeked .v2-card-desc,
  .v2-card.is-static.is-peeked .v2-card-desc { opacity: 1; }
  .v2-card.v-accordion.is-peeked .v2-card-shot,
  .v2-card.is-static.is-peeked .v2-card-shot { transform: translateY(var(--shot-open-y, 120px)); }
  /* Same slide timing as the accordion cards, for the ones that only
     ever peek (locked cards and the like). */
  .v2-card.is-static .v2-card-shot { transition: transform 600ms cubic-bezier(0.65, 0, 0.35, 1); }
  /* Same chevron icon as every other card, just rotated: pointing down
     at rest (this card expands downward, not sideways) and flipped to
     point up once open. */
  .v2-card.v-accordion .v2-card-arrow {
    transition: transform 0.3s var(--liquid-ease);
    transform: rotate(90deg);
  }
  .v2-card.v-accordion.is-open .v2-card-arrow { transform: rotate(-90deg); }
  /* Present in the HTML for the expanded layout below but invisible at
     rest — the side-panel design (every other card, and this card's own
     desktop/tablet state) has no divider at all. */
  .v2-card.v-accordion .v2-card-divider { display: none; }

  .v2-card.v-accordion.is-open {
    width: var(--v2-card-w); height: auto; /* width never changes for these — they grow downward only */
    flex-direction: column; align-items: stretch;
  }
  /* Unboxes .v2-card-main/-expand so their own children (title, desc,
     divider, stats, screenshot) become direct flex items of .v2-card
     itself — the only way to freely reorder across what's otherwise two
     separate nested flex containers without changing the HTML. */
  .v2-card.v-accordion.is-open .v2-card-main,
  .v2-card.v-accordion.is-open .v2-card-expand { display: contents; }

  /* The line sits at top:60px (absolute) at rest and when peeked; once
     open it flows after the title instead. Giving the title a 60px box
     (and the line no extra top padding) puts the first line at exactly
     the same y in both states, so it doesn't hop when the card opens. */
  .v2-card.v-accordion.is-open .v2-card-title { order: 1; box-sizing: border-box; min-height: 60px; }
  .v2-card.v-accordion.is-open .v2-card-desc {
    order: 2; position: static; opacity: 1; padding: 0 23px;
  }
  /* The arrow itself is the expand/collapse affordance now — the
     separate × button (needed for the side-panel's own centered-content
     layout) would just be a second, redundant control here. */
  .v2-card.v-accordion.is-open .v2-card-close { display: none; }
  .v2-card.v-accordion.is-open .v2-card-divider {
    order: 3; display: block; height: 1px; margin: 18px 23px 0;
    background: rgba(255,255,255,0.18);
  }
  .v2-card.on-light.v-accordion.is-open .v2-card-divider,
  .v2-card.duocon.v-accordion.is-open .v2-card-divider { background: rgba(0,0,0,0.12); }
  .v2-card.v-accordion.is-open .v2-stat {
    /* margin-top (not gap) for the space between rows — .v2-card-expand's
       own flex gap stopped applying once display:contents took it out of
       the flex-container business, so each stat has to carry its own. */
    order: 4; flex-direction: row; align-items: baseline; gap: 10px;
    white-space: normal; padding: 0 23px; margin-top: 12px;
  }
  .v2-card.v-accordion.is-open .v2-stat:first-of-type { margin-top: 18px; }
  .v2-card.v-accordion.is-open .v2-stat-num { font-size: 26px; }
  /* Cards with no shareable stats yet (pay with a link) show this
     instead — same order/spacing slot a first stat would take. */
  .v2-card.v-accordion.is-open .v2-card-soon,
  .v2-card.v-accordion.is-open .v2-card-note { order: 4; padding: 0 23px; margin-top: 18px; }
  .v2-card.v-accordion.is-open .v2-card-note + .v2-card-note { margin-top: 6px; }
  .v2-card.v-accordion.is-open .v2-card-link { order: 5; position: static; align-self: flex-end; margin: 20px 23px 0; }
  /* flex:0 0 auto (the base rule, for the side-panel's own fixed-width
     column) never shrinks, so a long label like "component library
     created for expansion" just ran off the card's right edge instead of
     wrapping — flex:1 lets it take the row's remaining width and wrap
     within it instead. */
  .v2-card.v-accordion.is-open .v2-stat-label { flex: 1 1 auto; min-width: 0; }
  /* Stays position:absolute in every state (rest, peeked, open) instead
     of switching to static/in-flow when open — a position change can't
     itself be animated, so keeping one consistent positioning mode
     throughout means the image can just slide smoothly (a plain
     transform transition) between however far down each state pushes
     it, opening *and* closing, with no jump and no separate fade
     needed. home-v2.js measures how far "below the stats" is per open
     and writes it to --shot-full-open-y; card height is computed to
     match so there's no gap (and thus no visible strip of the card's
     own dark background) below the image's own square-cut bottom
     corners, matching every other card's top-only rounding. */
  .v2-card.v-accordion.is-open .v2-card-shot {
    transform: translateY(var(--shot-full-open-y, 400px));
  }
  /* Overrides the base 1s transform transition (tuned for the
     side-panel cards' quicker reveal) so the image's slide and the
     card's own height change move at exactly the same pace — the
     "image moves down slower, in sync with the reveal" this card
     wanted, rather than the two drifting out of step. Kept in sync by
     hand with ACCORDION_MS/ACCORDION_EASE in home-v2.js. */
  .v2-card.v-accordion .v2-card-shot {
    transition: transform 600ms cubic-bezier(0.65, 0, 0.35, 1);
  }
}

/* ---------- CASE-STUDY ICONS: OPTIONAL LOCK ----------
   Off for now: every "dive deeper" link shows. To lock the site again,
   add class="site-locked" to <html> in index.html and remove `hidden`
   from the footer's .dive-lock button; links then stay hidden until
   unlocked from the footer (dive-lock.js), except .is-public (DuoCon). */
html.site-locked:not(.dive-unlocked) .v2-card-link:not(.is-public) { display: none; }

/* ---------- ROW DOTS (phones) ----------
   Built by home-v2.js under each company row; one per card, the active
   one in the accent color. Phone width only, where one card fills most
   of the screen and the rest of the row is easy to miss. */
.v2-row-dots { display: none; }
@media (max-width: 640px) {
  .v2-row-dots { display: flex; justify-content: center; gap: 4px; padding: 14px 0 0; }
  .v2-row-dot {
    width: 22px; height: 22px; padding: 0; border: 0; background: none; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    -webkit-tap-highlight-color: transparent;
  }
  .v2-row-dot::before {
    content: ''; width: 7px; height: 7px; border-radius: 50%; background: #cfcfcb;
    transition: background-color 0.3s ease, width 0.3s ease;
  }
  .v2-row-dot.is-active::before { background: var(--accent); width: 18px; border-radius: 4px; }
  .v2-row-dot:focus { outline: none; }
  .v2-row-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 6px; }
}

/* A small bracketed status note at the end of a card's line, e.g.
   "[current experiment]": same size, a touch quieter than the line. */
.v2-card-tag { white-space: nowrap; opacity: 0.6; }

/* Small project name under a stat's label (Remitly Business card), so
   each number says which piece of work it came from. */
.v2-stat-project {
  display: block; margin-top: 3px; font-size: 10.5px; line-height: 1.3;
  letter-spacing: 0.04em; text-transform: uppercase; opacity: 0.7;
}

/* Remitly Business stats: numbers of very different widths ("25,000+"
   vs "~2×") left each label starting somewhere else. On the desktop side
   panel the numbers get a shared column (sized to the widest), so every
   label lines up, and labels may wrap within what's left. */
@media (min-width: 641px) {
  .v2-card.remitly .v2-stat { white-space: normal; }
  .v2-card.remitly .v2-stat-num { min-width: 136px; }
  .v2-card.remitly .v2-stat-label { flex: 1 1 auto; min-width: 0; }
}

/* ---------- PHONES: REMITLY STATS IN A FIXED-HEIGHT CARD ----------
   An opened card no longer grows (the screenshot is cut off by its
   bottom edge, like on the first tap), so Remitly's four labeled stats
   have to fit in the card's own height. A tighter rhythm here, for that
   card only; the other cards' stats are shorter and fit as they are. */
@media (max-width: 640px) {
  .v2-card.remitly.v-accordion.is-open .v2-stat { margin-top: 7px; gap: 8px; }
  .v2-card.remitly.v-accordion.is-open .v2-stat:first-of-type { margin-top: 14px; }
  .v2-card.remitly.v-accordion.is-open .v2-stat-num { font-size: 21px; }
  .v2-card.remitly.v-accordion.is-open .v2-stat-label { font-size: 12px; line-height: 1.3; }
  .v2-card.remitly .v2-stat-project { font-size: 9.5px; margin-top: 2px; }
}

/* Same idea for every card: a slightly tighter stat rhythm so the stats
   and the "dive deeper" link sit comfortably inside the fixed card height
   on the narrowest phones. */
@media (max-width: 640px) {
  .v2-card.v-accordion.is-open .v2-stat { margin-top: 9px; }
  .v2-card.v-accordion.is-open .v2-stat:first-of-type { margin-top: 16px; }
}

/* ---------- PHONES: ONE LEFT MARGIN INSIDE A CARD ----------
   The title carried 21px of side padding while the line, divider,
   stats and link all use 23px, so the title sat 2px off the rest. Now
   everything in an accordion card starts at the same 23px. And Remitly's
   stat numbers share a column as wide as the longest ("25,000+"), so
   their labels all start at the same x too, instead of each right after
   its own number. */
@media (max-width: 640px) {
  .v2-card.v-accordion .v2-card-title { padding-left: 23px; padding-right: 23px; }
  .v2-card.remitly.v-accordion.is-open .v2-stat-num { min-width: 90px; }
  /* The other stat cards get the same treatment: one number column wide
     enough for the longest number, so every label starts at the same x. */
  .v2-card.streak.v-accordion.is-open .v2-stat-num,
  .v2-card.duocon.v-accordion.is-open .v2-stat-num,
  .v2-card.newsfeed.v-accordion.is-open .v2-stat-num { min-width: 96px; }
}
