/* ---------------------------------------------------------------------------
   Showcase: the parts that put the real product on the page.

   Added 2026-08-22 after the owner ruled the first build too plain. What that
   ruling overrides, explicitly: the earlier ban on device mockups, on floating
   UI fragments, on gradients, and on shadows heavier than level1. What it does
   NOT override: the palette and faces come from theme/v2, colour is still data,
   there are still no pills and no decorative hues, and every pixel of product
   imagery here is an unretouched capture of the shipped app.
   --------------------------------------------------------------------------- */

/* --- the phone ------------------------------------------------------------- */

/* A CSS bezel, not a photograph of hardware. It scales to any width, costs no
   bytes, stays crisp on any display, and raises no licensing question. */
.device {
  position: relative;
  width: 100%;
  max-width: 240px;
  margin-inline: auto;
  padding: var(--device-bezel);
  background: linear-gradient(160deg, #2c2c30 0%, #141417 46%, #232327 100%);
  border-radius: var(--device-radius);
  box-shadow: var(--elev-device);
}

/* A hairline rim. Without it the near-black bezel disappears into the dark
   chapter and the phone loses its edge. */
.device::after {
  position: absolute;
  inset: 0;
  content: "";
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
  pointer-events: none;
}

.device__screen {
  position: relative;
  overflow: hidden;
  border-radius: calc(var(--device-radius) - var(--device-bezel));
  background-color: var(--canvas);
  /* 1320 x 2868 is the capture size; reserving it stops any layout shift. */
  aspect-ratio: 1320 / 2868;
}

.device__screen img,
.device__screen video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.device--lg {
  max-width: 272px;
}

.device--sm {
  max-width: 200px;
}

/* --- the stage the phone stands on ----------------------------------------- */

/* A soft warm pool of the page's own canvas and inset tones. It gives the
   device somewhere to sit instead of floating on nothing. No purple, no glow,
   no colour that is not already in the palette. */
.stage {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding-block: var(--space-group);
  isolation: isolate;
}

.stage::before {
  position: absolute;
  z-index: -1;
  inset: 6% 0 4%;
  content: "";
  border-radius: 50%;
  background: radial-gradient(
    78% 58% at 50% 45%,
    var(--inset) 0%,
    color-mix(in srgb, var(--inset) 55%, transparent) 55%,
    transparent 78%
  );
}

/* The warm pool is made of the light page's own tones, so on the dark chapter
   it reads as a muddy brown halo. There it takes the dark raised surface. */
.section--dark .stage::before {
  background: radial-gradient(
    78% 58% at 50% 45%,
    var(--dark-raised) 0%,
    color-mix(in srgb, var(--dark-raised) 50%, transparent) 55%,
    transparent 78%
  );
}

/* --- dark chapter ---------------------------------------------------------- */

.section--dark {
  background-color: var(--dark);
  color: var(--on-dark);
  border-top: 0;
}

.section--dark h2,
.section--dark h3 {
  color: var(--on-dark);
}

.section--dark .lede,
.section--dark .caption,
.section--dark .eyebrow,
.section--dark p {
  color: var(--on-dark-secondary);
}

.section--dark .button--secondary {
  border-color: var(--dark-hairline);
  color: var(--on-dark);
}

.section--dark .button--secondary:hover {
  border-color: var(--on-dark);
  color: var(--on-dark);
}

.section--dark .play-badge__soon {
  color: var(--on-dark);
}

/* --- the tour video -------------------------------------------------------- */

.tour {
  display: grid;
  gap: var(--space-block);
  align-items: center;
}

.tour__copy > * + * {
  margin-top: var(--space-section);
}

/* Flat numbered rows, not equal cards. Each row is a hairline apart from the
   next, which is the same structural device the app uses for a ledger. */
.tour__steps {
  list-style: none;
  margin-top: var(--space-section);
}

.tour__step {
  display: grid;
  grid-template-columns: 2.25rem minmax(0, 1fr);
  gap: var(--space-label);
  padding-block: var(--space-group);
  border-top: var(--stroke-hairline) solid var(--dark-hairline);
  color: var(--on-dark);
}

.tour__step:last-child {
  border-bottom: var(--stroke-hairline) solid var(--dark-hairline);
}

.tour__num {
  font-family: var(--font-mono);
  font-size: var(--fs-sub);
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
  color: var(--brass);
}

.tour__step-body {
  font-size: var(--fs-sub);
  color: color-mix(in srgb, var(--on-dark) 78%, transparent);
}

.tour__step-title {
  display: block;
  margin-bottom: var(--space-tight);
  font-weight: 600;
  color: var(--on-dark);
}

.tour__note {
  margin-top: var(--space-section);
  font-size: var(--fs-caption);
}

/* Sits under the phone, on the dark chapter, and inherits the site's own
   secondary button. Rectangular with the card radius, never a pill. */
.video-toggle {
  margin-top: var(--space-section);
  gap: var(--space-label);
}

.video-toggle__icon {
  flex: none;
  width: 10px;
  height: 12px;
  background-color: currentcolor;
}

/* Two bars while it plays, one triangle while it is paused. Drawn in the
   button's own colour, so it follows the focus and hover states for free. */
.video-toggle[data-state="playing"] .video-toggle__icon {
  clip-path: polygon(0 0, 35% 0, 35% 100%, 0 100%, 0 0, 65% 0, 100% 0, 100% 100%, 65% 100%, 65% 0);
}

.video-toggle[data-state="paused"] .video-toggle__icon {
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}

@media (min-width: 62rem) {
  .tour {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr);
    gap: clamp(40px, 6vw, 96px);
  }

  .tour__figure {
    order: 1;
  }
}

/* A stage can carry its own disclosure, so the sample-data note sits with the
   screenshot it describes rather than floating in the copy column. */
/* --text-muted is a 2.88:1 tone meant for a rule or a mark, not for 14px
   text. A disclosure the reader has to be able to read takes the secondary
   text colour. */
.stage__note {
  margin-top: var(--space-label);
  font-size: var(--fs-caption);
  color: var(--text-secondary);
}

.section--dark .stage__note {
  color: color-mix(in srgb, var(--on-dark) 78%, transparent);
}

/* --- a short section with one supporting screen ----------------------------- */

/* Same idea as .showcase, at a smaller weight: the copy leads and the phone is
   evidence beside it, not the subject. */
.aside {
  display: grid;
  gap: var(--space-block);
  align-items: center;
}

.aside__copy > * + * {
  margin-top: var(--space-section);
}

.aside__copy .prose {
  max-width: 52ch;
}

@media (min-width: 62rem) {
  .aside {
    grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    gap: clamp(40px, 5vw, 72px);
  }
}

/* --- big product figure, alternating ---------------------------------------- */

.showcase {
  display: grid;
  gap: var(--space-block);
  align-items: center;
}

.showcase + .showcase {
  margin-top: var(--space-block-sm);
  padding-top: var(--space-block-sm);
  border-top: var(--stroke-hairline) solid var(--hairline);
}

.showcase__copy > * + * {
  margin-top: var(--space-section);
}

@media (min-width: 62rem) {
  .showcase {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: clamp(40px, 5vw, 80px);
  }

  .showcase--reverse .showcase__figure {
    order: -1;
  }
}

/* --- scroll reveal ---------------------------------------------------------- */

/* Only ever applied when JavaScript is running. With scripting off nothing is
   hidden, and under prefers-reduced-motion the movement is dropped and the
   element simply appears.

   Two blocks that enter the viewport together do not move together: the
   script gives each one in the batch its own `--reveal-delay`, in 90ms steps
   from the top of the screen down, so a screenful reads as one gesture rather
   than a single jump. Inside a block the phone trails the copy by 110ms, so the
   words land first and the picture answers them. */
html[data-js] .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity 520ms var(--ease-standard) var(--reveal-delay, 0ms),
    transform 520ms var(--ease-standard) var(--reveal-delay, 0ms);
}

html[data-js] .reveal.is-in {
  opacity: 1;
  transform: none;
}

html[data-js] .reveal .evidence__figure {
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity 520ms var(--ease-standard) calc(var(--reveal-delay, 0ms) + 110ms),
    transform 520ms var(--ease-standard) calc(var(--reveal-delay, 0ms) + 110ms);
}

html[data-js] .reveal.is-in .evidence__figure {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  html[data-js] .reveal,
  html[data-js] .reveal .evidence__figure {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* Print never waits for a scroll. */
@media print {
  html[data-js] .reveal,
  html[data-js] .reveal .evidence__figure {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* --- hero entrance ---------------------------------------------------------- */

/* The one animation on the page that runs without the script, because the hero
   is on screen before any deferred script has run and must not paint, vanish,
   and paint again. The `from` frame is the only place opacity is ever 0, so a
   browser with no animation support, or a print, simply shows the hero.

   The phone is the largest paint on the page, so it only rises. Fading it
   would hold back the moment the browser counts the page as drawn. */
@keyframes hero-rise-in {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
}

@keyframes hero-rise {
  from {
    transform: translateY(14px);
  }
}

.hero__copy > * {
  animation: hero-rise-in 520ms var(--ease-standard) both;
}

.hero__copy > :nth-child(2) {
  animation-delay: 70ms;
}

.hero__copy > :nth-child(3) {
  animation-delay: 140ms;
}

.hero__copy > :nth-child(4) {
  animation-delay: 210ms;
}

.hero__copy > :nth-child(n + 5) {
  animation-delay: 280ms;
}

.hero__stage {
  animation: hero-rise 520ms var(--ease-standard) 120ms both;
}

@media (prefers-reduced-motion: reduce) {
  .hero__copy > *,
  .hero__stage {
    animation: none;
  }
}

/* --- sticky header, condensed once the page moves --------------------------- */

.site-header {
  transition:
    border-color var(--dur-standard) var(--ease-standard),
    background-color var(--dur-standard) var(--ease-standard);
}

@supports (backdrop-filter: blur(12px)) {
  html[data-js] .site-header[data-scrolled] {
    background-color: color-mix(in srgb, var(--canvas) 82%, transparent);
    backdrop-filter: blur(14px) saturate(140%);
  }
}

/* --- closing call ----------------------------------------------------------- */

.final {
  display: grid;
  gap: var(--space-block);
  align-items: center;
}

.final__copy > * + * {
  margin-top: var(--space-section);
}

.final__copy .lede {
  max-width: 42ch;
}

.final__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-label);
}

.final__mark {
  justify-self: start;
}

.final__mark img {
  width: 112px;
  height: 112px;
  border-radius: 24px;
}

@media (min-width: 48rem) {
  .final {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-block);
  }

  .final__mark {
    justify-self: end;
  }

  /* Never past 120px: that is the source resolution of the extracted app icon,
     and upscaling it softens the tally. Raise this only once a 1024px icon
     source exists (open question OQ-19). */
  .final__mark img {
    width: 120px;
    height: 120px;
    border-radius: 26px;
  }
}
