/* ============================================================
   TOKENS
   ============================================================ */
:root {
  --bg: #0A0908;
  --bg-2: #14110E;
  --surface: #1E1A16;
  --surface-2: #2A2520;
  --ink: #F4F0E8;
  --ink-2: #B7B0A5;
  --ink-3: #6F675E;
  --line: #2A2520;
  --line-2: #3A3530;
  /* Vermilion. Base #B23A2F maps to old paper-bg context; on dark we lift
     slightly to #D4493A for buttons/glow so the punch survives against the
     warm off-black. Deep variant is reserved for shadows and pressed states
     only — it eats itself if used as a gradient stop on dark bg. */
  --vermilion: #B23A2F;
  --vermilion-bright: #D4493A;
  --vermilion-deep: #8A2A22;
  --vermilion-glow: rgba(212, 73, 58, 0.32);
  --gradient-warm: linear-gradient(180deg, #D4493A 0%, #B23A2F 100%);
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 20px;
  --radius-xl: 28px;
  --shadow-md: 0 8px 32px rgba(0, 0, 0, 0.4);
  --shadow-lg: 0 24px 64px rgba(0, 0, 0, 0.5);
  --shadow-glow: 0 8px 40px var(--vermilion-glow);
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-snap: cubic-bezier(0.4, 0, 0.2, 1);
}

/* ============================================================
   BASE — mobile-first
   ============================================================ */
* { box-sizing: border-box; margin: 0; padding: 0; }
*::selection { background: var(--vermilion-bright); color: var(--ink); }

html { scroll-behavior: smooth; }
body {
  background: var(--bg);
  color: var(--ink);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 17px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  /* Clearance for the mobile sticky CTA. Removed at ≥720px below. */
  padding-bottom: 88px;
}

/* Typography */
h1, h2, h3, h4 {
  font-family: 'Inter', sans-serif;
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 1.02;
}
h1 { font-size: clamp(48px, 9vw, 120px); font-weight: 900; letter-spacing: -0.05em; }
h2 { font-size: clamp(36px, 5.5vw, 72px); font-weight: 800; letter-spacing: -0.04em; line-height: 1.04; }
h3 { font-size: clamp(24px, 3vw, 32px); font-weight: 700; letter-spacing: -0.02em; line-height: 1.15; }
h4 { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; }

p { color: var(--ink-2); }
a { color: inherit; text-decoration: none; }

/* Container */
.container { max-width: 1280px; margin: 0 auto; padding: 0 24px; }

/* Header */
.header {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  backdrop-filter: saturate(140%) blur(16px);
  -webkit-backdrop-filter: saturate(140%) blur(16px);
  border-bottom: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
}
.header__inner {
  display: flex; align-items: center; gap: 14px;
  height: 64px;
}
.brand {
  font-weight: 800; font-size: 20px;
  letter-spacing: -0.03em;
  display: flex; align-items: center; gap: 10px;
  flex: 0 0 auto;
}
.brand__dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--vermilion-bright);
  box-shadow: 0 0 12px var(--vermilion-glow);
}
/* Right cluster — counter chip (when signed in) + nav links group
   together and anchor to the container edge via auto-margin. Account
   stays glued to the right; chip width changes only shrink the gap
   to the brand, never the spacing between Studio and Account. */
.header__cluster {
  display: flex; align-items: center; gap: 14px;
  margin-left: auto;
  flex: 0 0 auto;
}
.header__nav {
  display: flex; align-items: center; gap: 14px;
  flex: 0 0 auto;
}
/* Narrow phones (iPhone SE, mini): drop the "Studio" link — the
   brand mark already navigates to /studio when signed in, so it
   isn't lost. Frees ~60px so the counter chip + Account fit without
   the chip having to shrink (which clipped the "+N" badge past the
   pill's rounded right edge). */
@media (max-width: 480px) {
  .header__nav { gap: 10px; }
  .nav-link[href="/studio"] { display: none; }
}
@media (min-width: 720px) {
  .header__nav { gap: 20px; }
}
.nav-link {
  color: var(--ink-2);
  font-size: 14px; font-weight: 500;
  /* 44px iOS/Android touch target. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  padding: 0 4px;
  transition: color 180ms var(--ease-snap);
}
.nav-link:hover { color: var(--ink); }

/* Buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: inherit; font-size: 15px; font-weight: 600;
  letter-spacing: -0.01em;
  padding: 14px 22px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform 120ms var(--ease-snap),
              background 180ms var(--ease-snap),
              color 180ms var(--ease-snap),
              box-shadow 240ms var(--ease-snap);
  white-space: nowrap;
}
.btn:active { transform: scale(0.98); }
/* WCAG AA: ink (#F4F0E8) on vermilion-bright (#D4493A) = 3.88:1 — fails
   normal-text 4.5:1 bar. Use base vermilion (#B23A2F) instead → 5.22:1
   passes AA. The "lifted on dark" punch still reads because the glow
   uses vermilion-bright-derived rgba, and the hover state shifts to
   vermilion-bright for the interactive lift. */
.btn--accent {
  background: var(--vermilion);
  color: var(--ink);
  box-shadow: var(--shadow-glow);
}
.btn--accent:hover {
  background: var(--vermilion-bright);
  box-shadow: 0 12px 56px var(--vermilion-glow);
}
.btn--xl { padding: 22px 36px; font-size: 17px; border-radius: 12px; }

.text-link {
  color: var(--ink-2);
  font-size: 14px; font-weight: 500;
  display: inline-flex; align-items: center; gap: 6px;
  transition: color 180ms var(--ease-snap);
}
.text-link:hover { color: var(--vermilion-bright); }
.text-link::after { content: "→"; opacity: 0.6; transition: transform 180ms var(--ease-snap); }
.text-link:hover::after { transform: translateX(3px); opacity: 1; }

/* Hero */
.hero {
  position: relative;
  overflow: hidden;
}
.hero::before {
  content: "";
  position: absolute;
  top: 10%; right: 35%;
  width: 800px; height: 600px;
  background: radial-gradient(ellipse at center, var(--vermilion-glow) 0%, transparent 60%);
  opacity: 0.16;
  pointer-events: none;
  z-index: 0;
  filter: blur(60px);
}
.hero__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  align-items: stretch;
  position: relative; z-index: 1;
}
.hero__copy {
  max-width: 640px;
  /* Tight mobile top padding so the H1 reads above the iPhone notch zone
     without scroll. Desktop padding overridden below. */
  padding: 32px 0 16px;
  display: flex; flex-direction: column; justify-content: center;
}
.hero h1 {
  background: linear-gradient(180deg, var(--ink) 0%, color-mix(in srgb, var(--ink) 75%, var(--bg)) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  margin-bottom: 24px;
}
.hero h1 em {
  font-style: normal;
  /* Solid color, NOT a gradient with background-clip:text — that combo
     leaves sub-pixel gaps at multi-line fragment edges in Webkit and
     the h1's white fill bleeds through (the `e` of `like` got a white
     right edge). `-webkit-text-fill-color` overrides h1's inherited
     transparent. */
  color: var(--vermilion-bright);
  -webkit-text-fill-color: var(--vermilion-bright);
}
.hero__lead {
  font-size: clamp(18px, 1.8vw, 22px);
  color: var(--ink-2);
  margin-bottom: 40px;
  max-width: 52ch;
  line-height: 1.45;
}
.hero__cta {
  display: flex; flex-wrap: wrap; gap: 20px;
  align-items: center;
  margin-bottom: 8px;
}
/* Form wrappers around POST-to-/subscribe CTAs (footer-cta, sticky-cta;
   the in-card form is wrapped by .pricing__form inside the pricing card).
   Inline so they don't disrupt the surrounding flex/text flow; sticky-cta
   uses `display: contents` so the button remains a direct flex-child of
   .sticky-cta. */
.footer-cta__form { display: inline; margin: 0; }
.sticky-cta__form { display: contents; }
/* Hero visual. Mobile order matches source (H1/lead/CTA first, photo
   second). Mobile/tablet: full-bleed 4:5 via negative margins, with a
   `<picture>` source swap to a face-focused mobile crop (see template).
   Desktop (≥1024px) splits into the two-column grid and the photo bleeds
   past the right edge — see desktop overrides below. */
.hero__visual {
  position: relative;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  aspect-ratio: 4 / 5;
  background: var(--bg);
  overflow: hidden;
}
.hero__visual img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  /* Mobile/tablet: fade the image into the page bg via alpha mask. Mask
     interpolates alpha only (no color), avoiding the sub-pixel "brighter
     line" artifact that a color-gradient overlay produces when its endpoint
     alpha drops below 100% on the first/last row. Desktop disables this
     mask and uses a 90deg left-fade ::after overlay instead. */
  -webkit-mask-image: linear-gradient(180deg,
    rgba(0,0,0,0) 0%,
    rgba(0,0,0,1) 8%,
    rgba(0,0,0,1) 92%,
    rgba(0,0,0,0) 100%
  );
  mask-image: linear-gradient(180deg,
    rgba(0,0,0,0) 0%,
    rgba(0,0,0,1) 8%,
    rgba(0,0,0,1) 92%,
    rgba(0,0,0,0) 100%
  );
}
/* Before-shot inset: small reference selfie pinned to the lower-left
   of the hero photo (Aragon canonical pattern — proximity does the
   storytelling, no arrow needed). Mono label uses the same dot vocab
   as the brand mark. */
.hero__before {
  position: absolute;
  left: 20px; bottom: 20px;
  width: 136px; height: 136px;
  margin: 0;
  border-radius: 14px;
  overflow: visible;
  z-index: 3;
  filter: drop-shadow(0 18px 40px rgba(0, 0, 0, 0.62));
}
.hero__before img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: 14px;
  /* 2px bright-bg ring + 1px vermilion tint keeps the inset reading as a
     deliberate proof element, not a decorative thumbnail. */
  border: 2px solid color-mix(in srgb, var(--bg) 80%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--vermilion-bright) 38%, transparent);
  display: block;
}
.hero__before-label {
  position: absolute;
  top: -24px; left: 2px;
  z-index: 1;
  background: transparent;
  color: rgba(255, 255, 255, 0.96);
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  padding: 0;
  line-height: 1;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.75);
  display: inline-flex; align-items: center; gap: 8px;
}
.hero__before-label::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--vermilion-bright);
  box-shadow: 0 0 10px var(--vermilion-glow);
}

@media (min-width: 768px) {
  .hero__before { width: 168px; height: 168px; left: 24px; bottom: 24px; }
  .hero__before-label { font-size: 13px; top: -26px; }
}
@media (min-width: 1024px) {
  /* Hero photo bleeds past the fold on desktop, so anchoring to `bottom`
     of .hero__visual lands the inset off-screen. Anchor to the bottom
     of the visible viewport instead — 285px = header(~65) + inset(200)
     + 20px breathing room. */
  .hero__before {
    width: 200px; height: 200px;
    left: 28px; bottom: auto;
    top: calc(100vh - 285px);
  }
  .hero__before-label { font-size: 14px; top: -28px; letter-spacing: 0.22em; }
}

/* Section */
.section { padding: 32px 0; position: relative; }
.section__head { max-width: 720px; margin: 0 auto 32px; text-align: center; }
.section__head--left { margin: 0 0 24px; text-align: left; max-width: 640px; }
.section h2 { margin-bottom: 20px; }
.section__lead {
  font-size: clamp(17px, 1.5vw, 20px);
  color: var(--ink-2);
  line-height: 1.5;
}

/* Bento spread rows.
   Mobile base: 2-col × 3-row grid; the hero photo of each row spans both
   columns at a varying row position so the eye sweep is vertical
   (top → middle → bottom). Desktop override turns it into a 4-col × 2-row
   horizontal bento with hero L/C/R diagonal sweep. */
.spread-tile {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--surface);
  cursor: pointer;
  transition: transform 320ms var(--ease-soft);
}
.spread-tile:hover { transform: scale(1.02); }
.spread-tile:hover img { transform: scale(1.04); }
.spread-tile img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 480ms var(--ease-soft);
  /* faces park top-third so portrait crops don't cut chins/foreheads */
  object-position: center 25%;
}
/* Per-photo focal points where the default `center 25%` doesn't fit the
   source — mostly for mobile BIG tiles (1:1 square crop from 9:16 portrait)
   plus the two small tiles whose subject sits unusually close to the frame
   edge. Keyed against the filename slug (decoupled from slot label). */
.spread-tile img[src*="off-duty"] { object-position: center 15%; }
.spread-tile img[src*="night-out"] { object-position: center top; }
.spread-tile img[src*="game-night"] { object-position: center 5%; }
.spread-tile img[src*="beauty"] { object-position: center 7%; }
.spread-tile img[src*="bridal"] { object-position: center top; }
.spread-tile__label {
  position: absolute; bottom: 12px; left: 12px;
  background: color-mix(in srgb, var(--bg) 70%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  padding: 5px 11px;
  border-radius: 999px;
  font-size: 12px; font-weight: 600;
  color: var(--ink);
  z-index: 2;
}

.spread-row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(3, auto);
  grid-auto-flow: row dense;
  gap: 8px;
}
.spread-row > .spread-tile { aspect-ratio: 3 / 4; }
.spread-row--hero-left > .spread-tile:nth-child(1) {
  grid-column: 1 / span 2; grid-row: 1; aspect-ratio: 1 / 1;
}
.spread-row--hero-center > .spread-tile:nth-child(3) {
  grid-column: 1 / span 2; grid-row: 2; aspect-ratio: 1 / 1;
}
.spread-row--hero-right > .spread-tile:nth-child(5) {
  grid-column: 1 / span 2; grid-row: 3; aspect-ratio: 1 / 1;
}

/* Between-row pull statement. Quiet but premium: large display type,
   tight breathing room so it doesn't read as a section break. Vermilion
   dash sits close under the text — reads as a confident punctuation
   mark, not a section divider. */
.gallery-pull {
  padding: 24px 16px;
  text-align: center;
}
.gallery-pull p {
  font-size: clamp(20px, 2.2vw, 28px);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.3;
  color: var(--ink);
  margin: 0 auto;
}
.gallery-pull p::after {
  content: "";
  display: block;
  width: 24px;
  height: 2px;
  background: var(--vermilion-bright);
  border-radius: 999px;
  margin: 10px auto 0;
}
@media (min-width: 768px) {
  .gallery-pull { padding: 32px 16px; }
}
@media (min-width: 1024px) {
  .gallery-pull { padding: 40px 16px; }
}

/* Pricing */
.pricing-section {
  background: linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%);
  /* Pull the hash-scroll target up so the H2 lands ~12px under the
     64px sticky header, not 120px below it. The section's own padding-top
     (.section is 56px) would otherwise push the heading far down the
     screen post-jump. Section's empty padding-top simply gets cropped
     above the viewport when arriving via #pricing — fine, since the
     visual rhythm only matters when the section is reached by natural
     scroll, not anchor-jump. */
  scroll-margin-top: 20px;
}
.pricing {
  background: linear-gradient(180deg, var(--surface) 0%, var(--bg-2) 100%);
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
  border-radius: var(--radius-lg);
  padding: 32px;
  max-width: 520px;
  margin: 0 auto;
  position: relative;
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}
.pricing::before {
  content: "";
  position: absolute;
  top: -100px; right: -100px;
  width: 400px; height: 400px;
  background: radial-gradient(circle, var(--vermilion-glow), transparent 70%);
  pointer-events: none;
}
.pricing__head { position: relative; z-index: 1; margin-bottom: 32px; }
.pricing__tier {
  display: inline-block;
  background: color-mix(in srgb, var(--vermilion-bright) 16%, transparent);
  color: var(--vermilion-bright);
  font-size: 12px; font-weight: 700;
  padding: 6px 12px;
  border-radius: 999px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: 20px;
}
.pricing__price {
  display: flex; align-items: baseline; gap: 8px;
  margin-bottom: 8px;
}
.pricing__price-num {
  font-size: clamp(56px, 7vw, 72px); font-weight: 900;
  letter-spacing: -0.05em;
  color: var(--ink);
}
.pricing__price-unit {
  font-size: 17px; color: var(--ink-2); font-weight: 500;
}
.pricing__finals { color: var(--ink-2); font-size: 15px; margin-bottom: 28px; }
.pricing__features {
  list-style: none;
  display: flex; flex-direction: column; gap: 14px;
  padding: 28px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  margin-bottom: 28px;
  position: relative; z-index: 1;
}
/* No flex-wrap: a bare text node alongside the checkmark would otherwise
   wrap to a new flex row at narrow widths, leaving the checkmark alone on
   top. Without flex-wrap, the text node shrinks (anonymous flex item,
   default flex: 0 1 auto) and breaks internally as text. The strong+span
   variant on /style/:slug still fits because both children have explicit
   flex behavior (nowrap+shrink-0 for strong, flex:1+min-width:0 for span). */
.pricing__features li {
  display: flex; align-items: flex-start; gap: 8px;
  color: var(--ink); font-size: 15px; line-height: 1.45;
}
/* <strong> is a flex item alongside the checkmark ::before and the
   description <span>. Without nowrap+shrink-0 the strong collapses to
   its min-content width on narrow viewports and the trailing description
   wraps under it, producing a 3-row visual mess. */
.pricing__features li > strong { flex-shrink: 0; white-space: nowrap; }
.pricing__features li > span { color: var(--ink-2); flex: 1; min-width: 0; }
.pricing__features li::before {
  content: "";
  flex-shrink: 0;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: var(--vermilion-bright);
  margin-top: 2px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M2.5 6.2L4.5 8.2L9.5 3.2' stroke='%23F4F0E8' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-position: center;
  background-repeat: no-repeat;
}
.pricing__cta { width: 100%; position: relative; z-index: 1; }

/* FAQ */
.faq { max-width: 720px; margin: 0 auto; list-style: none; }
.faq__item { border-top: 1px solid var(--line); }
.faq__item:last-child { border-bottom: 1px solid var(--line); }
.faq details { padding: 12px 0; }
.faq summary {
  display: flex; justify-content: space-between; align-items: center;
  font-size: clamp(18px, 1.7vw, 20px); font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
  gap: 24px;
  /* 44px touch target. */
  min-height: 44px;
  padding: 12px 0;
  transition: color 180ms var(--ease-snap);
}
.faq summary:hover { color: var(--vermilion-bright); }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "+";
  font-size: 32px;
  line-height: 1;
  color: var(--vermilion-bright);
  font-weight: 400;
  transition: transform 320ms cubic-bezier(0.34, 1.4, 0.64, 1);
  flex-shrink: 0;
}
details[open] summary::after { transform: rotate(45deg); }
.faq__answer {
  margin-top: 16px;
  color: var(--ink-2);
  font-size: 16px;
  line-height: 1.6;
  max-width: 62ch;
}

/* Smooth open/close animation for FAQ accordions. Modern Chromium/Safari
   gain a height transition on details-content; Firefox and older engines
   fall back to default snap-open (acceptable degradation). */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .faq details::details-content {
    height: 0;
    overflow: hidden;
    opacity: 0;
    transition:
      height 320ms cubic-bezier(0.4, 0, 0.2, 1),
      opacity 200ms ease-out,
      content-visibility 320ms;
    transition-behavior: allow-discrete;
  }
  .faq details[open]::details-content {
    height: auto;
    opacity: 1;
  }
}

/* Footer CTA — photo-backed slab with vermilion overlay. */
.footer-cta {
  position: relative;
  padding: 72px 0;
  text-align: center;
  overflow: hidden;
  min-height: 360px;
  display: flex; align-items: center;
}
.footer-cta__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.footer-cta__bg img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  /* Brightness 0.4 so the vermilion overlay below can dominate; brighter
     than that and the slab reads as brown-muddy instead of branded. */
  filter: brightness(0.4);
}
.footer-cta__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Overlay bright→base (no deep stop), high alpha pushes vermilion above
     the photo neutrals. Reads as branded slab with photo texture, not
     photo with warm haze. */
  background:
    linear-gradient(180deg, var(--bg) 0%, transparent 25%, transparent 70%, var(--bg) 100%),
    linear-gradient(135deg, rgba(212, 73, 58, 0.55) 0%, rgba(178, 58, 47, 0.7) 100%);
  pointer-events: none;
}
.footer-cta__inner { position: relative; z-index: 1; width: 100%; }
.footer-cta h2 {
  color: var(--ink);
  margin-bottom: 16px;
  text-shadow: 0 4px 24px rgba(0,0,0,0.4);
}
.footer-cta__lead {
  color: var(--ink); opacity: 0.92;
  font-size: clamp(17px, 1.6vw, 20px);
  margin-bottom: 32px;
  max-width: 56ch; margin-left: auto; margin-right: auto;
  text-shadow: 0 2px 12px rgba(0,0,0,0.4);
}
.footer-cta .btn--accent {
  background: var(--ink);
  color: var(--vermilion-deep);
  box-shadow: 0 12px 56px rgba(0, 0, 0, 0.4);
}
.footer-cta .btn--accent:hover {
  background: var(--vermilion-bright);
  color: var(--ink);
}

/* Footer. WCAG AA: --ink-3 (#6F675E) on --bg (#0A0908) = 3.57:1 — fails
   normal-text bar at 13px. Use --ink-2 (#B7B0A5) for 9.17:1, well clear
   of 4.5:1. */
.footer { border-top: 1px solid var(--line); padding: 48px 0; }
.footer__inner {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 24px;
  color: var(--ink-2); font-size: 13px;
}
.footer a { color: var(--ink-2); transition: color 180ms var(--ease-snap); }
.footer a:hover { color: var(--ink); }

/* Sticky mobile CTA — visible on mobile only (hidden at ≥720px below).
   JS at end-of-body adds .is-hidden when the pricing-card CTA enters the
   viewport, so two CTAs never compete within the same 200px fold. */
.sticky-cta {
  display: flex;
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 60;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(150%) blur(20px);
  -webkit-backdrop-filter: saturate(150%) blur(20px);
  border-top: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  gap: 12px;
  align-items: center;
  transition: transform 220ms var(--ease-snap);
}
.sticky-cta.is-hidden { transform: translateY(100%); }
.sticky-cta__copy { flex: 1; font-size: 12px; line-height: 1.3; color: var(--ink-2); }
.sticky-cta__copy strong { color: var(--ink); font-weight: 700; }
.sticky-cta .btn { flex-shrink: 0; padding: 14px 18px; font-size: 14px; }

/* ============================================================
   TABLET+ (≥720px)
   ============================================================ */
@media (min-width: 720px) {
  body { padding-bottom: 0; }
  .container { padding: 0 40px; }
  .section { padding: 72px 0; }
  .sticky-cta { display: none; }
}

/* ============================================================
   DESKTOP+ (≥1024px)
   ============================================================ */
@media (min-width: 1024px) {
  .container { padding: 0 56px; }

  /* Hero — split grid, photo bleeds past the right edge. */
  .hero__grid {
    grid-template-columns: 1fr 1.15fr;
    min-height: 720px;
    gap: 60px;
  }
  .hero__copy { padding: 80px 0; }
  .hero__visual {
    aspect-ratio: auto;
    height: auto;
    width: 100%;
    margin-left: 0;
    margin-right: calc(50% - 50vw);
    border-radius: var(--radius-xl) 0 0 var(--radius-xl);
  }
  .hero__visual img {
    -webkit-mask-image: none;
    mask-image: none;
  }
  /* Desktop seam between the text column and the bleed-right photo.
     Mobile/tablet doesn't define ::after — the IMG's mask-image handles
     edge fades there. */
  .hero__visual::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(90deg, var(--bg) 0%, transparent 12%);
  }

  /* Bento — 4-col × 2-row, hero L/C/R diagonal sweep. */
  .spread-row {
    grid-template-columns: repeat(4, 1fr);
    grid-template-rows: repeat(2, 1fr);
    grid-auto-flow: row;
    gap: 12px;
    aspect-ratio: 3 / 2;
  }
  .spread-row > .spread-tile { aspect-ratio: auto; }

  .spread-row--hero-left > .spread-tile:nth-child(1) { grid-column: 1 / span 2; grid-row: 1 / span 2; aspect-ratio: auto; }
  .spread-row--hero-left > .spread-tile:nth-child(2) { grid-column: 3; grid-row: 1; }
  .spread-row--hero-left > .spread-tile:nth-child(3) { grid-column: 4; grid-row: 1; }
  .spread-row--hero-left > .spread-tile:nth-child(4) { grid-column: 3; grid-row: 2; }
  .spread-row--hero-left > .spread-tile:nth-child(5) { grid-column: 4; grid-row: 2; }

  .spread-row--hero-center > .spread-tile:nth-child(1) { grid-column: 1; grid-row: 1; }
  .spread-row--hero-center > .spread-tile:nth-child(2) { grid-column: 1; grid-row: 2; }
  .spread-row--hero-center > .spread-tile:nth-child(3) { grid-column: 2 / span 2; grid-row: 1 / span 2; aspect-ratio: auto; }
  .spread-row--hero-center > .spread-tile:nth-child(4) { grid-column: 4; grid-row: 1; }
  .spread-row--hero-center > .spread-tile:nth-child(5) { grid-column: 4; grid-row: 2; }

  .spread-row--hero-right > .spread-tile:nth-child(1) { grid-column: 1; grid-row: 1; }
  .spread-row--hero-right > .spread-tile:nth-child(2) { grid-column: 2; grid-row: 1; }
  .spread-row--hero-right > .spread-tile:nth-child(3) { grid-column: 1; grid-row: 2; }
  .spread-row--hero-right > .spread-tile:nth-child(4) { grid-column: 2; grid-row: 2; }
  .spread-row--hero-right > .spread-tile:nth-child(5) { grid-column: 3 / span 2; grid-row: 1 / span 2; aspect-ratio: auto; }
}

/* ============================================================
   SHARED COMPONENTS — used by /subscribe, /sign-in, /post-checkout,
   and the rest of the funnel pages. Single-column layouts centered
   within .container, with cards + form fields + status chips.
   ============================================================ */

/* Single-column funnel container — centers narrow content (forms,
   status, single-CTA cards) under the sticky header. */
.funnel {
  padding: 56px 0;
  display: flex; flex-direction: column; align-items: center;
  text-align: center;
}
.funnel__wrap { max-width: 520px; width: 100%; }
.funnel h1 { font-size: clamp(36px, 6vw, 56px); margin-bottom: 16px; }
.funnel__lead {
  font-size: clamp(17px, 1.6vw, 19px);
  color: var(--ink-2);
  line-height: 1.45;
  margin-bottom: 32px;
  max-width: 46ch;
  margin-left: auto; margin-right: auto;
}
.funnel__sub {
  color: var(--ink-3);
  font-size: 14px;
  line-height: 1.5;
  margin-top: 16px;
  max-width: 46ch;
  margin-left: auto; margin-right: auto;
}

/* Card chrome — used for the pricing block on /subscribe + the
   sign-in form wrapper. Same gradient as the landing's .pricing card
   but reusable. */
.card {
  background: linear-gradient(180deg, var(--surface) 0%, var(--bg-2) 100%);
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
  border-radius: var(--radius-lg);
  padding: 32px;
  text-align: left;
  position: relative;
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}
.card--glow::before {
  content: "";
  position: absolute;
  top: -100px; right: -100px;
  width: 400px; height: 400px;
  background: radial-gradient(circle, var(--vermilion-glow), transparent 70%);
  pointer-events: none;
  z-index: 0;
}
.card > * { position: relative; z-index: 1; }

/* Form input. Dark surface, soft border that brightens on focus.
   44px min-height for touch. Honors :user-invalid for inline form errors. */
.field-label {
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 8px;
}
.input {
  width: 100%;
  background: color-mix(in srgb, var(--bg) 60%, var(--surface));
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  color: var(--ink);
  font-family: inherit;
  font-size: 16px;
  padding: 14px 16px;
  min-height: 48px;
  transition: border-color 180ms var(--ease-snap),
              box-shadow 180ms var(--ease-snap);
}
.input::placeholder { color: var(--ink-3); }
.input:focus {
  outline: none;
  border-color: var(--vermilion-bright);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--vermilion-bright) 24%, transparent);
}
.input:user-invalid {
  border-color: var(--vermilion);
}

/* Status chip — vermilion-tinted block for inline page status
   (info/warning/error). Sits at the top of the funnel content under
   the H1, before the form/card. */
.status-chip {
  display: inline-flex; align-items: center; gap: 10px;
  font-size: 13px; font-weight: 500;
  padding: 10px 16px;
  border-radius: 999px;
  margin-bottom: 24px;
  border: 1px solid transparent;
}
.status-chip__dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.status-chip--info {
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  border-color: color-mix(in srgb, var(--ink) 14%, transparent);
  color: var(--ink-2);
}
.status-chip--info .status-chip__dot { background: var(--vermilion-bright); box-shadow: 0 0 8px var(--vermilion-glow); }
.status-chip--warning {
  background: color-mix(in srgb, #C49A3A 14%, transparent);
  border-color: color-mix(in srgb, #C49A3A 32%, transparent);
  color: #E6BC5E;
}
.status-chip--warning .status-chip__dot { background: #E6BC5E; }
.status-chip--error {
  background: color-mix(in srgb, var(--vermilion-bright) 14%, transparent);
  border-color: color-mix(in srgb, var(--vermilion-bright) 36%, transparent);
  color: var(--vermilion-bright);
}
.status-chip--error .status-chip__dot { background: var(--vermilion-bright); }

/* Secondary button variant (text-only, no fill). Used for "request a
   link" / "open email" actions where the primary lives on a card. */
.btn--ghost {
  background: transparent;
  color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 20%, transparent);
}
.btn--ghost:hover {
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  border-color: color-mix(in srgb, var(--ink) 32%, transparent);
}
.btn--block { width: 100%; }

/* Studio-log spinner (used by /post-checkout polling state). Mono
   font + animated bullet — quiet operational chrome that doesn't
   yell "loading". */
.studio-log {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin-top: 8px;
}
.studio-log__tick {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--vermilion-bright);
  box-shadow: 0 0 8px var(--vermilion-glow);
  animation: studio-log-pulse 1.6s ease-in-out infinite;
}
@keyframes studio-log-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.4; transform: scale(0.7); }
}

/* Inline metadata row used at the bottom of funnel cards — small
   mono helper text like "no password · link expires in 15 minutes". */
.funnel__meta {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-top: 16px;
}

/* Legal pages (/terms, /privacy) — long-form body copy with paragraph
   leads bolded. Mono eyebrow for "PRIVACY · DATE" wayfinding. */
.legal-eyebrow {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 16px;
}
.legal-body {
  display: flex; flex-direction: column;
  gap: 20px;
  color: var(--ink-2);
  font-size: 16px;
  line-height: 1.6;
  max-width: 64ch;
}
.legal-body p { margin: 0; }
.legal-body strong { color: var(--ink); font-weight: 700; }
.legal-body a {
  color: var(--vermilion-bright);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* Destructive action button — vermilion-deep base, hover lifts to
   vermilion-bright. Used by /account/delete confirm submit. */
.btn--danger {
  background: var(--vermilion-deep);
  color: var(--ink);
  border-color: var(--vermilion-deep);
}
.btn--danger:hover { background: var(--vermilion-bright); border-color: var(--vermilion-bright); }
.btn--danger:disabled,
.btn--danger[disabled] {
  background: color-mix(in srgb, var(--vermilion-deep) 50%, var(--bg));
  color: var(--ink-3);
  cursor: not-allowed;
}

/* Delete-profile checkbox row — visible checkbox + body copy on the
   same row, with the checkbox enlarged to a comfortable touch target. */
.delete-confirm {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 16px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--bg) 60%, var(--surface));
  margin-bottom: 20px;
  cursor: pointer;
}
.delete-confirm input[type="checkbox"] {
  width: 20px; height: 20px;
  margin-top: 1px;
  accent-color: var(--vermilion-bright);
  flex-shrink: 0;
}
.delete-confirm span {
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.5;
}

/* /history — flat photo archive with download-on-click tiles. */
.history-head {
  display: flex; flex-wrap: wrap; justify-content: space-between;
  align-items: flex-end; gap: 16px;
  margin-bottom: 32px;
}
.history-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
}
.history-frame {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-md);
  overflow: hidden;
  display: block;
  background: var(--surface);
  transition: transform 240ms var(--ease-soft);
}
.history-frame:hover { transform: scale(1.02); }
.history-frame img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  object-position: center 25%;
}
.history-frame__caption {
  position: absolute; bottom: 8px; left: 8px; right: 8px;
  display: flex; justify-content: space-between; align-items: center;
  gap: 8px;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  font-size: 12px; font-weight: 600;
  color: var(--ink);
  z-index: 2;
}
.history-frame__download {
  font-family: 'JetBrains Mono', monospace;
  font-size: 14px;
  color: var(--vermilion-bright);
  font-weight: 700;
}

@media (min-width: 720px) {
  .history-grid { grid-template-columns: repeat(3, 1fr); gap: 12px; }
}
@media (min-width: 1024px) {
  .history-grid { grid-template-columns: repeat(4, 1fr); gap: 16px; }
}

/* /style/:slug — hero photo + sample gallery + shoot CTA card. */
.style-detail__head {
  display: flex; flex-wrap: wrap; justify-content: space-between;
  align-items: flex-start; gap: 16px;
  margin-bottom: 32px;
}
.style-detail__hero {
  display: block;
  width: 100%;
  max-width: 640px;
  margin: 0 0 48px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface);
}
.style-detail__hero img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}
.style-detail__samples {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
  margin-bottom: 48px;
}
.style-detail__sample {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface);
}
.style-detail__sample img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  object-position: center 25%;
}
.style-detail__sample--placeholder {
  background: linear-gradient(135deg, var(--surface) 0%, var(--bg-2) 100%);
}
.proof-stub__number {
  position: absolute; bottom: 8px; left: 8px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

@media (min-width: 720px) {
  .style-detail__samples { grid-template-columns: repeat(3, 1fr); gap: 12px; }
}
@media (min-width: 1024px) {
  .style-detail__samples { grid-template-columns: repeat(4, 1fr); gap: 16px; }
}

/* ============================================================
   /onboarding/avatar — BIPA consent + sitter pick + reference photo.
   Single-column stack of cards; gender as 2-up radio cards; file
   upload as a click+drag dropzone with thumbnail preview. The form
   sits inside .funnel so it inherits the centered, narrow funnel
   container — but the card grid breaks left-aligned because forms
   read as forms, not as marketing centered prose.
   ============================================================ */
.onboarding-stack {
  display: flex;
  flex-direction: column;
  gap: 20px;
  text-align: left;
  margin-bottom: 16px;
}

/* Smaller card padding for stacked onboarding cards — three cards in
   a column at 32px padding feels chunky on mobile. */
.onboarding-stack .card {
  padding: 24px;
}

/* BIPA written consent — collapsed canonical text. The summary is a
   subtle, mono-tagged inline-link feel; the body opens to scrollable
   muted prose so it never blows up the card height. */
.bipa-details {
  margin-top: 16px;
  margin-bottom: 20px;
  border-top: 1px solid var(--line-2);
  padding-top: 16px;
}
.bipa-details summary {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
  cursor: pointer;
  list-style: none;
  padding: 6px 0;
  min-height: 32px;
  transition: color 180ms var(--ease-snap);
}
.bipa-details summary::-webkit-details-marker { display: none; }
.bipa-details summary::after {
  content: "+";
  font-size: 14px;
  color: var(--vermilion-bright);
  transition: transform 180ms var(--ease-snap);
}
.bipa-details[open] summary::after { transform: rotate(45deg); }
.bipa-details summary:hover { color: var(--ink); }
.bipa-details__body {
  display: flex; flex-direction: column; gap: 12px;
  margin-top: 12px;
  max-height: 280px;
  overflow-y: auto;
  padding-right: 8px;
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.6;
}
.bipa-details__body p { margin: 0; }

/* BIPA + age checkbox rows — visible checkbox + body copy on one
   row, large touch target, hover-bordered for affordance. Mirrors
   the .delete-confirm pattern but tuned for the BIPA stack (two
   stacked rows, no extra background — they sit inside the BIPA
   card which already has its surface). */
.consent-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--bg) 60%, var(--surface));
  margin-top: 12px;
  cursor: pointer;
  transition: border-color 180ms var(--ease-snap);
}
.consent-row:hover { border-color: color-mix(in srgb, var(--ink) 24%, transparent); }
.consent-row:has(input:checked) {
  border-color: color-mix(in srgb, var(--vermilion-bright) 50%, transparent);
  background: color-mix(in srgb, var(--vermilion-bright) 5%, var(--bg));
}
.consent-row input[type="checkbox"] {
  width: 20px; height: 20px;
  margin-top: 1px;
  accent-color: var(--vermilion-bright);
  flex-shrink: 0;
}
.consent-row span {
  color: var(--ink);
  font-size: 14px;
  line-height: 1.5;
}

/* Sitter pick — two side-by-side radio cards. The native input is
   visually-hidden but stays keyboard-tabbable; checked state drives
   the chrome via :has-of-checked-sibling. */
.gender-toggle__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
.gender-card {
  position: relative;
  display: block;
  cursor: pointer;
}
.gender-card input[type="radio"] {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0);
  border: 0;
}
.gender-card__chrome {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 18px 16px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--bg) 60%, var(--surface));
  text-align: center;
  transition: border-color 180ms var(--ease-snap),
              background 180ms var(--ease-snap),
              box-shadow 180ms var(--ease-snap);
}
.gender-card__chrome:hover { border-color: color-mix(in srgb, var(--ink) 24%, transparent); }
.gender-card input[type="radio"]:focus-visible + .gender-card__chrome {
  outline: none;
  border-color: var(--vermilion-bright);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--vermilion-bright) 24%, transparent);
}
.gender-card input[type="radio"]:checked + .gender-card__chrome {
  border-color: var(--vermilion-bright);
  background: color-mix(in srgb, var(--vermilion-bright) 10%, var(--bg));
  box-shadow: 0 0 0 1px var(--vermilion-bright) inset;
}
.gender-card__label {
  font-size: 17px;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.01em;
}
.gender-card__sub {
  font-size: 13px;
  color: var(--ink-3);
}
.gender-card input[type="radio"]:checked + .gender-card__chrome .gender-card__sub {
  color: var(--ink-2);
}

/* Reference-photo tips bullet list. Soft bullets so the eye scans the
   tip text, not the dot. */
.photo-tips {
  list-style: none;
  margin-top: 16px;
  margin-bottom: 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.photo-tips li {
  position: relative;
  padding-left: 20px;
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.5;
}
.photo-tips li::before {
  content: "";
  position: absolute;
  left: 4px; top: 8px;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--vermilion-bright);
  opacity: 0.8;
}

/* Photo dropzone — click-or-drop affordance with thumb preview. The
   native input is visually-hidden; the surrounding label is the click
   target. JS toggles --drag and --filled states. */
.photo-dropzone {
  position: relative;
  margin-top: 8px;
}
.photo-dropzone input[type="file"] {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0);
  border: 0;
}
.photo-dropzone__zone {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 6px;
  padding: 40px 24px;
  border: 1.5px dashed var(--line-2);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--bg) 60%, var(--surface));
  cursor: pointer;
  text-align: center;
  transition: border-color 180ms var(--ease-snap),
              background 180ms var(--ease-snap),
              transform 180ms var(--ease-snap);
}
.photo-dropzone__zone:hover {
  border-color: var(--vermilion-bright);
  background: color-mix(in srgb, var(--vermilion-bright) 5%, var(--bg));
}
.photo-dropzone__zone:focus-within {
  outline: none;
  border-color: var(--vermilion-bright);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--vermilion-bright) 24%, transparent);
}
.photo-dropzone__zone--drag {
  border-color: var(--vermilion-bright);
  background: color-mix(in srgb, var(--vermilion-bright) 10%, var(--bg));
  transform: scale(1.01);
}
.photo-dropzone__icon {
  display: flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--vermilion-bright) 16%, transparent);
  color: var(--vermilion-bright);
  font-size: 22px;
  font-weight: 400;
  line-height: 1;
}
.photo-dropzone__cta {
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  margin-top: 8px;
}
.photo-dropzone__hint {
  font-size: 13px;
  color: var(--ink-3);
}
.photo-dropzone__filled {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--bg) 60%, var(--surface));
}
/* Honor the `hidden` attribute on both states — CSS specificity of
   `.photo-dropzone__filled { display: flex }` would otherwise win over
   the UA default `[hidden] { display: none }`. The zone uses display:flex
   too and has the same hazard. Same hazard hits .photo-dropzone__error. */
.photo-dropzone__filled[hidden],
.photo-dropzone__zone[hidden],
.photo-dropzone__error[hidden],
.form-hint[hidden] { display: none; }
.photo-dropzone__thumb {
  width: 72px; height: 72px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--surface-2);
  flex-shrink: 0;
}
.photo-dropzone__meta {
  display: flex; flex-direction: column;
  gap: 6px;
  min-width: 0;
  flex: 1;
}
.photo-dropzone__name {
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.photo-dropzone__replace {
  align-self: flex-start;
  background: transparent;
  border: 0;
  padding: 0;
  font: inherit;
  cursor: pointer;
  font-size: 13px;
  color: var(--vermilion-bright);
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color 180ms var(--ease-snap);
}
.photo-dropzone__replace:hover { color: var(--ink); }
.photo-dropzone__error {
  margin-top: 10px;
  font-size: 13px;
  color: var(--vermilion-bright);
}
.photo-dropzone__technical {
  margin-top: 12px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Continue button — `.btn--incomplete` fades the button so the user
   knows something's still missing but the click still scrolls them
   to the first unfilled field (handled by JS). Once everything's
   ticked the class is removed and the button reads normal. */
.btn--incomplete {
  opacity: 0.5;
  box-shadow: none;
}
.btn--incomplete:hover {
  opacity: 0.7;
  box-shadow: none;
}
.form-hint {
  text-align: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-top: 12px;
}

@media (min-width: 720px) {
  .onboarding-stack .card { padding: 32px; }
  .photo-dropzone__zone { padding: 56px 32px; }
}

/* ============================================================
   SIGNED-IN CHROME — top-nav (desktop), live-banner (all sizes,
   conditional), bottom-tabs (mobile). Visibility is breakpoint-gated
   below; SSR keeps the elements in DOM so route changes don't depend
   on JS hydration.
   ============================================================ */
.top-nav {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  backdrop-filter: saturate(140%) blur(16px);
  -webkit-backdrop-filter: saturate(140%) blur(16px);
  border-bottom: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  display: none;
}
.top-nav__inner {
  display: flex; align-items: center; gap: 14px;
  height: 56px;
}
.top-nav__brand { flex: 0 0 auto; }
.top-nav__links {
  display: flex; align-items: center; gap: 20px;
  margin-left: auto;
  flex: 0 0 auto;
}
.top-nav__links .nav-link.is-active {
  color: var(--ink);
  position: relative;
}
.top-nav__links .nav-link.is-active::after {
  content: "";
  position: absolute;
  bottom: 8px; left: 4px; right: 4px;
  height: 2px;
  border-radius: 999px;
  background: var(--vermilion-bright);
}
@media (min-width: 720px) {
  .top-nav { display: block; }
}

/* Live-banner — iOS Live Activity-style chip↔card. Always present in the
   DOM (no display:none toggle) so the first shoot doesn't push content
   down by ~76px in a single frame. Two visual modes morphed via CSS:
     - .live-banner--idle: compact right-anchored chip (spark + remaining)
     - default: full-width card with title, sub, progress, chev
   The morph animates flex-basis, grid-template-columns, padding, gap on
   the banner, plus opacity + max-height on the text block — content
   below slides smoothly instead of snapping. The spark stays in the DOM
   in both modes so the studio fly-up always has a target to dock on.

   Host is a fixed overlay on both desktop (≥720px, under the 56px
   top-nav) and mobile (<720px, hugging safe-area-inset-top). z-index
   sits BELOW the top-nav (50) and bottom-tabs (60) but ABOVE content.
   Positioning lives in the per-viewport media queries below. */
.live-banner-host {
  z-index: 40;
  display: flex;
  justify-content: flex-end;
  /* Empty stretches of the host (visible only in idle, where the banner
     is a small right-anchored chip) shouldn't block clicks on content
     beneath. The banner itself re-enables pointer-events. */
  pointer-events: none;
}
.live-banner-host > * { pointer-events: auto; }
@media (min-width: 720px) {
  /* Desktop: fixed overlay under the 56px top-nav. Same rationale as the
     mobile fixed-overlay rule — the idle chip is small and right-
     aligned, so a 66px reserved row in flow forced the STUDIO kicker
     ~66px lower than it visually needed to be. With the host out of
     flow, content hugs the top-nav and the chip lands in the same
     horizontal band as the kicker (left-aligned text + right-anchored
     chip). When the banner morphs to active, main grows padding-top in
     lockstep to clear the expanded banner. */
  .live-banner-host {
    position: fixed;
    /* 56px nav + 8px breathing — chip hugs the nav, no dead black band
       between them. */
    top: 64px;
    left: 0;
    right: 0;
    width: calc(100% - 48px);
    max-width: 1280px;
    margin: 0 auto;
  }
  body.is-signed-in main {
    transition: padding-top 320ms var(--ease-soft);
  }
  body.is-signed-in:has(.live-banner:not(.live-banner--idle)) main,
  body.is-signed-in.has-active-banner main {
    /* Active banner top 64 + height ~80 = 144 (banner bottom). Main
       starts at 56 after the nav, so 88px more puts content at 144 —
       banner bottom; .studio-section's own 48px padding-top then gives
       the kicker its breathing room above.

       Dual selector: `:has()` works on modern browsers; the
       `has-active-banner` body class (set by live_banner.js) is the
       fallback for Safari <15.4 and Firefox <121 where `:has()` is
       unsupported and content would otherwise be overlapped by the
       morphed banner. */
    padding-top: 88px;
  }
}
.live-banner {
  flex: 1 1 100%;
  display: grid;
  grid-template-columns: 28px 1fr auto auto;
  grid-template-rows: auto;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: saturate(140%) blur(18px);
  -webkit-backdrop-filter: saturate(140%) blur(18px);
  border: 1px solid var(--line-2);
  box-shadow: var(--shadow-md);
  text-decoration: none;
  color: var(--ink);
  transition:
    flex-basis 320ms var(--ease-soft),
    flex-grow 320ms var(--ease-soft),
    grid-template-columns 320ms var(--ease-soft),
    padding 320ms var(--ease-soft),
    gap 320ms var(--ease-soft),
    transform 220ms var(--ease-soft),
    box-shadow 320ms var(--ease-soft),
    background-color 320ms var(--ease-soft),
    border-color 320ms var(--ease-soft);
}
.live-banner:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-lg);
}
.live-banner:focus-visible {
  /* Brand-coherent focus ring — without this the banner gets the
     browser-default blue outline, which clashes on the dark canvas
     and stands out as off-brand on what is now a prominent tab stop. */
  outline: 2px solid var(--vermilion-bright);
  outline-offset: 3px;
}
/* Self-referential banner on /history — the card is passive, no hover
   lift, no chevron arrow. User landed where the link would go. */
.live-banner--self { cursor: default; }
.live-banner--self:hover {
  transform: none;
  box-shadow: var(--shadow-md);
}

/* Idle: shrink the flex basis to a chip, collapse the text column, tuck
   the chev. flex-grow:0 prevents the basis from being overridden by
   leftover host space — without this the banner would stretch back to
   full width while the columns were still mid-morph. The card frame
   (background, border, shadow) drops out so the chip reads as two
   floating tokens (spark + pill) instead of a small lonely card
   marooned in the header gutter — the pill carries its own border. */
.live-banner--idle {
  flex: 0 0 auto;
  grid-template-columns: 28px 0fr auto auto;
  padding: 2px 0;
  gap: 6px;
  /* Explicit longhand `background-color` (not `background` shorthand) so
     the parent's `background-color` transition interpolates cleanly
     across Safari, which historically required longhand→longhand
     matching for transitioned properties. */
  background-color: transparent;
  border-color: transparent;
  box-shadow: none;
}
.live-banner--idle:hover {
  transform: none;
  box-shadow: none;
}

.live-banner__spark {
  width: 28px; height: 28px;
  border-radius: 50%;
  background-color: color-mix(in srgb, var(--vermilion-bright) 14%, transparent);
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  transition: background-color 320ms var(--ease-soft);
}
.live-banner--idle .live-banner__spark {
  /* Solid surface + saturating backdrop blur so the spark stays legible
     when the chip floats over bright photo content as the user scrolls.
     Without this, the dimmed vermilion dot on the dark canvas would
     vanish against light skin tones / white backgrounds. */
  background-color: var(--surface);
  backdrop-filter: saturate(140%) blur(8px);
  -webkit-backdrop-filter: saturate(140%) blur(8px);
}
.live-banner__spark-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--vermilion-bright);
  box-shadow: 0 0 8px var(--vermilion-glow);
  animation: studio-log-pulse 1.6s ease-in-out infinite;
  transition:
    opacity 220ms var(--ease-soft),
    box-shadow 320ms var(--ease-soft);
}
/* Idle: dim + stop the pulse — nothing's happening, so the dot reads
   as a passive status mark, not a live activity. */
.live-banner--idle .live-banner__spark-dot {
  animation: none;
  opacity: 0.7;
  box-shadow: none;
}
@media (prefers-reduced-motion: reduce) {
  .live-banner__spark-dot {
    animation: none;
    opacity: 0.85;
  }
}

.live-banner__text {
  display: flex; flex-direction: column;
  min-width: 0;
  overflow: hidden;
  /* max-height is the morphable analogue of auto height. 80px comfortably
     fits title (18px) + sub (14px) + progress (8px incl margin) on one
     line each; it collapses to 0 in idle. */
  max-height: 80px;
  gap: 2px;
  transition:
    max-height 320ms var(--ease-soft),
    opacity 220ms var(--ease-soft);
}
.live-banner--idle .live-banner__text {
  max-height: 0;
  opacity: 0;
}
.live-banner__title {
  font-size: 14px; font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.01em;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.live-banner__sub {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  display: flex; align-items: baseline; gap: 6px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.live-banner__dot { color: var(--ink-3); }
.live-banner__progress {
  height: 2px;
  border-radius: 999px;
  /* Empty-track affordance: when ready=0 but generating>0, fill width
     is 0%. The 2px grey track is still visible, but to make the bar
     read as "primed, waiting" instead of "broken empty", we paint a
     gradient on the track itself — fades from vermilion-glow at the
     leading edge to surface-2 — so there's directionality even with
     no fill. As soon as the first frame delivers, the fill paints
     over this and the glow becomes irrelevant. */
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--vermilion-bright) 22%, var(--surface-2)) 0%,
    var(--surface-2) 18%
  );
  overflow: hidden;
  margin-top: 6px;
}
.live-banner__progress[hidden] { display: none; }
.live-banner__progress-fill {
  display: block;
  height: 100%;
  background: var(--vermilion-bright);
  border-radius: 999px;
  transition: width 240ms var(--ease-snap);
  box-shadow: 0 0 8px var(--vermilion-glow);
}
.live-banner__pill {
  display: inline-flex; align-items: baseline; gap: 4px;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--line-2);
  background: var(--surface);
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  font-weight: 700;
  color: var(--vermilion-bright);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}
.live-banner__pill-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
  transition: opacity 180ms var(--ease-soft);
}
/* In idle the pill IS the banner's main affordance, so the LEFT
   micro-label drops out to keep the chip tight — leaving just the
   digit + spark to read as "30 remaining". */
.live-banner--idle .live-banner__pill-label {
  display: none;
}
.live-banner__chev {
  color: var(--vermilion-bright);
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  flex: 0 0 auto;
  display: inline-block;
  /* max-width is a non-constraining cap in active (the "→" glyph is
     ~16-20px wide depending on font) that can interpolate to 0 in idle,
     while preserving the glyph's natural width in active. Using width
     here instead clipped the glyph against an undersized box because
     `* { box-sizing: border-box; }` is global. */
  max-width: 28px;
  overflow: hidden;
  padding-left: 4px;
  padding-right: 2px;
  transition:
    transform 220ms var(--ease-snap),
    opacity 220ms var(--ease-soft),
    max-width 320ms var(--ease-soft),
    margin-left 320ms var(--ease-soft),
    padding-left 320ms var(--ease-soft),
    padding-right 320ms var(--ease-soft);
}
.live-banner:hover .live-banner__chev {
  transform: translateX(3px);
}
/* Idle: chev collapses to zero width + a negative margin that eats the
   12px column gap, so the pill sits flush against the chip's right
   padding. Pointer-events off keeps the invisible cell from intercepting
   taps that should hit the pill. */
.live-banner--idle .live-banner__chev {
  opacity: 0;
  max-width: 0;
  margin-left: -12px;
  padding-left: 0;
  padding-right: 0;
  pointer-events: none;
}
/* Narrow phones: tighter chrome on both modes. Pill keeps the digit on
   the only screen where /account is hardest to reach, but drops the
   LEFT microtype to fit 360-430px widths. */
@media (max-width: 480px) {
  .live-banner {
    padding: 10px 12px;
    gap: 8px;
  }
  .live-banner--idle {
    /* Re-state padding: base `.live-banner--idle { padding: 2px 0 }`
       loses to the mobile `.live-banner { padding: 10px 12px }` rule
       above due to equal specificity + later source order. Without this
       the idle chip on mobile would inherit active's padding and grow
       to ~50px tall. */
    padding: 2px 0;
    gap: 4px;
  }
  .live-banner__pill {
    padding: 3px 8px;
    font-size: 11px;
  }
  .live-banner__pill-label { display: none; }
  .live-banner--idle .live-banner__chev { margin-left: -8px; }
}

/* Bottom tabs — fixed-bottom mobile nav. Visible only ≤719px. */
.bottom-tabs {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  z-index: 60;
  display: none;
  grid-template-columns: repeat(3, 1fr);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: saturate(140%) blur(20px);
  -webkit-backdrop-filter: saturate(140%) blur(20px);
  border-top: 1px solid color-mix(in srgb, var(--line-2) 70%, transparent);
  padding-bottom: env(safe-area-inset-bottom);
}
@media (max-width: 719px) {
  .bottom-tabs { display: grid; }
}
.bottom-tab {
  position: relative;
  min-height: 56px;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 4px;
  text-decoration: none;
  color: var(--ink-3);
  transition: color 180ms var(--ease-snap);
}
.bottom-tab:hover { color: var(--ink-2); }
.bottom-tab__icon {
  width: 20px; height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.bottom-tab__label {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
}
.bottom-tab.is-active { color: var(--ink); }
.bottom-tab.is-active::before {
  content: "";
  position: absolute;
  top: 0; left: 14%; right: 14%;
  height: 3px;
  border-radius: 0 0 999px 999px;
  background: var(--vermilion-bright);
  box-shadow: 0 4px 12px var(--vermilion-glow);
}
/* Bottom-tabs push the page content up — without this the last row of
   the studio grid disappears behind the tab-bar on mobile. Desktop
   has no bottom-tabs so the padding is skipped. Top inset keeps content
   off the notch.

   Live-banner host on mobile is a fixed overlay rather than sticky-in-
   flow: the idle chip is small and right-aligned, so making it
   reserve a 46px row of dead space above the studio hero made the
   /studio top read as "huge empty header with a tiny status dot
   floating in it." Fixed lets content (studio-section's own
   padding-top) hug the notch. When the banner morphs to active, main
   grows padding-top in lockstep — synced 320ms transition — so the
   expanding banner pushes content down smoothly without overlapping. */
@media (max-width: 719px) {
  body.is-signed-in main {
    padding-top: env(safe-area-inset-top);
    padding-bottom: calc(56px + env(safe-area-inset-bottom));
    transition: padding-top 320ms var(--ease-soft);
  }
  body.is-signed-in:has(.live-banner:not(.live-banner--idle)) main,
  body.is-signed-in.has-active-banner main {
    /* env + host margin (6) + banner active height (~67 with progress)
       + host margin (6) ≈ 79px. Round to 80 for a 1px buffer.
       Dual selector: `:has()` for modern browsers, `has-active-banner`
       body class (set by live_banner.js) as fallback for Safari <15.4
       and Firefox <121. */
    padding-top: calc(env(safe-area-inset-top) + 80px);
  }
  .live-banner-host {
    position: fixed;
    top: env(safe-area-inset-top);
    left: 12px;
    right: 12px;
    width: auto;
    margin: 6px 0;
  }
}

/* Visually-hidden helper kept (was inside the old counter-shelf block,
   but other partials use it — sign-in funnel, aria mirrors). */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ============================================================
   STUDIO PAGE — typography
   ============================================================ */
.studio-section { padding-top: 48px; padding-bottom: 64px; }
.studio-h1 {
  font-size: clamp(40px, 6.5vw, 80px);
  font-weight: 900;
  letter-spacing: -0.04em;
  line-height: 1.02;
  margin-bottom: 24px;
  background: linear-gradient(180deg, var(--ink) 0%, color-mix(in srgb, var(--ink) 75%, var(--bg)) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
.studio-h1 em {
  font-style: normal;
  background: var(--gradient-warm);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
.studio-lead {
  font-size: clamp(16px, 1.5vw, 19px);
  color: var(--ink-2);
  line-height: 1.5;
  max-width: 54ch;
  margin-bottom: 48px;
}

/* ============================================================
   PACK GRID — one card per category
   ============================================================
   Mobile 1-col, tablet 2-col, desktop 3-col. `.pack-drawer` is a
   grid child via `grid-column: 1 / -1` so opening a pack never
   reflows the cards row. On mobile JS reparents the drawer next
   to the tapped pack (accordion expands where the user expects);
   on desktop it stays parked at the end of the grid. */
.pack-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}
@media (min-width: 720px) {
  .pack-grid { grid-template-columns: repeat(2, 1fr); gap: 20px; }
}
@media (min-width: 1024px) {
  .pack-grid { grid-template-columns: repeat(3, 1fr); gap: 24px; }
}

.pack {
  position: relative;
  /* Without this, a long pack label inside .pack__title (nowrap +
     overflow:hidden) sets the column track's min-content size to the
     full label width, blowing past 1fr — the entire card overflows
     the viewport on mobile. `min-width: 0` overrides the default
     `auto` on grid items so 1fr can actually compute as 1fr. */
  min-width: 0;
}

.pack__card {
  display: block;
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 0;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  font-family: inherit;
  color: inherit;
  transition: transform 320ms var(--ease-soft),
              border-color 200ms var(--ease-snap),
              box-shadow 240ms var(--ease-snap);
}
.pack__card:hover {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--ink) 22%, transparent);
  box-shadow: var(--shadow-md);
}
.pack.is-open .pack__card {
  border-color: var(--vermilion-bright);
  box-shadow: 0 0 0 1px var(--vermilion-bright),
              0 12px 48px color-mix(in srgb, var(--vermilion-bright) 22%, transparent);
  /* Defend the mobile fusion seam. The pre-existing `.pack__card:hover`
     rule lifts the card -2px on hover. On iOS Safari `:hover` sticks
     after tap, so without this override the open card would float 2px
     above the drawer and the seam between the action row and the
     drawer's top border would re-open. */
  transform: none;
}
.pack__card:focus-visible {
  outline: 2px solid var(--vermilion-bright);
  outline-offset: 2px;
}

/* Mosaic — grid of portrait preview tiles, one per published style
   in the pack. data-count picks both the grid template AND a
   container aspect-ratio so cells stay 9:16 portrait. Counts 4-5 use
   the same 3×2 grid as 6 — leftover cells fall through to the clip
   background and visually disappear. */
.pack__mosaic-clip {
  position: relative;
  overflow: hidden;
  background: var(--bg-2);
}
.pack__mosaic {
  display: grid;
  gap: 2px;
  width: 100%;
}
.pack__mosaic[data-count="1"] {
  aspect-ratio: 9 / 16;
  grid-template-columns: 1fr;
}
.pack__mosaic[data-count="2"] {
  aspect-ratio: 9 / 8;
  grid-template-columns: repeat(2, 1fr);
}
.pack__mosaic[data-count="3"] {
  aspect-ratio: 27 / 16;
  grid-template-columns: repeat(3, 1fr);
}
.pack__mosaic[data-count="4"],
.pack__mosaic[data-count="5"],
.pack__mosaic[data-count="6"] {
  aspect-ratio: 27 / 32;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(2, 1fr);
}
.pack__mosaic-tile {
  position: relative;
  overflow: hidden;
  background: var(--surface);
}
.pack__mosaic-tile img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center 25%;
  display: block;
  transition: transform 480ms var(--ease-soft);
}
.pack__card:hover .pack__mosaic-tile img { transform: scale(1.04); }

.pack__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 16px 20px 14px;
}
.pack__title {
  font-size: clamp(17px, 1.6vw, 20px);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ink);
  /* Long titles truncate rather than crush the new-dot beside them. */
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pack__new-dot {
  display: inline-block;
  vertical-align: middle;
  width: 7px; height: 7px;
  margin-left: 8px;
  /* Lift the dot one pixel so it optically centres against the title's
     x-height rather than its full glyph height. */
  margin-bottom: 2px;
  border-radius: 50%;
  background: var(--vermilion-bright);
  box-shadow: 0 0 0 0 var(--vermilion-glow);
  animation: pack-new-pulse 1.8s ease-in-out infinite;
  flex-shrink: 0;
}
@keyframes pack-new-pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--vermilion-glow); }
  50%      { box-shadow: 0 0 0 6px transparent; }
}

/* Action row — the whole card is the toggle <button>, this strip carries
   the visible CTA. Reads as a button from across the screen (full-width,
   warm vermilion wash, JetBrains Mono caps in vermilion-bright). Open
   state flips to a solid vermilion-bright fill with ink text so the
   transition reads as a real press. */
.pack__action {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px 20px;
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--vermilion-deep) 16%, transparent);
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--vermilion-bright);
  transition: background-color 220ms var(--ease-snap),
              color 220ms var(--ease-snap),
              border-color 220ms var(--ease-snap);
}
.pack__action-label { white-space: nowrap; }
.pack__action-label--close { display: none; }
.pack.is-open .pack__action-label--open { display: none; }
.pack.is-open .pack__action-label--close { display: inline; }
.pack__action-glyph {
  display: inline-block;
  font-weight: 400;
  transition: transform 200ms var(--ease-snap);
}
/* Hover affordances on the action row + glyph slide. Gated on a
   hover-capable device so iOS Safari's sticky-:hover doesn't leave
   the card looking pressed after tap. */
@media (hover: hover) and (pointer: fine) {
  .pack__card:hover .pack__action {
    background: color-mix(in srgb, var(--vermilion-deep) 32%, transparent);
  }
  .pack__card:hover .pack__action-label--open .pack__action-glyph {
    transform: translateX(3px);
  }
}
.pack.is-open .pack__action {
  background: var(--vermilion-bright);
  border-top-color: var(--vermilion-bright);
  color: var(--ink);
}

/* ============================================================
   PACK DRAWER — accordion sibling of the pack cards
   ============================================================
   The 0fr → 1fr grid-template-rows trick gives the row a transitionable
   height. JS owns the rest of the open/close/switch choreography (see
   showDrawerFor in studio.html). overflow-anchor:none keeps Chrome/
   Firefox from scroll-anchoring on the next pack during closePack's
   collapse — iOS Safari doesn't honor it, but it's harmless there. */
.pack-grid,
.pack { overflow-anchor: none; }

/* Symmetric 260ms expand/collapse. The switch path in JS suspends
   transitions and does an instant collapse + reparent + scroll-pin in
   one synchronous task before re-enabling them for the expand, so
   these durations only apply to cold-open expand and closePack
   collapse — both of which the user should see as smooth motion. */
.pack-drawer {
  grid-column: 1 / -1;
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 260ms var(--ease-soft);
  will-change: grid-template-rows;
  position: relative; /* anchor for the pointer arrow */
}
.pack-drawer.is-open {
  grid-template-rows: 1fr;
}
.pack-drawer__inner {
  overflow: hidden;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 0;
  background: transparent;
  transition: padding 260ms var(--ease-soft),
              border-color 220ms var(--ease-snap),
              background-color 220ms var(--ease-snap),
              box-shadow 220ms var(--ease-snap);
}
.pack-drawer.is-open .pack-drawer__inner {
  padding: 0 20px 20px;
  background: var(--surface);
  border-color: var(--vermilion-bright);
  box-shadow: 0 0 0 1px var(--vermilion-bright),
              0 12px 48px color-mix(in srgb, var(--vermilion-bright) 22%, transparent);
}

/* Panels are siblings inside the drawer. Only the active one is laid
   out — the others stay display:none so the drawer's natural height
   tracks the active panel. The active panel fades + slides up as the
   drawer expands. */
.pack-drawer__panel {
  display: none;
  opacity: 0;
  transform: translate3d(0, 6px, 0);
  transition: opacity 200ms var(--ease-soft), transform 260ms var(--ease-soft);
}
.pack-drawer__panel.is-active { display: block; }
.pack-drawer.is-open .pack-drawer__panel.is-active {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* Drawer header — names the pack and gives an explicit close so users
   know where they are and how to back out. The close button is
   pointer-sized (≥44px tap target) on mobile via the symbol + word. */
.pack-drawer__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 0 14px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
.pack-drawer__header-text {
  flex: 1 1 auto;
  min-width: 0;
}
.pack-drawer__title {
  font-size: clamp(18px, 1.8vw, 22px);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin: 0;
  line-height: 1.2;
}
.pack-drawer__subtitle {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 6px 0 0;
}
.pack-drawer__close {
  flex-shrink: 0;
  /* WCAG 2.5.5 / Apple HIG: ≥44px target. Vertical padding does the
     work — on desktop the pill can be narrower (smaller padding) since
     a mouse pointer doesn't need 44px. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: transparent;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  padding: 10px 16px;
  cursor: pointer;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
  transition: color 200ms var(--ease-snap),
              border-color 200ms var(--ease-snap),
              background-color 200ms var(--ease-snap);
}
@media (min-width: 720px) {
  .pack-drawer__close {
    min-height: 0;
    padding: 8px 14px;
  }
}
@media (hover: hover) and (pointer: fine) {
  .pack-drawer__close:hover {
    color: var(--ink);
    border-color: var(--vermilion-bright);
    background: color-mix(in srgb, var(--vermilion-deep) 24%, transparent);
  }
}
.pack-drawer__close:focus-visible {
  outline: 2px solid var(--vermilion-bright);
  outline-offset: 2px;
}
.pack-drawer__close-glyph {
  font-size: 14px;
  line-height: 1;
  color: var(--vermilion-bright);
}

/* Pointer arrow — a pure-CSS triangle (no glyph, no SVG, no icon
   library) sitting on top of the drawer's vermilion-bright border.
   JS sets --arrow-x to the originating card's horizontal center
   relative to the drawer's left edge so the drawer reads as belonging
   to that column. Display:none on mobile; the card-drawer fusion
   already makes ownership unambiguous there. */
.pack-drawer__arrow {
  display: none;
  position: absolute;
  /* -11px so the 12px-tall arrow overlaps the drawer's top border
     (sits ON the edge, not floating above it). */
  top: -11px;
  left: var(--arrow-x, 50%);
  transform: translateX(-50%);
  width: 0; height: 0;
  border-left: 12px solid transparent;
  border-right: 12px solid transparent;
  border-bottom: 12px solid var(--vermilion-bright);
  pointer-events: none;
  z-index: 2;
  opacity: 0;
  transition: opacity 200ms var(--ease-snap);
}
.pack-drawer__arrow::after {
  /* Inner triangle in surface color — masks the 1px outline below the
     drawer's box-shadow so the arrow reads as cut INTO the border, not
     stuck on top of it. */
  content: "";
  position: absolute;
  top: 1px; left: -11px;
  width: 0; height: 0;
  border-left: 11px solid transparent;
  border-right: 11px solid transparent;
  border-bottom: 11px solid var(--surface);
}
.pack-drawer.is-open .pack-drawer__arrow {
  opacity: 1;
}

/* Mobile fusion: card + drawer read as one object.
   - Drawer's top gap closes via -16px margin so the two pieces kiss.
   - Card's bottom-radius drops to 0 so the action row's flat bottom
     edge meets the drawer's flat top edge.
   - Drawer's top-radius drops to 0 to match.
   - Action row when open is already solid vermilion-bright; it reads
     as the "neck" connecting the card head to the drawer below.
   - Arrow stays hidden — fusion makes ownership unambiguous.
   - Soft halo is dialled back: the full-width mobile drawer would
     otherwise spray a 48px vermilion glow against the live-banner
     above. 1px outline + fusion already carry ownership. */
@media (max-width: 719px) {
  .pack-drawer.is-open {
    margin-top: -16px;
  }
  .pack.is-open .pack__card {
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  }
  .pack-drawer.is-open .pack-drawer__inner {
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    box-shadow: 0 0 0 1px var(--vermilion-bright),
                0 8px 20px color-mix(in srgb, var(--vermilion-bright) 12%, transparent);
  }
}

/* Tablet/Desktop: drawer is a standalone framed shape; the grid row-gap
   (20-24px) is the only spacing between card row and drawer. The arrow
   straddles the drawer's top edge and reaches into that gap to point
   back at the originating column — no extra margin needed. */
@media (min-width: 720px) {
  .pack-drawer.is-open .pack-drawer__inner {
    border-radius: var(--radius-lg);
  }
  .pack-drawer__arrow {
    display: block;
  }
}

/* Tile grid inside an open pack — denser cadence than the pack grid.
   Mobile 2-col, desktop 3-col. */
.pack-drawer__tiles {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}
@media (min-width: 1024px) {
  .pack-drawer__tiles { grid-template-columns: repeat(3, 1fr); gap: 14px; }
}

/* ============================================================
   SHOOT TILE — tap = shoot
   ============================================================ */
.shoot-tile {
  position: relative;
  display: block;
  width: 100%;
  background: var(--surface);
  border: none;
  border-radius: var(--radius-md);
  padding: 0;
  cursor: pointer;
  overflow: hidden;
  font-family: inherit;
  color: inherit;
  text-align: left;
  animation: shoot-tile-enter 320ms var(--ease-soft) both;
  transition: transform 200ms var(--ease-snap);
}
@keyframes shoot-tile-enter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Light stagger — a freshly-opened pack reveals tiles in sequence. */
.shoot-tile:nth-child(2) { animation-delay: 30ms; }
.shoot-tile:nth-child(3) { animation-delay: 60ms; }
.shoot-tile:nth-child(4) { animation-delay: 90ms; }
.shoot-tile:nth-child(5) { animation-delay: 120ms; }
.shoot-tile:nth-child(6) { animation-delay: 150ms; }
.shoot-tile:nth-child(7) { animation-delay: 180ms; }
.shoot-tile:nth-child(n+8) { animation-delay: 210ms; }

.shoot-tile.is-pressing { transform: scale(0.97); }
.shoot-tile:focus-visible {
  outline: 2px solid var(--vermilion-bright);
  outline-offset: 2px;
}
.shoot-tile.is-shaking { animation: shoot-tile-shake 480ms ease-in-out; }
@keyframes shoot-tile-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(2px); }
}

.shoot-tile.is-new::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: var(--vermilion-bright);
  z-index: 3;
  pointer-events: none;
}

.shoot-tile__cover {
  position: relative;
  display: block;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  background: var(--bg-2);
}
.shoot-tile__cover img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center 25%;
  display: block;
  transition: transform 480ms var(--ease-soft);
}
.shoot-tile:hover .shoot-tile__cover img { transform: scale(1.04); }
.shoot-tile__cost {
  position: absolute;
  top: 8px; right: 8px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
  background: color-mix(in srgb, var(--bg) 70%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  padding: 4px 8px;
  border-radius: 999px;
  z-index: 2;
}
.shoot-tile__cooking-phrase {
  position: absolute;
  inset: auto 0 0 0;
  padding: 14px 10px;
  text-align: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  background: linear-gradient(0deg, color-mix(in srgb, var(--bg) 88%, transparent) 0%, transparent 100%);
  opacity: 0;
  transition: opacity 600ms var(--ease-soft);
  z-index: 2;
}
.shoot-tile__cooking-phrase.is-on { opacity: 1; }

/* Foot is the only thing that owns the bottom-rounded chrome on the tile.
   It stacks the style title above the TAP TO SHOOT microline so the
   gesture-to-outcome link is unmissable at the moment of decision. */
.shoot-tile__foot {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: 12px 14px;
  background: var(--surface);
  border-radius: 0 0 var(--radius-md) var(--radius-md);
}
.shoot-tile__title {
  display: block;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  line-height: 1.3;
}
.shoot-tile__cta {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* WCAG AA: --ink-2 (#B7B0A5) on --surface (#1E1A16) ≈ 7.3:1, AAA. */
  color: var(--ink-2);
  transition: color 200ms var(--ease-snap);
}
.shoot-tile__cta-arrow {
  color: var(--vermilion-bright);
  transition: transform 200ms var(--ease-snap);
}
@media (hover: hover) and (pointer: fine) {
  .shoot-tile:hover .shoot-tile__cta { color: var(--ink); }
  .shoot-tile:hover .shoot-tile__cta-arrow { transform: translateX(3px); }
}
/* In-flight chrome (status pill + dim cover + cooking ticker) carries the
   signal — the CTA microline would compete with it. visibility:hidden
   (vs display:none) reserves the slot's height so the tile's foot
   stays consistent across in-flight and idle states; row-equalization
   no longer needs to paper over a shrinking foot. */
.shoot-tile.is-shooting .shoot-tile__cta { visibility: hidden; }

/* In-shoot state: dim the cover image + show the "In studio" pill.
   No red outline or glow around the whole tile — the dim cover +
   pill + bottom-cooking-phrase carry the signal, and the red ring
   competed with the brand accent inside the pill itself. Cursor
   stays `pointer` (not `progress`) because re-tap is supported —
   every tap queues another shoot subject to allowance. */
.shoot-tile.is-shooting .shoot-tile__cover img {
  filter: brightness(0.62) saturate(0.85);
  transition: filter 220ms var(--ease-snap);
}

/* Status pill — instant signal that the tile is busy. Lives top-left
   over the cover; the JetBrains Mono + pulsing dot is the same visual
   language as .studio-log so the chrome reads as one system. The cost
   pill (top-right) keeps its slot; status sits opposite so they don't
   collide. */
.shoot-tile__status {
  position: absolute;
  top: 8px; left: 8px;
  display: none;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--vermilion-deep) 78%, transparent);
  border: 1px solid color-mix(in srgb, var(--vermilion-bright) 70%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  z-index: 2;
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  pointer-events: none;
}
.shoot-tile.is-shooting .shoot-tile__status { display: inline-flex; }
.shoot-tile__status-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--vermilion-bright);
  box-shadow: 0 0 8px var(--vermilion-glow);
  animation: studio-log-pulse 1.6s ease-in-out infinite;
  flex-shrink: 0;
}
.shoot-tile__status-label { letter-spacing: 0.1em; }
/* ×N count — visible only when a tile has more than one shoot in
   flight (re-tap path). Empty `data-shoot-count` keeps the slot out
   of the layout entirely so a single-shoot tile reads as "In studio"
   not "In studio ·". */
.shoot-tile__status-count {
  margin-left: 2px;
  padding-left: 8px;
  border-left: 1px solid color-mix(in srgb, var(--ink) 28%, transparent);
  color: var(--vermilion-bright);
  font-variant-numeric: tabular-nums;
}
.shoot-tile__status-count:empty { display: none; }
/* Hide the cost pill while shooting — the status pill takes priority,
   and the user already committed to the cost when they tapped. */
.shoot-tile.is-shooting .shoot-tile__cost { display: none; }

/* ============================================================
   FLY-UP GHOST — clones the tile cover and arcs to the shelf
   ============================================================ */
#fly-zone {
  position: fixed;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 90;
  /* Above header (z 50) and sticky-cta (z 60), below toast (z 100). */
}
.fly-ghost {
  position: fixed;
  pointer-events: none;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  overflow: hidden;
  background: var(--surface);
  will-change: transform, opacity, filter;
  filter: drop-shadow(0 18px 28px rgba(0, 0, 0, 0.35));
}
.fly-ghost img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center 25%;
  display: block;
}

/* ============================================================
   STUDIO TOAST — inline feedback (insufficient finals, errors)
   ============================================================ */
.studio-toast {
  position: fixed;
  left: 50%;
  bottom: calc(24px + env(safe-area-inset-bottom));
  transform: translateX(-50%) translateY(20px);
  z-index: 100;
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: saturate(150%) blur(20px);
  -webkit-backdrop-filter: saturate(150%) blur(20px);
  border: 1px solid color-mix(in srgb, var(--vermilion-bright) 32%, transparent);
  border-radius: 999px;
  padding: 12px 20px;
  display: flex; align-items: center; gap: 14px;
  opacity: 0;
  transition: opacity 220ms var(--ease-snap),
              transform 280ms var(--ease-soft);
  box-shadow: var(--shadow-lg);
  max-width: calc(100vw - 32px);
}
.studio-toast.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
.studio-toast__text {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
}
.studio-toast__cta {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--vermilion-bright);
}
.studio-toast__cta[hidden] { display: none; }

/* ============================================================
   REDUCED MOTION
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}
