/* ═══════════════════════════════════════════════════════════════════════
   PerpetualShare — Landing page section animations

   Scroll-triggered sequences for How It Works, the feature bento and the
   pricing card, plus the hero's continuously arriving photo wall.

   Two rules govern everything in this file:

   1. Animations are opt-in. Every "hidden at start" declaration is scoped
      under html.js-anim, which public/pages/landing.html stamps from a
      blocking script in its head — and only when the visitor has not asked
      for reduced motion. If that script never runs, if premium-fx.js fails,
      or if motion is reduced, no rule here hides anything and every section
      renders in its finished state. Two shipped defects in this codebase
      came from content that stayed invisible until JavaScript said so.

   2. Only opacity and transform animate, so the work stays on the
      compositor. Never apply a forwards fill to an element JavaScript
      writes styles to — anything carrying data-tilt, data-depth or
      data-magnetic. A filling keyframe outranks the inline style and
      silently breaks the interaction.

   Sections are triggered by data-play, which premium-fx.js resolves to a
   one-shot .px-playing class. The hero wall is the exception: it loops on
   infinite keyframes with no JavaScript involvement at all.

   Easing tokens come from premium.css: --px-ease, --px-ease-soft,
   --px-spring. There is no --ease.
   ═══════════════════════════════════════════════════════════════════════ */


/* ───────────────────────────────────────────────────────────────────────
   Reveal safety net

   premium.css sets [data-reveal] { opacity: 0 } and [data-stagger] > *
   { opacity: 0 } unconditionally, with no opt-in gate — the only escape is
   its prefers-reduced-motion block. So if premium-fx.js fails to boot, is
   blocked, or JavaScript is off entirely, every revealed section on the page
   stays invisible: the steps, the feature bento, pricing, the demo.

   That is the exact failure html.js-anim exists to prevent; premium.css
   simply predates it. Since js-anim is stamped only when scripts run, its
   absence is a reliable signal that nothing will ever reveal these elements,
   so show them immediately.

   Scoped to this page deliberately. premium.css ships on every page, and the
   same hole exists there — fixing it globally means stamping js-anim on every
   page and is a separate change.
   ─────────────────────────────────────────────────────────────────────── */

html:not(.js-anim) [data-reveal],
html:not(.js-anim) [data-stagger] > * {
  opacity: 1;
  transform: none;
  filter: none;
}


/* ───────────────────────────────────────────────────────────────────────
   How It Works — relay timeline

   Each step gains a small visualisation of what that step actually produces:
   a QR card drawing itself, a guest phone pulsing as it joins, a gallery
   filling. A spark runs the connector between them and each number pops as it
   arrives, so the three read as one sequence rather than three paragraphs.

   Roughly 3.5s end to end, played once when the section comes into view.

   Everything below renders in its finished state without html.js-anim: full
   QR pattern, visible rings, all six tiles. Only the hidden-at-start rules
   are scoped under that class.
   ─────────────────────────────────────────────────────────────────────── */

.step__viz {
  position: relative;
  height: 96px;
  margin: -0.8rem auto 1.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ── 01 · QR card ── */

.step__qr {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 3px;
  width: 76px;
  padding: 9px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(242, 204, 164, 0.14);
}

.step__qr i {
  aspect-ratio: 1;
  border-radius: 1px;
  background: transparent;
}

.step__qr i.on {
  background: rgba(242, 204, 164, 0.75);
}

/* ── 02 · Guest joins ── */

.step__phone {
  position: relative;
  z-index: 1;
  width: 34px;
  height: 56px;
  border-radius: 8px;
  border: 1.5px solid rgba(242, 204, 164, 0.5);
  background: rgba(10, 8, 8, 0.9);
  display: flex;
  align-items: center;
  justify-content: center;
}

.step__phone b {
  width: 12px;
  height: 12px;
  border-radius: 3px;
  background: rgba(242, 204, 164, 0.6);
}

.step__ring {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 56px;
  height: 56px;
  margin: -28px 0 0 -28px;
  border-radius: 50%;
  border: 1px solid rgba(242, 204, 164, 0.32);
}

.step__ring--2 {
  width: 84px;
  height: 84px;
  margin: -42px 0 0 -42px;
  border-color: rgba(242, 204, 164, 0.16);
}

/* ── 03 · Gallery fills ── */

.step__viz--fill {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 44px;
  gap: 4px;
  width: 132px;

  /* .step__viz centres its flex children; inherited into this grid that
     collapses every empty tile to zero height. Grid's own default is what
     is wanted here. */
  align-items: stretch;
}

.step__tile {
  /* Explicit, not derived from aspect-ratio: these are empty elements inside
     a fixed-height box, and an intrinsic size would collapse them. */
  height: 44px;
  border-radius: 5px;
  background-size: cover;
  background-position: center;
}

/* ── The relay ── */

/* The spark rides the connector that .steps::before already draws. Hidden
   until the sequence plays, and gone entirely once the steps stack. */
.steps::after {
  content: '';
  position: absolute;
  top: 25px;
  left: calc(16.67% + 1.5rem);
  width: 34px;
  height: 5px;
  border-radius: 999px;
  background: linear-gradient(90deg, transparent, var(--primary), transparent);
  opacity: 0;
  z-index: 0;
  pointer-events: none;
}

/* Animating `left` rather than transform, deliberately. The connector's ends
   are percentage-based, so the spark's travel has to be too — a fixed pixel
   translate would be wrong at every width but one. Percentages on `left`
   resolve against .steps, which is exactly the span needed. The cost is
   normally a reason to avoid this, but the spark is absolutely positioned and
   5px tall, so it reflows nothing but itself. */
@keyframes stepSpark {
  0%   { opacity: 0; left: calc(16.67% + 1.5rem); }
  8%   { opacity: 1; }
  92%  { opacity: 1; }
  100% { opacity: 0; left: calc(83.33% - 1.5rem - 34px); }
}

@keyframes stepPop {
  0%   { border-color: rgba(242, 204, 164, 0.22); box-shadow: 0 0 0 10px var(--surface), 0 0 40px rgba(242, 204, 164, 0.07); }
  40%  { border-color: rgba(242, 204, 164, 0.7);  box-shadow: 0 0 0 10px var(--surface), 0 0 46px rgba(242, 204, 164, 0.34); }
  100% { border-color: rgba(242, 204, 164, 0.22); box-shadow: 0 0 0 10px var(--surface), 0 0 40px rgba(242, 204, 164, 0.07); }
}

@keyframes stepCellIn {
  from { opacity: 0; transform: scale(0.4); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes stepRingOut {
  0%   { opacity: 0; transform: scale(0.35); }
  35%  { opacity: 1; }
  100% { opacity: 0; transform: scale(1.25); }
}

@keyframes stepTileIn {
  from { opacity: 0; transform: scale(0.8); }
  to   { opacity: 1; transform: scale(1); }
}

/* Hidden-at-start rules — scoped so a missing script leaves everything shown */
html.js-anim .steps .step__qr i.on,
html.js-anim .steps .step__tile {
  opacity: 0;
}

html.js-anim .steps .step__ring {
  opacity: 0;
}

html.js-anim .steps.px-playing::after {
  animation: stepSpark 3.3s var(--px-ease-soft) 0.15s 1 both;
}

/* Each number lights as the spark reaches it */
html.js-anim .steps.px-playing .step:nth-child(1) .step__number { animation: stepPop 0.7s var(--px-ease) 0.15s both; }
html.js-anim .steps.px-playing .step:nth-child(2) .step__number { animation: stepPop 0.7s var(--px-ease) 1.35s both; }
html.js-anim .steps.px-playing .step:nth-child(3) .step__number { animation: stepPop 0.7s var(--px-ease) 2.55s both; }

/* 01 — cells fill in reading order, fast enough to read as drawing */
html.js-anim .steps.px-playing .step__qr i.on {
  animation: stepCellIn 0.34s var(--px-ease) both;
}

html.js-anim .steps.px-playing .step__qr i:nth-child(7n+1) { animation-delay: 0.30s; }
html.js-anim .steps.px-playing .step__qr i:nth-child(7n+2) { animation-delay: 0.36s; }
html.js-anim .steps.px-playing .step__qr i:nth-child(7n+3) { animation-delay: 0.42s; }
html.js-anim .steps.px-playing .step__qr i:nth-child(7n+4) { animation-delay: 0.48s; }
html.js-anim .steps.px-playing .step__qr i:nth-child(7n+5) { animation-delay: 0.54s; }
html.js-anim .steps.px-playing .step__qr i:nth-child(7n+6) { animation-delay: 0.60s; }
html.js-anim .steps.px-playing .step__qr i:nth-child(7n+7) { animation-delay: 0.66s; }

/* 02 — the pulse repeats a few times, then settles */
html.js-anim .steps.px-playing .step__ring   { animation: stepRingOut 1.5s var(--px-ease-soft) 1.45s 2 both; }
html.js-anim .steps.px-playing .step__ring--2 { animation-delay: 1.75s; }

/* 03 — tiles land one after another */
html.js-anim .steps.px-playing .step__tile { animation: stepTileIn 0.4s var(--px-ease) both; }
html.js-anim .steps.px-playing .step__tile:nth-child(1) { animation-delay: 2.65s; }
html.js-anim .steps.px-playing .step__tile:nth-child(2) { animation-delay: 2.78s; }
html.js-anim .steps.px-playing .step__tile:nth-child(3) { animation-delay: 2.91s; }
html.js-anim .steps.px-playing .step__tile:nth-child(4) { animation-delay: 3.04s; }
html.js-anim .steps.px-playing .step__tile:nth-child(5) { animation-delay: 3.17s; }
html.js-anim .steps.px-playing .step__tile:nth-child(6) { animation-delay: 3.30s; }

/* Stacked layout: the connector hides itself, so the spark must go too. */
@media (max-width: 680px) {
  .steps::after { display: none; }

  .step__viz { height: 84px; }
}


/* ───────────────────────────────────────────────────────────────────────
   Feature bento — each card demonstrates itself

   Live Gallery streams photos in, Photo Categories filters by chip, Download
   & Share drops a photo into a tray. All three play once when the grid comes
   into view, offset so they read one after another rather than at once.

   Every tile is a hero wall tile: cached from the hero long before this
   section scrolls into view, and already carrying the warm grade.

   Sizes are explicit. These are empty elements inside fixed-height boxes, and
   an intrinsic or aspect-ratio height collapses them — the defect that broke
   the prototype and again in the How It Works grid.

   No `forwards` fill lands on .bento-card itself: data-tilt and
   data-spotlight write transforms to it, and a filling keyframe would
   outrank them and kill the hover.
   ─────────────────────────────────────────────────────────────────────── */

.bento-card__viz {
  position: relative;
  margin-bottom: 1.6rem;
}

.bento-tile {
  display: block;
  border-radius: 6px;
  background-size: cover;
  background-position: center;
}

/* ── Live Gallery · photos stream in ── */

.bento-card__viz--stream {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 6px;
  align-items: stretch;
}

.bento-card__viz--stream .bento-tile {
  height: 62px;
}

/* ── Photo Categories · filter by chip ── */

.bento-card__viz--filter {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.bento-chips {
  display: flex;
  gap: 5px;
}

.bento-chip {
  height: 14px;
  flex: 1;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(242, 204, 164, 0.1);
}

.bento-chip--pick {
  background: rgba(242, 204, 164, 0.16);
  border-color: rgba(242, 204, 164, 0.32);
}

.bento-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 5px;
  align-items: stretch;
}

.bento-grid .bento-tile {
  height: 42px;
}

/* ── Download & Share · photo lands in the tray ── */

.bento-card__viz--drop {
  height: 96px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.bento-drop {
  position: absolute;
  top: 0;
  left: 50%;
  margin-left: -26px;
  width: 52px;
  height: 52px;
  border-radius: 7px;
  background-size: cover;
  background-position: center;
}

.bento-tray {
  width: 132px;
  height: 34px;
  border-radius: 10px;
  border: 1px dashed rgba(242, 204, 164, 0.3);
  background: rgba(242, 204, 164, 0.04);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--primary);
}

.bento-tray .material-symbols-outlined {
  font-size: 17px;
}

/* ── Play ── */

@keyframes bentoTileIn {
  from { opacity: 0; transform: translateY(12px) scale(0.9); }
  to   { opacity: 1; transform: none; }
}

@keyframes bentoDim {
  to { opacity: 0.22; }
}

@keyframes bentoChipPick {
  0%   { background: rgba(255, 255, 255, 0.05); border-color: rgba(242, 204, 164, 0.1); }
  100% { background: rgba(242, 204, 164, 0.16); border-color: rgba(242, 204, 164, 0.32); }
}

@keyframes bentoDropIn {
  0%   { opacity: 0; transform: translateY(-14px) scale(0.9); }
  25%  { opacity: 1; transform: translateY(0) scale(1); }
  70%  { opacity: 1; transform: translateY(46px) scale(0.62); }
  100% { opacity: 0; transform: translateY(56px) scale(0.5); }
}

@keyframes bentoTrayLit {
  0%, 60% { border-color: rgba(242, 204, 164, 0.3); background: rgba(242, 204, 164, 0.04); }
  75%     { border-color: rgba(242, 204, 164, 0.75); background: rgba(242, 204, 164, 0.16); box-shadow: 0 0 26px rgba(242, 204, 164, 0.22); }
  100%    { border-color: rgba(242, 204, 164, 0.42); background: rgba(242, 204, 164, 0.07); }
}

/* Hidden-at-start — without js-anim every card shows its finished state */
html.js-anim .bento .bento-tile,
html.js-anim .bento .bento-drop {
  opacity: 0;
}

html.js-anim .bento .bento-chip--pick {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(242, 204, 164, 0.1);
}

/* Card 1 — six photos land in sequence */
html.js-anim .bento.px-playing .bento-card__viz--stream .bento-tile {
  animation: bentoTileIn 0.44s var(--px-ease) both;
}
html.js-anim .bento.px-playing .bento-card__viz--stream .bento-tile:nth-child(1) { animation-delay: 0.10s; }
html.js-anim .bento.px-playing .bento-card__viz--stream .bento-tile:nth-child(2) { animation-delay: 0.22s; }
html.js-anim .bento.px-playing .bento-card__viz--stream .bento-tile:nth-child(3) { animation-delay: 0.34s; }
html.js-anim .bento.px-playing .bento-card__viz--stream .bento-tile:nth-child(4) { animation-delay: 0.46s; }
html.js-anim .bento.px-playing .bento-card__viz--stream .bento-tile:nth-child(5) { animation-delay: 0.58s; }
html.js-anim .bento.px-playing .bento-card__viz--stream .bento-tile:nth-child(6) { animation-delay: 0.70s; }

/* Card 2 — tiles arrive, then a chip is picked and the rest fade back */
html.js-anim .bento.px-playing .bento-grid .bento-tile {
  animation: bentoTileIn 0.4s var(--px-ease) both;
}
html.js-anim .bento.px-playing .bento-grid .bento-tile:nth-child(1) { animation-delay: 0.30s; }
html.js-anim .bento.px-playing .bento-grid .bento-tile:nth-child(2) { animation-delay: 0.38s; }
html.js-anim .bento.px-playing .bento-grid .bento-tile:nth-child(3) { animation-delay: 0.46s; }
html.js-anim .bento.px-playing .bento-grid .bento-tile:nth-child(4) { animation-delay: 0.54s; }
html.js-anim .bento.px-playing .bento-grid .bento-tile:nth-child(5) { animation-delay: 0.62s; }
html.js-anim .bento.px-playing .bento-grid .bento-tile:nth-child(6) { animation-delay: 0.70s; }

html.js-anim .bento.px-playing .bento-chip--pick {
  animation: bentoChipPick 0.4s var(--px-ease) 1.05s both;
}

/* The non-matching tiles keep their entrance animation and then dim, so both
   run on the same element — comma-separated, delays matched to each. */
html.js-anim .bento.px-playing .bento-grid .bento-tile:not(.is-match) {
  animation: bentoTileIn 0.4s var(--px-ease) both,
             bentoDim 0.5s var(--px-ease-soft) 1.25s forwards;
}
html.js-anim .bento.px-playing .bento-grid .bento-tile:not(.is-match):nth-child(2) { animation-delay: 0.38s, 1.25s; }
html.js-anim .bento.px-playing .bento-grid .bento-tile:not(.is-match):nth-child(4) { animation-delay: 0.54s, 1.30s; }
html.js-anim .bento.px-playing .bento-grid .bento-tile:not(.is-match):nth-child(6) { animation-delay: 0.70s, 1.35s; }

/* Card 3 — the photo falls into the tray, which lights as it lands */
html.js-anim .bento.px-playing .bento-drop {
  animation: bentoDropIn 1.5s var(--px-ease-soft) 0.55s both;
}

html.js-anim .bento.px-playing .bento-tray {
  animation: bentoTrayLit 1.9s var(--px-ease-soft) 0.55s both;
}


/* ───────────────────────────────────────────────────────────────────────
   Pricing — the plan assembles

   No new markup: the existing name, price, note, divider, feature rows and
   button are sequenced into view. Roughly 1.2s, played once.

   Nothing here animates .plan itself. It carries data-tilt and
   data-spotlight, so premium-fx.js writes inline transforms to it, and a
   keyframe with a forwards fill would outrank those and kill the hover —
   the defect that once broke lightbox drag-to-dismiss.
   ─────────────────────────────────────────────────────────────────────── */

@keyframes planRise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

@keyframes planPrice {
  from { opacity: 0; transform: translateY(10px) scale(0.9); }
  to   { opacity: 1; transform: none; }
}

@keyframes planRule {
  from { opacity: 0; transform: scaleX(0); }
  to   { opacity: 1; transform: none; }
}

/* Hidden-at-start — without js-anim the card renders complete */
html.js-anim .plan .plan__name,
html.js-anim .plan .plan__price,
html.js-anim .plan .plan__price-note,
html.js-anim .plan .plan__divider,
html.js-anim .plan .plan__features li,
html.js-anim .plan .btn-plan {
  opacity: 0;
}

html.js-anim .plan.px-playing .plan__name       { animation: planRise 0.5s var(--px-ease) 0.05s both; }
html.js-anim .plan.px-playing .plan__price      { animation: planPrice 0.6s var(--px-spring) 0.16s both; }
html.js-anim .plan.px-playing .plan__price-note { animation: planRise 0.5s var(--px-ease) 0.30s both; }

/* The rule draws outward from its left edge as the divider it is. */
html.js-anim .plan.px-playing .plan__divider {
  transform-origin: left center;
  animation: planRule 0.5s var(--px-ease) 0.40s both;
}

html.js-anim .plan.px-playing .plan__features li {
  animation: planRise 0.45s var(--px-ease) both;
}

html.js-anim .plan.px-playing .plan__features li:nth-child(1) { animation-delay: 0.50s; }
html.js-anim .plan.px-playing .plan__features li:nth-child(2) { animation-delay: 0.57s; }
html.js-anim .plan.px-playing .plan__features li:nth-child(3) { animation-delay: 0.64s; }
html.js-anim .plan.px-playing .plan__features li:nth-child(4) { animation-delay: 0.71s; }
html.js-anim .plan.px-playing .plan__features li:nth-child(5) { animation-delay: 0.78s; }
html.js-anim .plan.px-playing .plan__features li:nth-child(6) { animation-delay: 0.85s; }
html.js-anim .plan.px-playing .plan__features li:nth-child(7) { animation-delay: 0.92s; }

html.js-anim .plan.px-playing .btn-plan {
  animation: planRise 0.5s var(--px-ease) 1.02s both;
}


/* ───────────────────────────────────────────────────────────────────────
   Demo phone — Gallery / Upload views

   The phone's two nav items became real buttons so the upload screen can be
   shown. They are a showcase: the upload view is a still of what a guest
   sees, with no file input and nothing to submit.

   Every string reuses an existing event.* translation key, so this adds no
   copy to translate.
   ─────────────────────────────────────────────────────────────────────── */

/* The nav items are <button> now, which brings UA background, border and font
   with it. Strip those back to how the spans rendered. */
.demo__nav-item {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  margin: 0;
  font: inherit;
  font-size: 0.58rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 200ms var(--px-ease-soft), color 200ms var(--px-ease-soft);
}

.demo__nav-item:hover {
  color: var(--primary);
  background: rgba(242, 204, 164, 0.07);
}

/* Keyboard users need to see where they are; the phone is inside a section
   with its own dark surfaces, so the ring has to be bright. */
.demo__nav-item:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* One view at a time. Both occupy the flex:1 slot between the stats row and
   the nav, so the phone never changes height when switching. */
.demo__upload {
  flex: 1;
  display: none;
  flex-direction: column;
  gap: 9px;
  padding: 12px 12px 4px;
  text-align: center;
}

.demo__screen.is-uploading .demo__grid { display: none; }
.demo__screen.is-uploading .demo__upload { display: flex; }

.demo__upload-title {
  font-family: 'Noto Serif', Georgia, serif;
  font-size: 0.95rem;
  color: var(--text);
  line-height: 1.2;
}

.demo__upload-sub {
  font-size: 0.6rem;
  color: var(--text-muted);
  margin-top: -4px;
}

/* Quota — the real page shows remaining uploads above the drop zone. */
.demo__quota {
  text-align: left;
  padding: 7px 9px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(242, 204, 164, 0.1);
}

.demo__quota-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 0.54rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 5px;
}

.demo__quota-row b {
  color: var(--primary);
  font-size: 0.62rem;
  letter-spacing: 0;
}

.demo__quota-bar {
  height: 3px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}

.demo__quota-bar i {
  display: block;
  height: 100%;
  width: 15%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--amber), var(--primary));
}

.demo__upload-label {
  font-size: 0.52rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  text-align: left;
}

/* Text-free pills, for the same reason the bento chips are: visible labels
   would be event-specific and would need translating. */
.demo__upload-chips {
  display: flex;
  gap: 5px;
  margin-top: -4px;
}

.demo__upload-chip {
  height: 15px;
  flex: 1;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(242, 204, 164, 0.1);
}

.demo__upload-chip.is-on {
  background: rgba(242, 204, 164, 0.16);
  border-color: rgba(242, 204, 164, 0.3);
}

.demo__dropzone {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 12px 10px;
  border-radius: 14px;
  border: 1px dashed rgba(242, 204, 164, 0.28);
  background: rgba(242, 204, 164, 0.03);
}

.demo__dropzone-logo {
  width: 26px;
  height: auto;
  opacity: 0.85;
}

.demo__dropzone-title {
  font-size: 0.6rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--primary);
}

.demo__dropzone-desc {
  font-size: 0.55rem;
  line-height: 1.5;
  color: var(--text-muted);
  margin: 0;
  max-width: 20ch;
}


/* ───────────────────────────────────────────────────────────────────────
   Mobile performance pass

   The landing page declares backdrop-filter on more than twenty elements,
   most of them large panels the visitor scrolls straight through: the bento
   grid and its cards, the pricing panel and plan, the closing CTA, the join
   form, the demo phone's chrome. Each one makes the browser resample and
   reblur everything behind it on every frame it or its backdrop moves, which
   is the standard cause of stuttering scroll on a phone GPU.

   Below 768px the blur is dropped and the affected surfaces are made solid
   enough to hold their own. There is very little parallax or depth visible
   behind a panel on a narrow screen, so the glass effect costs far more than
   it shows. Desktop keeps it in full.

   This block must stay in a stylesheet that loads after the page's inline
   <style>, or these rules lose the cascade to it at equal specificity.
   ─────────────────────────────────────────────────────────────────────── */

@media (max-width: 768px) {
  .nav__mobile-menu,
  .token-gate-overlay,
  .token-gate-card,
  .hero__subtitle,
  .join-form,
  .bento,
  .bento-card,
  .plans,
  .plan,
  .cta-final,
  .demo__chip,
  .demo__nav,
  .demo__toast,
  .px-glass {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  /* Surfaces that leaned on the blur for legibility need real opacity now.
     Everything else already sat at 0.75–0.97 alpha and reads unchanged. */
  .join-form {
    background: rgba(28, 24, 21, 0.92);
  }

  .bento-card,
  .plan {
    background: rgba(24, 21, 19, 0.94);
  }

  .demo__chip {
    background: rgba(10, 8, 8, 0.88);
  }

  .px-glass {
    background: rgba(24, 21, 19, 0.92);
  }
}


/* ───────────────────────────────────────────────────────────────────────
   Hero — living photo wall

   A mosaic of guest photographs behind the headline, continuously receiving
   new ones. Three depth bands parallax against the pointer; the tiles inside
   them fade out and back in on long, deliberately unsynchronised cycles.

   The wall is visible with no JavaScript at all. The rules below that hide
   anything are scoped under html.js-anim, so a failed script leaves a
   complete, still mosaic rather than an empty hero.

   Tiles are positioned around the type, never behind it: the centre column
   between roughly 25%–75% horizontally and 30%–70% vertically stays clear,
   which is where the headline, join form and hint sit.
   ─────────────────────────────────────────────────────────────────────── */

.hero__wall {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.hero__wall-band {
  position: absolute;
  inset: 0;
}

/* Scrim. Without it the mosaic reads as wallpaper — an even field of
   rectangles competing with the type. This sinks the centre, where the
   headline and join form sit, and the top and bottom edges, where the nav and
   the next section meet the hero, leaving the photographs strongest in a ring
   around the text.

   A pseudo-element rather than a mask or filter on .hero__wall: both of those
   would change how the wall's descendants are contained, and the hero has
   absolutely positioned children that must stay anchored to the section. */
.hero__wall::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(ellipse 62% 48% at 50% 48%,
        rgba(10, 8, 8, 0.94) 0%,
        rgba(10, 8, 8, 0.72) 42%,
        rgba(10, 8, 8, 0.2) 68%,
        transparent 82%),
    linear-gradient(to bottom,
        rgba(10, 8, 8, 0.92) 0%,
        rgba(10, 8, 8, 0.25) 18%,
        rgba(10, 8, 8, 0.25) 76%,
        rgba(10, 8, 8, 0.95) 100%);
}

.hero__wall-tile {
  position: absolute;
  display: block;
  background-size: cover;
  background-position: center;
  border-radius: 10px;
  opacity: var(--wt-o);

  /* No filter here, deliberately. The warm grade is baked into the WebP by
     scripts/build-wall-tiles.js instead. A filter on an animated element
     forces the browser to re-rasterise that layer, and there are ten of them
     on a phone; it would also have made this element a containing block. */
}

/* Shapes, carried in the markup because they describe the photograph rather
   than its placement. Sources are square; the crops give them these ratios. */
.wt-p { aspect-ratio: 3 / 4; }
.wt-s { aspect-ratio: 1 / 1; }
.wt-l { aspect-ratio: 4 / 3; }
.wt-t { aspect-ratio: 2 / 3; }

/* Depth: further back is smaller, dimmer, and moves least. */
.hero__wall-band--far  { --wt-o: 0.15; }
.hero__wall-band--mid  { --wt-o: 0.2; }
.hero__wall-band--near { --wt-o: 0.25; }

.hero__wall-band--far  .hero__wall-tile { width: 120px; }
.hero__wall-band--mid  .hero__wall-tile { width: 168px; }
.hero__wall-band--near .hero__wall-tile { width: 224px; }

/* ── Placement ─────────────────────────────────────────────────────────── */

.hero__wall-band--far .hero__wall-tile:nth-child(1) { top: 6%;  left: 4%; }
.hero__wall-band--far .hero__wall-tile:nth-child(2) { top: 13%; left: 22%; }
.hero__wall-band--far .hero__wall-tile:nth-child(3) { top: 4%;  left: 44%; }
.hero__wall-band--far .hero__wall-tile:nth-child(4) { top: 9%;  left: 68%; }
.hero__wall-band--far .hero__wall-tile:nth-child(5) { top: 17%; left: 86%; }
.hero__wall-band--far .hero__wall-tile:nth-child(6) { bottom: 8%;  left: 8%; }
.hero__wall-band--far .hero__wall-tile:nth-child(7) { bottom: 15%; left: 30%; }
.hero__wall-band--far .hero__wall-tile:nth-child(8) { bottom: 6%;  left: 58%; }
.hero__wall-band--far .hero__wall-tile:nth-child(9) { bottom: 13%; left: 80%; }

.hero__wall-band--mid .hero__wall-tile:nth-child(1) { top: 21%; left: -2%; }
.hero__wall-band--mid .hero__wall-tile:nth-child(2) { top: 31%; left: 12%; }
.hero__wall-band--mid .hero__wall-tile:nth-child(3) { top: 24%; left: 78%; }
.hero__wall-band--mid .hero__wall-tile:nth-child(4) { top: 35%; left: 90%; }
.hero__wall-band--mid .hero__wall-tile:nth-child(5) { bottom: 22%; left: 2%; }
.hero__wall-band--mid .hero__wall-tile:nth-child(6) { bottom: 26%; left: 72%; }
.hero__wall-band--mid .hero__wall-tile:nth-child(7) { bottom: 17%; left: 46%; }

.hero__wall-band--near .hero__wall-tile:nth-child(1) { top: 40%; left: -8%; }
.hero__wall-band--near .hero__wall-tile:nth-child(2) { top: 52%; left: 88%; }
.hero__wall-band--near .hero__wall-tile:nth-child(3) { bottom: -4%; left: 34%; }
.hero__wall-band--near .hero__wall-tile:nth-child(4) { top: -6%;    left: 70%; }
.hero__wall-band--near .hero__wall-tile:nth-child(5) { bottom: 30%; left: -4%; }

/* ── Arrival cycle ─────────────────────────────────────────────────────── */

/* A tile rests at its band opacity for most of the cycle, drops out, then
   returns brighter for a moment — a photograph landing in that slot. Only
   opacity and transform move, so the whole wall stays on the compositor. */
@keyframes wallArrive {
  0%, 86% {
    opacity: var(--wt-o);
    transform: scale(1);
  }
  88% {
    opacity: 0;
    transform: scale(0.9);
  }
  92% {
    opacity: calc(var(--wt-o) * 2.6);
    transform: scale(1.05);
  }
  100% {
    opacity: var(--wt-o);
    transform: scale(1);
  }
}

/* Phones get the same beat without the scale — opacity alone never forces a
   re-raster, so the wall costs the compositor nothing while the page moves. */
@keyframes wallArriveFlat {
  0%, 86%  { opacity: var(--wt-o); }
  88%      { opacity: 0; }
  92%      { opacity: calc(var(--wt-o) * 2.6); }
  100%     { opacity: var(--wt-o); }
}

html.js-anim .hero__wall-tile {
  animation-name: wallArrive;
  animation-timing-function: var(--px-ease-soft);
  animation-iteration-count: infinite;
}

/* Durations and delays are mutually prime so arrivals never fall into step.
   No forwards fill — an infinite animation does not need one, and a filling
   keyframe here would outrank the pointer parallax written to the bands. */
html.js-anim .hero__wall-band--far .hero__wall-tile:nth-child(1) { animation-duration: 23s; animation-delay: 0s; }
html.js-anim .hero__wall-band--far .hero__wall-tile:nth-child(2) { animation-duration: 29s; animation-delay: 7s; }
html.js-anim .hero__wall-band--far .hero__wall-tile:nth-child(3) { animation-duration: 31s; animation-delay: 13s; }
html.js-anim .hero__wall-band--far .hero__wall-tile:nth-child(4) { animation-duration: 19s; animation-delay: 3s; }
html.js-anim .hero__wall-band--far .hero__wall-tile:nth-child(5) { animation-duration: 37s; animation-delay: 17s; }
html.js-anim .hero__wall-band--far .hero__wall-tile:nth-child(6) { animation-duration: 23s; animation-delay: 11s; }
html.js-anim .hero__wall-band--far .hero__wall-tile:nth-child(7) { animation-duration: 29s; animation-delay: 5s; }
html.js-anim .hero__wall-band--far .hero__wall-tile:nth-child(8) { animation-duration: 31s; animation-delay: 19s; }
html.js-anim .hero__wall-band--far .hero__wall-tile:nth-child(9) { animation-duration: 19s; animation-delay: 9s; }

html.js-anim .hero__wall-band--mid .hero__wall-tile:nth-child(1) { animation-duration: 27s; animation-delay: 4s; }
html.js-anim .hero__wall-band--mid .hero__wall-tile:nth-child(2) { animation-duration: 33s; animation-delay: 14s; }
html.js-anim .hero__wall-band--mid .hero__wall-tile:nth-child(3) { animation-duration: 21s; animation-delay: 8s; }
html.js-anim .hero__wall-band--mid .hero__wall-tile:nth-child(4) { animation-duration: 29s; animation-delay: 20s; }
html.js-anim .hero__wall-band--mid .hero__wall-tile:nth-child(5) { animation-duration: 37s; animation-delay: 2s; }
html.js-anim .hero__wall-band--mid .hero__wall-tile:nth-child(6) { animation-duration: 23s; animation-delay: 16s; }
html.js-anim .hero__wall-band--mid .hero__wall-tile:nth-child(7) { animation-duration: 31s; animation-delay: 10s; }

html.js-anim .hero__wall-band--near .hero__wall-tile:nth-child(1) { animation-duration: 35s; animation-delay: 6s; }
html.js-anim .hero__wall-band--near .hero__wall-tile:nth-child(2) { animation-duration: 41s; animation-delay: 18s; }
html.js-anim .hero__wall-band--near .hero__wall-tile:nth-child(3) { animation-duration: 29s; animation-delay: 12s; }
html.js-anim .hero__wall-band--near .hero__wall-tile:nth-child(4) { animation-duration: 37s; animation-delay: 24s; }
html.js-anim .hero__wall-band--near .hero__wall-tile:nth-child(5) { animation-duration: 31s; animation-delay: 0s; }

/* ── Phones ────────────────────────────────────────────────────────────── */

/* Ten tiles instead of twenty-one, and slower cycles. display:none on a
   background-image element means the hidden tiles are never downloaded. */
@media (max-width: 768px) {
  .hero__wall-band--near { display: none; }
  .hero__wall-band--far .hero__wall-tile:nth-child(n+7) { display: none; }
  .hero__wall-band--mid .hero__wall-tile:nth-child(n+5) { display: none; }

  .hero__wall-band--far .hero__wall-tile { width: 96px; }
  .hero__wall-band--mid .hero__wall-tile { width: 132px; }

  /* The desktop scrim is sized in viewport percentages, so on a narrow, tall
     screen its ellipse covers the entire frame and the wall disappears. Here
     it wraps the text block only — wide and shallow — leaving photographs
     visible above and below it. */
  .hero__wall::after {
    background:
      radial-gradient(ellipse 96% 26% at 50% 44%,
          rgba(10, 8, 8, 0.94) 0%,
          rgba(10, 8, 8, 0.7) 48%,
          rgba(10, 8, 8, 0.18) 76%,
          transparent 92%),
      linear-gradient(to bottom,
          rgba(10, 8, 8, 0.88) 0%,
          rgba(10, 8, 8, 0.2) 16%,
          rgba(10, 8, 8, 0.2) 80%,
          rgba(10, 8, 8, 0.94) 100%);
  }

  .hero__wall-band--far { --wt-o: 0.19; }
  .hero__wall-band--mid { --wt-o: 0.24; }

  /* Matched to the specificity of the per-tile rules above — a plain
     .hero__wall-tile selector here would lose to them and silently do
     nothing. Equal specificity, later in the file, so this wins.

     Phones also get the opacity-only cycle: scaling a layer forces it to be
     re-rasterised mid-scroll, which is exactly what a phone cannot spare. */
  html.js-anim .hero__wall-band .hero__wall-tile:nth-child(n) {
    animation-duration: 47s;
    animation-name: wallArriveFlat;
  }
}
