/* Pendi landing: extended sections (steps, phone gallery, family, demo, signup). */

.section__head--center {
  margin-inline: auto;
  text-align: center;
  max-width: 46ch;
}

/* ---- How capture works: a real 3-step sequence ---- */
.steps {
  /* This is an <ol>: kill the native decimal markers and their indent so the
     only visible numbering is the styled .step__n badges inside each card. */
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.1rem;
  counter-reset: step;
}
.steps > .step::marker {
  content: none;
}
.step {
  position: relative;
  padding: clamp(1.4rem, 3vw, 1.9rem);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  background: var(--surface);
}
.step__n {
  width: 42px;
  height: 42px;
  border-radius: 13px;
  display: grid;
  place-items: center;
  background: color-mix(in oklch, var(--brand) 14%, var(--surface));
  color: var(--brand-ink);
  font-family: var(--display);
  font-weight: 720;
  font-size: 1.15rem;
  margin-bottom: 1.05rem;
}
.step h3 {
  font-size: 1.18rem;
  letter-spacing: -0.01em;
}
.step p {
  color: var(--muted);
  margin-top: 0.55rem;
  font-size: 0.99rem;
}

/* ---- Phone frames + screenshot gallery ---- */
.gallery {
  display: grid;
  grid-auto-flow: column;
  /* A FIXED track, not a minmax. The cards need to be wider than the row can
     fit, so that several sit off-screen and the arrows have somewhere to go.
     minmax(200px, 250px) cannot do that: a track only grows towards its ceiling
     out of FREE space, and five 200px minimums already overflow the row, so
     every track stays pinned at 200px and nothing changes. The min() keeps a
     card from ever being wider than the screen it is scrolling inside. */
  grid-auto-columns: min(250px, 80vw);
  gap: clamp(1rem, 2.5vw, 1.6rem);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding: 0.4rem 0.2rem 1.4rem;
  /* No native scrollbar under the phones: it cut a grey rule across the
     section. Hiding the bar hides nothing else, wheel, drag, touch, keyboard
     and scroll-snap all still work. The affordances move to the buttons and
     the edge fades below. */
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Fallback while media.js has not run: fade both ends. It is the safe guess,
     since a strip that cannot scroll at all simply has nothing under the
     gradient. */
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 26px,
    #000 calc(100% - 26px),
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 26px,
    #000 calc(100% - 26px),
    transparent 100%
  );
}
.gallery::-webkit-scrollbar {
  display: none;
}

/* ---- Screenshot strip: desktop navigation ----
   media.js marks the strip .js-carousel and then owns the fades, so an edge
   only softens when there is really something behind it. That is why the
   default under .js-carousel is no mask at all: the classes below add back
   exactly the side that has content. */
.gallery.js-carousel {
  -webkit-mask-image: none;
  mask-image: none;
}
.gallery.js-carousel.fade-start {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 26px);
  mask-image: linear-gradient(to right, transparent 0, #000 26px);
}
.gallery.js-carousel.fade-end {
  -webkit-mask-image: linear-gradient(to left, transparent 0, #000 26px);
  mask-image: linear-gradient(to left, transparent 0, #000 26px);
}
.gallery.js-carousel.fade-start.fade-end {
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 26px,
    #000 calc(100% - 26px),
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 26px,
    #000 calc(100% - 26px),
    transparent 100%
  );
}
/* Offsets are 0 because an absolutely positioned child resolves against its
   containing block's PADDING box, and .wrap's padding edge is exactly where
   the strip starts and ends. */
.gallery-wrap {
  position: relative;
}
.gallery-nav {
  position: absolute;
  top: 50%;
  /* Lift off the figcaption band so the chevrons centre on the phones rather
     than on the phones plus their labels. */
  margin-top: -1.4rem;
  translate: 0 -50%;
  z-index: 2;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  border: 1px solid var(--hairline);
  background: color-mix(in oklch, var(--surface) 86%, transparent);
  backdrop-filter: saturate(180%) blur(10px);
  color: var(--ink);
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: background 0.2s var(--ease), color 0.2s var(--ease),
    border-color 0.2s var(--ease), opacity 0.2s var(--ease),
    transform 0.2s var(--ease);
}
.gallery-nav:hover {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--on-brand);
  transform: scale(1.06);
}
.gallery-nav:active {
  transform: scale(0.96);
}
.gallery-nav--prev {
  left: 0;
}
.gallery-nav--next {
  right: 0;
}
/* Hidden at the end it points at. visibility, not just opacity, so it leaves
   the tab order instead of sitting there as a focusable no-op. */
.gallery-nav.is-off {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
/* Touch already has the swipe these stand in for. */
@media (hover: none) {
  .gallery-nav {
    display: none;
  }
}
@media (hover: hover) {
  .gallery.js-carousel {
    cursor: grab;
  }
}
.gallery.is-dragging,
.gallery.is-dragging .phone__screen img {
  cursor: grabbing;
}
.gallery.is-dragging {
  user-select: none;
}
.phone {
  scroll-snap-align: center;
  margin: 0;
}
.phone__screen {
  aspect-ratio: 1080 / 2400;
  border-radius: 28px;
  overflow: hidden;
  border: 7px solid color-mix(in oklch, var(--ink) 82%, #000);
  background: #0b0910;
  box-shadow: var(--shadow);
  position: relative;
}
.phone__screen img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}
.phone figcaption {
  text-align: center;
  margin-top: 0.75rem;
  font-size: 0.85rem;
  color: var(--muted);
}
.shot-dark {
  display: none;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .shot-dark {
    display: block;
  }
  :root:not([data-theme="light"]) .shot-light {
    display: none;
  }
}
:root[data-theme="dark"] .shot-dark {
  display: block;
}
:root[data-theme="dark"] .shot-light {
  display: none;
}
:root[data-theme="light"] .shot-dark {
  display: none;
}
:root[data-theme="light"] .shot-light {
  display: block;
}

/* ---- Family spaces (split on a tinted band) ---- */
.split {
  display: grid;
  grid-template-columns: 1.02fr 0.98fr;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  background: var(--bg-tint);
  border: 1px solid var(--hairline);
  border-radius: var(--r-xl);
  padding: clamp(1.8rem, 4vw, 3.4rem);
}
.split h2 {
  font-size: clamp(1.6rem, 1.2rem + 1.8vw, 2.3rem);
}
.split p {
  color: var(--muted);
  margin-top: 0.9rem;
  max-width: 42ch;
}
.split ul {
  list-style: none;
  padding: 0;
  margin-top: 1.3rem;
  display: grid;
  gap: 0.7rem;
}
.split li {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  font-size: 0.99rem;
}
.split li svg {
  flex: none;
  margin-top: 2px;
  color: var(--brand-ink);
}
.split__art {
  display: flex;
  justify-content: center;
  gap: 1rem;
}
.split__art .phone {
  width: min(58%, 220px);
}
.split__art .phone:nth-child(2) {
  margin-top: 2.4rem;
}

/* ---- Demo placeholder (intentional poster, accepts a real GIF later) ---- */
.demo-wrap {
  display: grid;
  place-items: center;
}
/* Bounded on purpose. The poster used to inherit the phone's 1080/2400 ratio,
   which at this width is about 660px of mostly empty violet for one line of
   copy. Capping the height makes it an object of one glance; the gradient, the
   amber wash, the glyph and the brand shadow are unchanged. */
.demo-frame {
  width: min(340px, 82vw);
  aspect-ratio: 1080 / 2400;
  max-height: 360px;
  border-radius: 30px;
  border: 7px solid color-mix(in oklch, var(--ink) 82%, #000);
  background: linear-gradient(160deg, var(--violet), #4c1d95 78%);
  position: relative;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 1rem;
  overflow: hidden;
  box-shadow: var(--shadow-brand);
  text-align: center;
  padding: 1.5rem;
}
.demo-frame::before {
  content: "";
  position: absolute;
  inset: -30% 30% auto -30%;
  height: 60%;
  background: radial-gradient(circle, oklch(0.82 0.16 82 / 0.3), transparent 65%);
}
.demo-play {
  width: 78px;
  height: 78px;
  border-radius: 999px;
  background: oklch(1 0 0 / 0.16);
  border: 1px solid oklch(1 0 0 / 0.34);
  display: grid;
  place-items: center;
  color: #fff;
  position: relative;
  z-index: 1;
}
.demo-frame p {
  color: oklch(0.95 0.03 293);
  font-size: 0.95rem;
  max-width: 22ch;
  position: relative;
  z-index: 1;
}
.demo-frame b {
  color: #fff;
  font-family: var(--display);
  font-size: 1.15rem;
}

/* ---- Beta signup / waitlist ---- */
.signup {
  text-align: center;
  border: 1px solid var(--hairline);
  background: linear-gradient(
    150deg,
    var(--bg-tint),
    color-mix(in oklch, var(--brand) 8%, var(--surface))
  );
  border-radius: var(--r-xl);
  padding: clamp(2.2rem, 5vw, 3.6rem);
}
.signup h2 {
  font-size: clamp(1.7rem, 1.2rem + 2vw, 2.5rem);
  max-width: 20ch;
  margin-inline: auto;
}
.signup > p {
  color: var(--muted);
  margin-top: 0.85rem;
  max-width: 48ch;
  margin-inline: auto;
}
.wl-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  max-width: 470px;
  margin: 1.5rem auto 0;
}

/* The Turnstile widget is a fixed-size iframe, so as a plain flex item it competed with the email field
   for the row and squashed it to a stub. It takes the whole line instead and drops below the field and
   the button, which is also where a person expects a verification step: after what you typed, before
   you are done. Wrapping is what makes that work, so [.wl-form] gained flex-wrap for this and only this. */
.wl-form .cf-turnstile {
  flex: 0 0 100%;
  display: flex;
  justify-content: center;
  /* Reserve the widget's own height so the panel does not jump when the iframe finishes loading. */
  min-height: 65px;
}
.wl-form input[type="email"] {
  flex: 1;
  min-width: 0;
  padding: 0.85rem 1.15rem;
  border-radius: 999px;
  /* A control edge, not a decorative hairline: it is the only thing that
     separates the field from the tinted panel, so it needs 3:1. */
  border: 1px solid var(--field-border);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 1rem;
}
.wl-form input::placeholder {
  color: var(--muted);
}
.wl-form input:focus-visible {
  border-color: var(--brand);
  outline-offset: 1px;
}
.wl-form input[aria-invalid="true"] {
  border-color: var(--danger);
}
.wl-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
.wl-status {
  min-height: 1.4em;
  margin-top: 0.95rem;
  font-weight: 620;
  font-size: 0.95rem;
}
.wl-status[data-state="success"] {
  color: var(--ok);
}
.wl-status[data-state="error"] {
  color: var(--danger);
}
.wl-note {
  font-size: 0.82rem;
  color: var(--muted);
  margin-top: 0.9rem;
}
.wl-note a,
.wl-fallback a {
  color: var(--brand-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ---- Product video player ----
   LIVE since the refresh: the video section now carries a real <video> inside
   .video-frame, with the old .demo-frame poster kept beside it as .demo-fallback.
   Which of the two a visitor sees is decided at runtime by the media.js HEAD probe,
   not here, so a missing or half-deployed file degrades to the poster instead of a
   broken player. The full 1080/2400 ratio is deliberate: the poster could be capped
   at 360px because it was one line of copy, but cropping a real phone recording
   would cut the screen it exists to show. */
.video-frame {
  display: grid;
  place-items: center;
  width: 100%;
}
.demo-video {
  width: min(320px, 80vw);
  /* height:auto is load-bearing, not tidiness. A <video>'s width and height
     ATTRIBUTES are presentational hints that map to CSS width and height, and this
     rule only overrode the width, so the attribute height won and the element
     rendered 320x1200 instead of 320x711: the right picture in a box two-thirds
     too tall. With height auto the aspect-ratio below is what decides. */
  height: auto;
  aspect-ratio: 1080 / 2400;
  border-radius: 30px;
  border: 7px solid color-mix(in oklch, var(--ink) 82%, #000);
  background: #0b0910;
  box-shadow: var(--shadow-brand);
  object-fit: cover;
}
.demo-video[hidden] {
  display: none;
}
.demo-fallback[hidden] {
  display: none;
}

/* ---- Lightbox (native <dialog>, dependency-free) ---- */
.gallery .phone__screen img {
  cursor: zoom-in;
}
dialog.lightbox {
  border: 0;
  padding: 0;
  margin: 0;
  max-width: 100vw;
  max-height: 100dvh;
  width: 100%;
  height: 100%;
  background: transparent;
  overflow: hidden;
  place-items: center;
}
dialog.lightbox[open] {
  display: grid;
}
dialog.lightbox::backdrop {
  background: oklch(0.14 0.02 293 / 0.84);
  backdrop-filter: blur(6px);
}
.lightbox__img {
  max-width: min(92vw, 460px);
  max-height: 90dvh;
  width: auto;
  height: auto;
  border-radius: 26px;
  border: 6px solid color-mix(in oklch, var(--ink) 82%, #000);
  box-shadow: var(--shadow-brand);
}
.lightbox__close {
  position: fixed;
  top: clamp(0.9rem, 3vw, 1.6rem);
  right: clamp(0.9rem, 3vw, 1.6rem);
  width: 46px;
  height: 46px;
  border-radius: 999px;
  border: 1px solid oklch(1 0 0 / 0.32);
  background: oklch(1 0 0 / 0.16);
  color: #fff;
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: background 0.2s var(--ease);
}
.lightbox__close:hover {
  background: oklch(1 0 0 / 0.28);
}

/* ---- Home-screen widget (the Today widget section) ----
   The widget is DRAWN in CSS rather than shipped as a screenshot, because
   assets/ carries no widget capture yet and an invented PNG would be a claim
   about pixels nobody has taken. Every value here is a page token, so the card
   follows the site's light/dark the same way the real widget follows the
   phone's system mode. When a real capture exists, replace the whole
   .widget-mock figure with the usual <img class="shot-light">/<img
   class="shot-dark"> pair; the copy beside it does not change. */
.widget-show {
  display: grid;
  grid-template-columns: 0.95fr 1.05fr;
  gap: clamp(1.6rem, 4vw, 3.2rem);
  align-items: center;
}
.widget-show__art {
  display: grid;
  place-items: center;
  padding-block: clamp(0.8rem, 3vw, 2rem);
}
.widget-mock {
  position: relative;
  margin: 0;
  width: min(100%, 440px);
}
/* The page glow. Anchored to the card rather than to the column, so it reads
   as light coming off the widget instead of a blob parked beside it: one
   violet radial, centred on the card and fading to nothing well before its own
   box edge, so it never draws a seam and never catches a pointer. The card
   itself carries --shadow-brand, the same violet cast the hero tile and the
   primary button wear. */
.widget-mock::before {
  content: "";
  position: absolute;
  inset: -34% -20%;
  background: radial-gradient(
    54% 58% at 50% 48%,
    oklch(0.6 0.22 293.5 / 0.22),
    transparent 70%
  );
  pointer-events: none;
  z-index: 0;
}
.widget-mock__card {
  position: relative;
  z-index: 1;
  display: flex;
  gap: 0.75rem;
  padding: 0.95rem 1rem;
  border-radius: var(--r-lg);
  border: 1px solid var(--hairline);
  background: var(--surface);
  box-shadow: var(--shadow-brand);
}
.widget-mock__body {
  flex: 1;
  min-width: 0;
}
.widget-mock__eyebrow {
  color: var(--muted);
  font-size: 0.82rem;
  font-weight: 560;
  margin-bottom: 0.6rem;
}
.widget-mock__rows {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 0.55rem;
}
.widget-mock__row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
}
.widget-mock__check {
  flex: none;
  width: 19px;
  height: 19px;
  border-radius: 6px;
  border: 2px solid var(--field-border);
}
/* One line, truncated rather than wrapped, because that is what the widget
   itself does: Glance text has no shrink-to-fit and the rows are maxLines 1. */
.widget-mock__title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.9rem;
  font-weight: 560;
  color: var(--ink);
}
.widget-mock__row--hero .widget-mock__title {
  font-size: 1.02rem;
  font-weight: 720;
}
/* One treatment for all three times. The widget itself draws the next Pendi's
   time as a filled chip and the rest plain, but at three rows that reads as a
   drifting baseline rather than as emphasis, so the mock keeps the times as a
   quiet right-aligned column and lets the hero's heavier title carry the
   promotion on its own. */
.widget-mock__time {
  flex: none;
  text-align: right;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--muted);
}
.widget-mock__rail {
  flex: none;
  display: grid;
  gap: 0.5rem;
  align-content: space-between;
}
.widget-mock__tile {
  width: 38px;
  height: 38px;
  border-radius: 12px;
  display: grid;
  place-items: center;
  background: color-mix(in oklch, var(--brand) 12%, var(--surface));
  color: var(--brand-ink);
}
.widget-mock figcaption {
  position: relative;
  z-index: 1;
  text-align: center;
  margin-top: 0.85rem;
  font-size: 0.85rem;
  color: var(--muted);
}
.widget-points {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 0.9rem;
}
.widget-points li {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  font-size: 0.99rem;
  color: var(--muted);
}
.widget-points li svg {
  flex: none;
  margin-top: 3px;
  color: var(--brand-ink);
}
.widget-points b {
  color: var(--ink);
}

@media (max-width: 780px) {
  .steps {
    grid-template-columns: 1fr;
  }
  .split {
    grid-template-columns: 1fr;
  }
  .split__art {
    order: -1;
  }
  .widget-show {
    grid-template-columns: 1fr;
  }
  .widget-show__art {
    order: -1;
  }
}
/* On the narrowest phones the card gives up chrome, not title. Trimming the
   padding, the rail gap and the tiles buys back the ~25px that otherwise
   pushes the row titles into their ellipsis at a 320px viewport. */
@media (max-width: 400px) {
  .widget-mock__card {
    padding: 0.85rem 0.75rem;
    gap: 0.55rem;
  }
  .widget-mock__tile {
    width: 34px;
    height: 34px;
  }
}
@media (max-width: 620px) {
  .wl-form {
    flex-direction: column;
  }
  .wl-form .btn {
    justify-content: center;
  }
}

/* ---- Google Play badge: official artwork, shown unaltered with its own clear space ---- */
.hero .hero__cta {
  align-items: center;
}
.play-badge {
  display: inline-block;
  line-height: 0;
}
.play-badge img {
  width: 180px;
  height: auto;
}
