/* =========================================================
   Contexto — landing.css
   landing.html only, top to bottom in page order: hero (with the
   language picker and the card's placement), regeneration band,
   conversation band, closing line.

   The flashcard widget itself is a separate file —
   hero-flashcard.css — loaded before this one. What lives here is
   only how the landing hero *places* that card: every such rule is
   scoped to .landing-hero__demo.
   ========================================================= */

/* Landing runs to the hero's measure, not the 820px prose one: its sections
   cap their own copy (see .regen__lede) and want the extra width for the
   screenshot strip. The hero band above already supplies the top gap.
   Overrides .about-wrap in base.css — same specificity, so this file has to
   load second, which layout.html's block order guarantees. */
.about-wrap--landing {
  max-width: 1120px;
  padding-top: 0;
}

/* ---------- LANDING HERO ----------
   Sits outside .about-wrap in its own full-width band so it can run to a
   wider measure (--hero-width) than the 820px prose column below it. */
.landing-hero {
  --hero-width: 1120px;
  padding: 4.75rem 0 5.25rem;
}

.landing-hero__grid {
  max-width: var(--hero-width);
  margin: 0 auto;
  padding: 0 1.5rem;
  display: grid;
  /* minmax(0, …) so a long unbroken sentence can't push a column wider than
     its share. Slightly wider left column: the copy carries the hero. */
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: 3.5rem;
  align-items: center;
}

/* Left column: headline → spine → badge → languages, all flush left and
   ragged right. Gaps are per-element (margins) rather than one flex `gap`,
   because the four items want visibly different amounts of air. */
.landing-hero__intro {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
}

.landing-hero__caption {
  font-size: clamp(2rem, 4.6vw, 3.25rem);
  line-height: 1.08;
  margin: 0;
  font-weight: 700;
  color: var(--color-secondary);
  letter-spacing: -0.01em;
}

/* The "spine" — the one sentence that states the product. The left rule
   echoes the sentence bar inside the flashcard beside it, so the claim reads
   as the same kind of object as the thing it describes. */
.landing-hero__spine {
  margin: 1.15rem 0 0;
  border-left: 3px solid var(--color-primary);
  padding-left: 0.9rem;
  max-width: 44ch;
  font-size: 1.25rem;
  line-height: 1.55;
  color: var(--color-muted);
}

.landing-hero__store {
  margin-top: 1.75rem;
}

.landing-hero__store img {
  height: 52px;
  width: auto;
  display: block;
}

.landing-hero__demo {
  display: flex;
  justify-content: center;
}

/* ---------- LANGUAGE PICKER (inside the hero, under the badge) ----------
   Radio inputs are visually hidden rather than display:none so they keep
   native keyboard behaviour; all visual state hangs off :checked + label.
   The visible label is a plain <span>, not the <legend> (which carries the
   a11y name via aria-label on the fieldset instead): browsers give <legend>
   its own layout, anchored to the border and exempt from the parent's flex
   properties, so it can't reliably sit inline beside the flags. */
.lang-picker {
  border: 0;          /* fieldset defaults */
  padding: 0;
  min-width: 0;       /* fieldsets refuse to shrink below content otherwise */
  max-width: 100%;
  margin: 1.85rem 0 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.6rem;
}

.lang-picker__label {
  font-size: 0.875rem;
  font-weight: 700;
  color: var(--color-muted);
  opacity: 0.75;
  white-space: nowrap;
}

.lang-picker__options {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.lang-picker__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}

/* Flag-only tiles. The name is gone from the visible chip (it survives as
   the img alt / label title), so the flag has to be large enough to identify
   on its own. The 2px border is transparent when unselected so selecting a
   tile changes colour without changing layout. */
.lang-picker__opt {
  display: block;
  padding: 6px;
  background: transparent;
  border: 2px solid transparent;
  border-radius: 12px;
  cursor: pointer;
  transition: background-color 140ms ease-out, border-color 140ms ease-out,
              transform 140ms ease-out;
}

.lang-picker__flag {
  width: 64px;
  height: auto;
  display: block;
  border-radius: 4px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
  /* unselected flags recede so the active one reads at a glance */
  filter: saturate(0.3) opacity(0.65);
  transition: filter 140ms ease-out, box-shadow 140ms ease-out;
}

/* Hover: the tile lifts and its flag comes back to (nearly) full colour with
   a deeper shadow. Two channels at once — motion and colour — because the
   chips carry no other affordance that says "control". */
.lang-picker__opt:hover {
  background: rgba(0, 0, 0, 0.05);
  transform: translateY(-2px);
}

.lang-picker__opt:hover .lang-picker__flag {
  filter: saturate(0.85) opacity(0.95);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
}

.lang-picker__input:checked + .lang-picker__opt {
  border-color: var(--color-primary);
  background: rgba(211, 47, 47, 0.07);
}

.lang-picker__input:checked + .lang-picker__opt .lang-picker__flag {
  filter: none;
}

.lang-picker__input:focus-visible + .lang-picker__opt {
  outline: 2px solid var(--color-secondary);
  outline-offset: 2px;
}

/* LOAD HINT — touch users never hover, so one unselected tile bounces once,
   shortly after load, to say the row is tappable. It runs on the second chip
   only (nth-of-type counts labels, so that's the first non-default language)
   and only while that chip is unselected, so a fast picker isn't animated
   after they've already chosen. `backwards` (not `both`) holds the from-state
   through the delay without persisting the end-state afterwards: a forwards
   fill would keep overriding this one tile's hover lift for good. */
@keyframes lang-picker-hint {
  0%   { transform: none; }
  45%  { transform: translateY(-6px) scale(1.06); }
  70%  { transform: translateY(0) scale(0.99); }
  100% { transform: none; }
}

.lang-picker__input:not(:checked) + .lang-picker__opt:nth-of-type(2) {
  animation: lang-picker-hint 520ms ease-out 900ms 1 backwards;
}

/* Hovering mid-bounce would fight it — a running animation outranks any
   declared transform — so cancel it the moment the pointer arrives and let
   the hover lift take over. Selector matches the rule above plus :hover, or
   it would lose on specificity rather than order. */
.lang-picker__input:not(:checked) + .lang-picker__opt:nth-of-type(2):hover {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .lang-picker__opt,
  .lang-picker__flag {
    transition: none;
  }

  .lang-picker__input:not(:checked) + .lang-picker__opt:nth-of-type(2) {
    animation: none;
  }

  .lang-picker__opt:hover {
    transform: none;
  }
}

/* ---------- THE FLASHCARD, AS PLACED IN THE HERO ----------
   The widget's own styles are in hero-flashcard.css. These rules are the
   hero's framing of it and nothing else — hence the .landing-hero__demo
   scope on every selector.

   Slight tilt so the card reads as "picked up off the table"; it straightens
   while hovered/focused (the cycle pauses then too). The angle lives in
   --card-tilt rather than the transform itself so the swivel keyframes can
   swing the card without flattening it — and so the narrow-screen
   breakpoint can change the angle in one place. */
.landing-hero__demo .hero-flashcard {
  --card-tilt: -4deg;
  transform: rotate(var(--card-tilt));
  transition: transform 250ms ease-out;
}

.landing-hero__demo .hero-flashcard:hover,
.landing-hero__demo .hero-flashcard:focus-within {
  transform: rotate(0deg);
}

/* LANGUAGE-SWAP SWIVEL — picking a flag rewrites the word and the sentence
   set, but the click lands in the picker, several inches from the card that
   actually changed. The card turns so the change is caught even if the eye
   was still on the flags. Added by hero-flashcard.js (.is-swiveling) on every
   accepted swap.

   The card swings through its own tilt rather than sliding sideways. It is
   already sitting at --card-tilt, so rocking about that angle reads as the
   card turning to face you — the tilt stops being a static styling detail and
   becomes the axis the gesture happens on.

   What made the previous version read as an *error* was never its direction:
   a translateX applied after a rotate already travels along the card's own
   rotated axis, not the screen's. It was the rate. Six reversals in 420ms is
   a head-shake "no". This is four, over half again as long, each swing
   smaller than the last — a pendulum coming to rest rather than a buzzer.

   The first swing goes positive, lifting the card past upright toward the
   reader, so it opens by perking up rather than by flinching. */
@keyframes hero-flashcard-swivel {
  0%   { transform: rotate(var(--card-tilt))                scale(1);    }
  16%  { transform: rotate(calc(var(--card-tilt) + 5deg))   scale(1.02); }
  38%  { transform: rotate(calc(var(--card-tilt) - 3.5deg)) scale(1.01); }
  58%  { transform: rotate(calc(var(--card-tilt) + 2deg))   scale(1);    }
  78%  { transform: rotate(calc(var(--card-tilt) - 1deg))   scale(1);    }
  100% { transform: rotate(var(--card-tilt))                scale(1);    }
}

/* ease-in-out per segment is the pendulum part — quickest through the middle
   of each swing, slowest at the turns. The decay does the settling. */
.landing-hero__demo .hero-flashcard.is-swiveling {
  animation: hero-flashcard-swivel 640ms ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
  .landing-hero__demo .hero-flashcard {
    transition: none;
  }

  .landing-hero__demo .hero-flashcard.is-swiveling {
    animation: none;
  }
}

/* Stack before the two columns get too narrow to hold a 46ch line beside a
   full-size card. Copy stays flush left when stacked — the spine's left rule
   only reads as a rule against a straight edge. */
@media (max-width: 860px) {
  .landing-hero {
    padding: 3rem 0 4rem;
  }

  /* Stacked, the hero reads as one sequence: claim → spine → the card that
     demonstrates it → badge → languages. Left to itself the card lands last,
     after the badge and the flags, where it reads as a separate block that
     happens to follow the hero rather than as the hero's own evidence.

     display: contents dissolves the intro wrapper so its four children and
     the card become siblings in the same grid and can be interleaved by
     `order`. The wrapper is a plain div carrying no semantics, so nothing is
     lost by removing its box; above this breakpoint it stays and holds the
     left column exactly as before.

     gap goes to 0 because it would otherwise apply between all five children
     at once. Spacing is already carried by each element's own margin — the
     card is the only one that needs a new one. */
  .landing-hero__intro {
    display: contents;
  }

  .landing-hero__grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  .landing-hero__caption { order: 1; }
  .landing-hero__spine   { order: 2; }
  .landing-hero__demo    { order: 3; }
  .lang-picker           { order: 4; }
  .landing-hero__store   { order: 5; }

  /* The flags follow the card, reversing the desktop order. They are a
     control *for* the card — picking one re-languages it — and side by side
     on desktop that reads on its own. Stacked it does not, and leaving the
     badge between them put the CTA in the middle of a control and the thing
     it operates. So the card and its languages group, and the badge closes
     the hero rather than interrupting it.

     The margins carry the grouping: the flags sit close under the card, and
     the badge takes more air so it reads as its own beat. */
  .landing-hero__demo {
    margin: 2.5rem 0 0.25rem;
  }

  .lang-picker {
    margin-top: 1.35rem;
  }

  .landing-hero__store {
    margin-top: 2.25rem;
  }

  .landing-hero__demo .hero-flashcard {
    --card-tilt: -2deg; /* gentler tilt on narrow screens */
  }

  /* 64px flags are sized against the desktop headline. Stacked, the headline
     drops to its 2rem floor while the flags stay fixed, so the row ends up
     louder than the sentence above it — and four of them at full size nearly
     fill a 375px column. */
  .lang-picker__flag {
    width: clamp(44px, 12vw, 56px);
  }
}

/* ---------- REGENERATION SECTION ----------
   One word, three reviews, three sentences — shown as a strip of full app
   screenshots so the claim above is visible rather than asserted. A dark
   full-bleed band: it is the page's one piece of evidence, and the inversion
   marks it off from the hero above and the closing line below.
   The band itself is edge-to-edge; .regen__inner holds the content column,
   which is why the horizontal padding lives there and not here. */
.regen {
  margin-top: 3rem;   /* air between the hero band and this one */
  padding: 3.5rem 0 3rem;
  background: var(--color-muted);
  text-align: center;
}

.regen__inner {
  max-width: 1120px;   /* same column as the hero and .about-wrap--landing */
  margin: 0 auto;
  padding: 0 1.5rem;
}

/* On the dark band every text colour is the page background tinted down by
   opacity, so the whole block stays on one hue. */
.regen__title {
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  font-weight: 700;
  color: var(--color-bg);
  margin: 0 0 1rem;
}

/* Capped independently of the wrap: the strip below wants the full width,
   the prose wants a readable measure. */
.regen__lede {
  max-width: 56ch;
  margin: 0 auto 0.7rem;
  font-size: 1.1875rem;
  line-height: 1.6;
  color: var(--color-bg);
  opacity: 0.86;
}

/* Screenshots run full height — no crop, no lightbox, they are legible as
   rendered. Their source heights differ slightly (1165/1264/1285px), so
   tops align and the bottom edges sit a little ragged by design. */
.regen__strip {
  list-style: none;
  margin: 2rem auto 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: start;
  gap: 1.25rem;
}

.regen__label {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--color-bg);
  opacity: 0.6;
}

/* No shadow: the frames are flat, non-interactive plates. The white cards
   already separate themselves from the dark ground; the hairline is only
   there to define their edge where the screenshot's own red bars meet it
   (--color-border is black-based and vanishes on this background). */
.regen__frame {
  margin: 0;
  border: 1px solid rgba(252, 252, 252, 0.14);
  border-radius: 14px;
  overflow: hidden;
}

.regen__frame img {
  display: block;
  width: 100%;
}

.regen__caption {
  margin: 1.1rem 0 0;
  font-size: 0.875rem;
  font-style: italic;
  color: var(--color-bg);
  opacity: 0.7;
}

/* Three columns stop being legible well before the phone breakpoint: at
   ~110px each the sentence text is unreadable. Stacking them was the first
   answer, but three whole app screenshots in a column ran to 2000px — a
   quarter of the entire mobile page — so the strip turns and scrolls sideways
   instead, the same move the forms shelf makes.

   It also states the claim better than a stack does: swiping from the 1st
   review to the 9th is the point of the section, and a column asks you to
   scroll past the evidence rather than through it.

   Snapping to `start`, not `center` like the forms shelf: this is a sequence
   read left to right from the first review, so card one should sit flush at
   rest — the forms shelf centres because its subject card is the middle one. */
@media (max-width: 760px) {
  .regen {
    padding: 2.5rem 0 2.25rem;
  }

  .regen__inner {
    padding: 0 1.25rem;
  }

  .regen__strip {
    display: flex;
    grid-template-columns: none;
    max-width: none;
    gap: 1rem;
    overflow-x: auto;
    overflow-y: hidden;   /* without this the strip becomes a vertical trap */
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Full-bleed inside the band, but the cards still line up with the copy's
       margin at either end. */
    margin-inline: -1.25rem;
    padding-inline: 1.25rem;
  }

  .regen__strip::-webkit-scrollbar {
    display: none;
  }

  .regen__step {
    flex: 0 0 auto;
    width: 76vw;
    max-width: 330px;
    scroll-snap-align: start;
  }
}

/* ---------- CONVERSATION SECTION ----------
   Full-bleed tinted band that butts directly against the dark regeneration
   band above it — no margin, so the two read as one continuous run of colour
   and the shift from dark to tint is the only divider. White resumes below.
   The tint is --color-primary at low alpha rather than a new hex; the rgba
   line is the fallback for engines without color-mix(). */
.convo {
  --phone-width: 280px;   /* the auto grid track sizes itself to this */

  padding: 3.5rem 0;
  background: rgba(211, 47, 47, 0.06);
  background: color-mix(in srgb, var(--color-primary) 6%, var(--color-bg));
}

.convo__inner {
  max-width: 1120px;   /* same column as the hero and the band above */
  margin: 0 auto;
  padding: 1rem 1.5rem;
}

/* The video is portrait (886x1920): at any sensible column width it is
   height, not width, that runs away. So the phone gets a fixed width and the
   `auto` track collapses onto it, leaving the rest of the row to the copy —
   rather than an fr split that would strand dead space beside the video.
   The phone stays flush to the column's left edge, on the same spine as the
   hero copy and the screenshot strip above. */
.convo__grid {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 3rem;
  align-items: center;
}

/* 56ch, the same measure as .regen__lede in the band above — wide enough to
   fill the track, so the row has no dead space at its right end. The cap
   lives here rather than on each paragraph so the title obeys it too. */
.convo__text {
  max-width: 56ch;
}

/* Device bezel. box-sizing: border-box is global, so the padding and border
   sit inside --phone-width. */
.convo__phone {
  width: var(--phone-width);
  padding: 6px;
  background: var(--color-bg);
  border: 2px solid var(--color-muted);
  border-radius: 20px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.14);
}

/* aspect-ratio holds the slot before metadata arrives, so the band does not
   jump when the video loads. */
.convo__video {
  display: block;
  width: 100%;
  aspect-ratio: 886 / 1920;
  border-radius: 14px;
  background: var(--color-muted);
}

.convo__title {
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  font-weight: 700;
  color: var(--color-secondary);
  margin: 0 0 0.85rem;
}

.convo__body {
  margin: 0;
  font-size: 1.1875rem;
  line-height: 1.6;
  color: var(--color-muted);
}

/* Same left rule as the hero spine: this is the beat the video never shows,
   so it has to carry itself typographically. */
.convo__pullout {
  margin: 1.25rem 0 0;
  border-left: 3px solid var(--color-primary);
  padding-left: 0.9rem;
  font-size: 1.1875rem;
  line-height: 1.6;
  font-weight: 600;
  color: var(--color-text);
}

@media (max-width: 860px) {
  .convo {
    padding: 2.5rem 0;
  }

  .convo__inner {
    padding: 0 1.25rem;
  }

  .convo__grid {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: 2.25rem;
  }

  /* Copy stays flush left under the centred phone — the pull-out's rule
     needs a straight edge, same as the hero spine. Tighter than the 56ch
     above: stacked, there is no track left to fill, and the narrower measure
     keeps the text block closer to the phone's width. */
  .convo__text {
    max-width: 46ch;
  }
}

/* ---------- MASTERY SECTION ----------
   The lightest of the three claim sections: no band, no frame, no shadow —
   plain page background, a diagram, and air. The conversation band above ends
   in colour, so the return to white is itself the divider and this section
   needs no rule of its own. */
/* Deeper below than above: the extra room is what separates this section's
   caption from the forms band's edge, so the two claims do not read as one
   run of text with a colour change in the middle. */
.mastery {
  padding: 4.5rem 0 6rem;
}

.mastery__inner {
  max-width: 1120px;   /* same column as every section above */
  margin: 0 auto;
  padding: 0 1.5rem;
}

/* Mirror of .convo__grid: there the fixed track is first, here it is last,
   which is what puts this section's visual on the opposite side. The diagram
   track sizes itself to the SVG's own max-width rather than taking an fr —
   it must never grow wider than the screenshot columns above it. */
.mastery__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 3.5rem;
  align-items: center;
}

.mastery__text {
  max-width: 52ch;
}

.mastery__title {
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  font-weight: 700;
  color: var(--color-secondary);
  margin: 0 0 0.85rem;
}

.mastery__body {
  margin: 0 0 0.9rem;
  font-size: 1.1875rem;
  line-height: 1.6;
  color: var(--color-muted);
}

.mastery__body:last-child {
  margin-bottom: 0;
}

/* The diagram's own palette. Kept as custom properties on the figure (rather
   than hard-coded into the SVG) so the shapes stay token-driven like the rest
   of the page: red is the logo red, ink is body-copy colour, and the two
   greys are that same ink thinned against the page ground. The rgba lines are
   the fallback for engines without color-mix(). */
.mastery__figure {
  --mx-red: var(--color-primary);
  --mx-ink: var(--color-muted);

  --mx-muted: rgba(42, 44, 65, 0.62);
  --mx-muted: color-mix(in srgb, var(--color-muted) 62%, var(--color-bg));

  --mx-line: rgba(42, 44, 65, 0.3);
  --mx-line: color-mix(in srgb, var(--color-muted) 30%, var(--color-bg));

  margin: 0;
  width: 100%;
  max-width: 420px;
}

/* font-family is not set: the SVG's <text> inherits Dosis from the page, so
   the labels match the body copy instead of dropping to a serif fallback. */
.mastery__diagram {
  display: block;
  width: 100%;
  height: auto;
}

.mastery__caption {
  margin: 1.1rem 0 0;
  text-align: center;
  font-size: 0.875rem;
  font-style: italic;
  color: var(--color-muted);
  opacity: 0.7;
}

/* Same breakpoint as the conversation section above, so the two rows collapse
   together rather than one at a time. Stacked, the copy leads: the header
   carries the claim and the diagram only illustrates it. */
@media (max-width: 860px) {
  .mastery {
    padding: 3rem 0 4rem;
  }

  .mastery__inner {
    padding: 0 1.25rem;
  }

  .mastery__grid {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: 2.25rem;
  }

  .mastery__text {
    max-width: 46ch;
  }
}

/* ---------- FORMS SECTION ----------
   Copy above a full-width shelf of five cards. Centred like the regeneration
   band (the other copy-above-visual section) rather than flush left like the
   two-column ones. */
/* A full-bleed band, like the two above it — .forms__inner carries the column
   inside, which is why the horizontal padding lives there and not here.

   The tint is --color-muted (the regeneration band's own colour) at 5%, so
   the faintest wash on the page is an echo of the darkest one and the palette
   stays at two hues. Cooler and quieter than the conversation band's red 6%,
   which is the right order: that section is a Tier 1 claim, this one is
   credibility. It also leaves mastery as the only white claim section, which
   is what keeps mastery the quietest of the three.
   The rgba line is the fallback for engines without color-mix(). */
.forms {
  padding: 4rem 0 4.5rem;
  text-align: center;
  background: rgba(42, 44, 65, 0.05);
  background: color-mix(in srgb, var(--color-muted) 5%, var(--color-bg));
}

.forms__inner {
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 1.5rem;
}

.forms__title {
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  font-weight: 700;
  color: var(--color-secondary);
  margin: 0 0 0.85rem;
}

/* Same measure as .regen__lede — the shelf below wants the full width, the
   prose wants a readable line. */
.forms__body {
  max-width: 56ch;
  margin: 0 auto 0.7rem;
  font-size: 1.1875rem;
  line-height: 1.6;
  color: var(--color-muted);
}

/* THE SHELF.
   The five crops are not one aspect ratio (0.767–0.894), so equal widths give
   ragged heights — same as the regeneration strip, and fine here because the
   row is staggered on purpose. align-items: center makes the ragged edge
   symmetrical instead of hanging off the bottom.

   No overlap between cards. The handoff allows a slight one, but every crop
   carries its morph tags in a row spanning the card's full width, so any
   overlap at all would cover a tag — and tags fully visible is the harder
   requirement. Depth comes from the centre card's size and shadow instead. */
.forms__shelf {
  --card-w: clamp(150px, 15.5vw, 196px);

  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.75rem;
  margin: 2.25rem auto 0;
}

.forms__card {
  flex: 0 0 auto;   /* never shrink: below ~900px the end cards crop instead */
  width: var(--card-w);
  margin: 0;
  border: 1px solid var(--color-border);
  border-radius: 14px;
  overflow: hidden;
  background: var(--color-bg);
}

.forms__card img {
  display: block;
  width: 100%;
  height: auto;
}

/* Each card is a control. Stripped back to the image it holds — the figure
   around it already carries the border, radius and clipping. */
.forms__zoom {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: zoom-in;
}

/* Inset, because the figure's overflow: hidden would clip a ring drawn
   outside the button. */
.forms__zoom:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: -3px;
}

/* Appended to the image's own description so the button's accessible name
   says what it shows AND what activating it does. */
.zoom-hint {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* The stagger. Applied as margin, not transform — transform belongs to the
   entrance animation below, and the two would overwrite each other. Under
   align-items: center the margin box is what gets centred, so a 48px margin
   on one side moves the card 24px the other way. */
.forms__card--low {
  margin-top: 48px;
}

.forms__card--high {
  margin-bottom: 48px;
}

/* llegamos: the form named in the header, so it is the only card that scales
   up and the only one with a shadow. Sized rather than transform: scale() for
   the same reason as the offsets above. */
.forms__card--hero {
  width: calc(var(--card-w) * 1.16);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12);
}

.forms__caption {
  margin: 1.6rem 0 0;
  font-size: 0.875rem;
  font-style: italic;
  color: var(--color-muted);
  opacity: 0.7;
}

/* ENTRANCE — the cards rise into place once, when the shelf first scrolls
   into view.

   The hidden state is opt-in: the script in landing.html adds --animate only
   once it knows it can observe, so with no JS (or no IntersectionObserver)
   the cards never hide in the first place. Hiding them in plain CSS and
   relying on JS to reveal them would leave the whole shelf invisible if the
   script never ran. */
.forms__shelf--animate .forms__card {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.forms__shelf--animate.is-in-view .forms__card {
  opacity: 1;
  transform: translateY(0);
}

.forms__shelf--animate.is-in-view .forms__card:nth-child(1) { transition-delay: 0ms; }
.forms__shelf--animate.is-in-view .forms__card:nth-child(2) { transition-delay: 80ms; }
.forms__shelf--animate.is-in-view .forms__card:nth-child(3) { transition-delay: 160ms; }
.forms__shelf--animate.is-in-view .forms__card:nth-child(4) { transition-delay: 240ms; }
.forms__shelf--animate.is-in-view .forms__card:nth-child(5) { transition-delay: 320ms; }

@media (prefers-reduced-motion: reduce) {
  .forms__shelf--animate .forms__card {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* TABLET — the end cards crop against the container edge. Deliberate: a
   cropped edge implies more forms beyond the five, which is the claim.

   The width tracks the viewport so the crop stays proportional. A fixed width
   here cropped the end cards down to a blank sliver of their own margin at the
   narrow end of the range, which reads as a broken image rather than an
   intentional cut. This keeps roughly a third to a half of each end card
   showing across the range — enough that it still reads as a card. */
@media (max-width: 900px) {
  .forms__shelf {
    --card-w: clamp(160px, 22vw, 190px);

    overflow: hidden;
  }
}

/* MOBILE — the shelf becomes a snap-scroll strip. Equal cards, no hero
   scaling and no stagger: at this width the offsets only cost vertical space,
   and the scroll position is what carries the "there are more" idea now.

   Switched at 700px rather than the handoff's ~600: between 600 and 700 the
   cropped shelf leaves too little of the end cards to read, and the cards
   themselves get too small for their own morph tags. The strip handles that
   band better. */
@media (max-width: 700px) {
  .forms {
    padding: 3rem 0;
  }

  .forms__inner {
    padding: 0 1.25rem;
  }

  .forms__shelf {
    display: flex;
    gap: 0.75rem;
    justify-content: flex-start;
    overflow-x: auto;
    overflow-y: hidden;   /* without this the strip becomes a vertical trap */
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Full-bleed: the strip runs edge to edge, but the first and last cards
       still line up with the copy's margin when scrolled to either end. */
    margin-inline: -1.25rem;
    padding-inline: 1.25rem;
  }

  .forms__shelf::-webkit-scrollbar {
    display: none;
  }

  .forms__card,
  .forms__card--hero {
    width: 75vw;         /* the next card peeks in */
    margin: 0;           /* drops the stagger */
    scroll-snap-align: center;
  }

  .forms__card--hero {
    box-shadow: none;
  }
}

/* ---------- LIGHTBOX ----------
   The dialog box itself is only a frame: transparent, no chrome, sized to the
   image inside it. Everything visible is either the backdrop or the picture. */
.lightbox {
  margin: auto;   /* older engines centre modal dialogs this way */
  padding: 0;
  border: 0;
  background: transparent;
  max-width: 92vw;
  max-height: 92vh;
  overflow: visible;
  cursor: zoom-out;
}

.lightbox::backdrop {
  background: rgba(0, 0, 0, 0.78);
  cursor: zoom-out;
}

/* Capped on both axes so tall crops fit portrait windows and wide ones fit
   landscape, without either overflowing. */
.lightbox__img {
  display: block;
  width: auto;
  height: auto;
  max-width: 92vw;
  max-height: 92vh;
  border-radius: 18px;
  background: var(--color-bg);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}

/* Fixed rather than absolute: pinned to the viewport corner, so it does not
   ride on the image's edge and shift with every aspect ratio. */
.lightbox__close {
  position: fixed;
  top: 1rem;
  right: 1.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;    /* a real touch target */
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(252, 252, 252, 0.16);
  color: var(--color-bg);
  font-family: inherit;
  font-size: 1.75rem;
  line-height: 1;
  cursor: pointer;
}

.lightbox__close:hover {
  background: rgba(252, 252, 252, 0.28);
}

.lightbox__close:focus-visible {
  outline: 3px solid var(--color-bg);
  outline-offset: 2px;
}

/* ---------- UTILITY VIGNETTES ----------
   The supporting features. Ranked below the claim sections, not hidden: these
   are real reasons someone picks an app, and a visitor scanning for "does it
   do X?" has to be able to find them. The ranking is carried by scale — a
   smaller header, smaller frames, shorter copy — not by making the section
   look like something left over.

   Balanced padding, standing as its own section. It used to be padded far
   deeper below than above so it grouped upward as a coda to the forms band,
   which is exactly what made it read as an appendage. */
.utility {
  max-width: 1120px;
  margin: 0 auto;
  padding: 5rem 1.5rem 4.5rem;
}

/* A real section header, in the same red as the claim headers but a step down
   in size — the rank shows in the scale, not in hiding the section. */
.utility__title {
  margin: 0 0 2.75rem;
  text-align: center;
  font-size: clamp(1.25rem, 2.1vw, 1.5rem);
  font-weight: 700;
  color: var(--color-secondary);
}

/* Deliberately tighter than the gap between top-level sections (which run
   4–5rem of padding a side): the two vignettes have to read as a pair of
   moments inside one section, not as two sections that happen to share a
   header. */
.utility__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 3rem;
}

/* Copy above the panes and centred, following the regeneration band rather
   than the feature sections. The feature sections alternate text-left and
   text-right; borrowing that here would give these rows the same weight as
   the claims they sit below. */
.utility__vignette {
  text-align: center;
}

/* The feature's name, so the section is scannable. Without these each row was
   a sentence in grey and nothing announced what the feature *is* — which is
   what made real features read as filler. */
.utility__name {
  margin: 0 0 0.4rem;
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--color-secondary);
}

/* Normal body copy, at full opacity. Previously the smallest type on the page
   and dimmed to 85% on top of that, which put it below reading weight. */
.utility__line {
  max-width: 52ch;
  margin: 0 auto;
  font-size: 1rem;
  line-height: 1.55;
  color: var(--color-muted);
}

/* Equal widths, each frame at its own screenshot's height — tops align and
   the bottoms sit ragged, the same rule the regeneration strip follows.

   Equalising the heights instead was the first answer and it looked tidier,
   but it sizes each pane by its shot's ratio, and the speaking pair runs 1.23
   against 0.66: the after-pane came out 170px wide with its per-word scores
   unreadable — the smallest thing on the page, and the half of the pair that
   carries the claim. Equal widths also happen to be the honest drawing. Both
   panes of a pair are the same card before and after; what changed is that
   the after grew, and a ragged bottom edge is that growth.

   The old three-card row set one fixed frame and contained the shots inside
   it, which letterboxed badly here — these four run 1.75 to 0.66. Nothing
   sets a frame shape now; each frame takes the shape of its own shot.

   --pair is set per row in the markup and is what holds these below
   feature-section weight: both rows stay well inside the width the three
   cards used to occupy, and each pane comes out a little wider than the
   frames it replaces. */
.utility__panes {
  list-style: none;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 1.25rem;
  max-width: var(--pair);
  margin: 1.5rem auto 0;
  padding: 0;
}

.utility__pane {
  flex: 1 1 0;
  min-width: 0;
}

/* The connective tissue between the two panes, and the whole reason there is
   no arrow: this is the regeneration strip's step label ("1st review / 4th
   review"), same size and weight, re-inked for a light ground — the labels
   carry the before→after reading on their own, and reusing them ties the two
   sections together instead of adding a third way of showing a sequence. */
.utility__step {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--color-muted);
  opacity: 0.7;
}

/* The frame is also the zoom control, so it carries the button reset here
   rather than wrapping another element inside it. It has no size of its own:
   the shot inside sets the height, so the frame is always exactly the shape
   of the picture it holds.

   The padding is a mat around the shot, and it only reads as one because the
   frame has no shape of its own. It used to be paired with aspect-ratio on
   the frame, which put the ratio on the *border* box — the content box came
   out a slightly different shape from the image, object-fit letterboxed it on
   one axis, and the mat went uneven: about 9px at the sides against 13px top
   and bottom, on every pane. That read as a misaligned frame, and recropping
   the source could never fix it, because the extra white was the frame's, not
   the picture's. With the height coming from the image instead, the padding
   is the whole surround and is even by construction. */
.utility__frame {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0.5rem;
  border: 1px solid var(--color-border);
  border-radius: 14px;
  background: var(--color-bg);
  overflow: hidden;
  font: inherit;
  color: inherit;
  cursor: zoom-in;
}

/* Inset, like the shelf cards': the frame clips its own overflow, so a ring
   drawn outside the border would be cut off. */
.utility__zoom:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: -3px;
}

/* Fills the frame's content box exactly — no object-fit, so there is nothing
   to letterbox and the frame's padding is the only white around the shot. The
   width/height attributes in the markup are the shot's real pixel size, which
   is what holds the frame's height open before the image loads; recropping a
   screenshot means updating those two numbers and nothing here. */
.utility__shot {
  display: block;
  width: 100%;
  height: auto;
}

/* Side by side stops carrying legible UI well before the phone breakpoint —
   the tall speaking shot would be ~110px wide — so the panes stack, before
   above after. Same 760px the regeneration strip turns at.

   Capped at 260px rather than left full width: four stacked panes at column
   width would make a Tier 3 section one of the tallest on the page, which is
   the opposite of its rank. Detail is a tap away in the lightbox either way. */
@media (max-width: 760px) {
  .utility {
    padding: 3.5rem 1.25rem 3rem;
  }

  .utility__rows {
    gap: 2.5rem;
  }

  .utility__panes {
    flex-direction: column;
    align-items: center;
    max-width: none;
    gap: 1rem;
  }

  .utility__pane {
    flex: 0 0 auto;
    width: 100%;
    max-width: 260px;
  }
}

/* ---------- LANDING CLOSE (second half of the hero spine) ---------- */
/* No rule above it: the dark band's bottom edge is already the divider, and
   a hairline a few rem below it just reads as a second, weaker one. */
.landing-close {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.5rem;
  padding-top: 4rem;
  text-align: center;
}

/* Inked in --color-secondary, the same red as the line below it, so the mark
   and the payoff read as one stamp rather than two competing reds. Sized just
   under the header's 120px: the header is not sticky, so this is the first
   sight of the logo since the top of the page — but the line beneath it is
   still the thing that has to land, so the mark introduces it rather than
   outweighing it. Just above the header's 120px: on a white expanse the mark
   carries less than it does packed into the red bar, and below about 120 the
   wordmark sets smaller than any body copy on the page, which reads as
   undersized rather than restrained. */
.landing-close__logo {
  display: block;
  width: 200px;
  height: auto;
}

.landing-close__line {
  margin: 0;
  max-width: 32ch;
  font-size: clamp(1.375rem, 3.2vw, 1.875rem);
  font-weight: 700;
  line-height: 1.2;
  color: var(--color-secondary);
}

.landing-close__store img {
  height: 52px;
  width: auto;
  display: block;
}
