/* Landing page only — loaded by landing.html, not by the app.
 *
 * A different job from the catalogue: this page argues a case to a stranger,
 * so it gets room, scale and motion the app deliberately does not have. Keeping
 * it in its own file means a signed-in collector opening a card page never
 * downloads the marketing.
 */

/* 1320, to buy the fan its hover spread without taking width off the headline.
   1280 was not enough because the hero's own 56px padding sits *inside* the
   wrap's 56px gutter -- double-counted on the first attempt, which squeezed the
   text column from 462px to 418px and wrapped the headline onto three lines. */
body.landing .wrap { max-width: 1320px; }

/* Free of the card page's frame now (see .card-hero in app.css), so the panel
   here is deliberate: a soft wash and a hairline that is brightest along the top
   edge, which is the same "light from above" the glass language uses everywhere
   else. A flat bordered rectangle reads as a widget; this reads as a surface.

   And it has real padding. The headline previously sat 1px from a visible
   border, which is the whole of what felt tight. */
.hero {
  position: relative;
  display: grid;
  /* 0.72:1, not 1.05:0.95. The fan is 561px wide at rest and ~646px on hover,
     and it was living in a 418px column -- overflowing 71px to the left and
     reaching into the sub-copy. This gives the art column ~647px and leaves the
     text column at the ~465px it already had, so nothing reflows.
     minmax(0, ...) because an fr track's automatic minimum is its content, and
     a 68px headline would otherwise refuse to let the fan have the room. */
  grid-template-columns: minmax(0, 0.72fr) minmax(0, 1fr);
  /* --s-6 not --s-7 between the columns: the fan already keeps 67px of clear
     air inside its own track, so 56px of grid gap on top of that was width
     spent twice. */
  gap: var(--s-6); align-items: center;
  padding: var(--s-8) var(--s-7) var(--s-7);
  min-height: 66vh;
  border-radius: var(--r-lg);
  background:
    radial-gradient(120% 90% at 88% 8%, rgba(155, 93, 229, .13), transparent 62%),
    linear-gradient(180deg, rgba(255,255,255,.035), transparent 34%),
    var(--ground-1);
  box-shadow: 0 24px 70px rgba(0,0,0,.42);
}
/* The hairline, drawn rather than bordered, so it can fade out down the sides
   instead of ringing the panel at uniform weight. */
.hero::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit; padding: 1px;
  background: linear-gradient(180deg, rgba(255,255,255,.16), rgba(255,255,255,.02) 40%, transparent);
  -webkit-mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
}
.hero > * { position: relative; z-index: 1; }

@media (max-width: 1000px) {
  .hero { grid-template-columns: 1fr; gap: var(--s-6);
          padding: var(--s-7) var(--s-5) var(--s-6); }
}

.kicker {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .22em;
  text-transform: uppercase; color: var(--violet-lo); margin: 0 0 var(--s-5);
}
.hero-h1 {
  font-family: var(--font-display);
  font-weight: 800;
  /* 62 not 68: Archivo is a wider face than Inter at the same size, so the same
     headline needs more room per line. Measured, not guessed -- at 68px it took
     three lines where Inter took two. */
  font-size: clamp(38px, 5.6vw, 62px); line-height: 1.03;
  letter-spacing: -.035em; font-weight: 700; margin: 0 0 var(--s-5);
  text-wrap: balance;
}
.hero-h1 .dot { color: var(--live); }
.hero-sub {
  font-size: 17px; line-height: 1.68; color: var(--ink-2);
  max-width: 46ch; margin: 0 0 var(--s-6);
}

.hero-cta { display: flex; gap: var(--s-3); flex-wrap: wrap;
            margin-bottom: var(--s-7); }
.btn-primary {
  padding: 13px 28px; border-radius: 999px; font-size: 14.5px; font-weight: 600;
  color: #fff; background: linear-gradient(180deg, var(--violet), #7B44C9);
  box-shadow: 0 8px 26px rgba(155,93,229,.34);
  transition: transform .14s ease, box-shadow .18s ease, filter .14s ease;
}
.btn-primary:hover { filter: brightness(1.08); box-shadow: 0 12px 34px rgba(155,93,229,.46); }
.btn-primary:active { transform: translateY(1px); }
.btn-secondary {
  padding: 13px 24px; border-radius: 999px; font-size: 14.5px; font-weight: 500;
  color: var(--ink-2); border: 1px solid var(--glass-edge);
  transition: color .16s ease, border-color .16s ease, background .16s ease;
}
.btn-secondary:hover {
  color: var(--ink); border-color: rgba(255,255,255,.2);
  background: rgba(255,255,255,.04);
}

/* --s-6 not --s-7: at 56px the three figures wrapped to two lines in the
   hero's left column, and three numbers on one line is the whole point of
   putting them in a row. */
.hero-figures { display: flex; gap: var(--s-6); flex-wrap: wrap; margin: 0; }
.hero-figures dt {
  font-family: var(--mono); font-size: 24px; font-weight: 600; letter-spacing: -.02em;
  background: linear-gradient(180deg, var(--ink), var(--violet-lo));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero-figures dd {
  margin: 5px 0 0; font-size: 11px; text-transform: uppercase;
  letter-spacing: .1em; color: var(--ink-3);
}

/* ---------------------------------------------------------------- the fan */

/* --cw lives here, not on .fan-card. A custom property declared on the element
   that uses it beats the same property inherited from an ancestor, so the
   single-column override below was being silently ignored. */
.hero-art {
  position: relative; display: grid; place-items: center;
  --cw: clamp(108px, 10.6vw, 148px);
}

/* Tracks the card rather than sitting at a fixed 400px, so a smaller fan does
   not leave a band of dead space under itself. */
.fan { position: relative; width: 100%;
       height: calc(var(--cw, 148px) * 1.3969 + 96px); }

/* The arc lives in three custom properties rather than inside the transform, so
   hover changes the *numbers* and the transform is declared once. That is what
   lets the deal keyframes below land exactly on the resting position without
   restating the arc a third time and getting it subtly wrong. */
.fan-card {
  position: absolute; top: 50%; left: 50%;
  width: var(--cw); aspect-ratio: 733/1024;
  /* Half the card, so it stays centred on the anchor as it scales. */
  margin: calc(var(--cw) * 1.3969 / -2) 0 0 calc(var(--cw) / -2);
  border-radius: 9px; overflow: hidden;
  border: 1px solid rgba(255,255,255,.1);
  box-shadow: 0 18px 46px rgba(0,0,0,.55), 0 0 0 1px rgba(0,0,0,.3);

  /* The card scales, and everything derived from it scales too. At 148px fixed
     the fan measured 496px across, which overflowed the hero's right column
     around 1000px wide and spilled the cards over the sub-copy -- and 1000px is
     an extremely ordinary laptop. Deriving the offsets from --cw rather than
     hardcoding them is what makes one clamp enough. */
  --spread: calc(var(--cw) * 0.392);
  --lift: calc(var(--cw) * 0.034);
  --tilt: 7deg;
  /* Steps from the middle card, so the arc is symmetrical about card four. */
  --n: calc(var(--i) - 3);
  --fx: calc(var(--n) * var(--spread));
  --fy: calc(var(--n) * var(--n) * var(--lift));
  --fr: calc(var(--n) * var(--tilt));

  transform: translateX(var(--fx)) translateY(var(--fy)) rotate(var(--fr));

  /* Dealt, in two stages inside one animation: up from below the fold as a
     squared deck, then splayed open into the fan. The per-card delay staggers
     both stages, so it reads as a dealer flicking cards out one at a time
     rather than a block arriving, which is the gesture a fan of cards is
     already about. The final keyframe *is* the resting transform, so nothing
     snaps when the animation ends and no fill-mode is needed to hold it. */
  animation: fan-deal 1.05s cubic-bezier(.16,.84,.28,1) backwards;
  animation-delay: calc(var(--i) * 85ms);
  transition: transform .45s cubic-bezier(.2,.7,.3,1);
}
.fan-card img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Spreading on hover is the whole gesture. Three numbers, not a second
   transform. */
.fan:hover .fan-card { --spread: calc(var(--cw) * 0.473);
                       --lift: calc(var(--cw) * 0.020); --tilt: 9deg; }

/* Held at frame one until the art has decoded. The `backwards` fill means a
   paused animation is already showing its 0% keyframe -- below the fold and
   invisible -- so no separate hiding rule is needed, and therefore none can
   fight the animation once it runs. Scoped to .js so that with scripting off the
   fan animates on load exactly as before rather than waiting for a class nobody
   is going to add. */
.js .fan:not(.ready) .fan-card { animation-play-state: paused; }

@keyframes fan-deal {
  /* Below the fold, small and flat: a deck being lifted onto the table. Capped
     in px as well as vh so a tall monitor does not fling them a screen and a
     half away, while a short one still starts them out of sight. */
  0% {
    opacity: 0;
    transform: translateX(0) translateY(min(48vh, 400px)) rotate(0deg) scale(.9);
  }


  /* Squared up in the middle. Deliberately not perfectly aligned: a hair of
     rotation keeps it reading as seven cards rather than as one. */
  38% {
    opacity: 1;
    transform: translateX(0) translateY(0) rotate(calc(var(--n) * .6deg)) scale(1);
  }
  /* And open. */
  100% {
    opacity: 1;
    transform: translateX(var(--fx)) translateY(var(--fy)) rotate(var(--fr)) scale(1);
  }
}

/* ─────────────── the fan, responsively
 *
 * At the END of the fan's rules, deliberately. This has now been the same
 * mistake three times: a custom property overridden inside a media query that
 * sits *above* the rule declaring it does nothing, because at equal specificity
 * source order decides. --cw was overridden successfully and --spread silently
 * was not, so on a phone all seven cards stayed stacked behind an empty
 * aperture frame.
 *
 * Every breakpoint override for the fan belongs here, after every base value
 * it overrides. Adding one anywhere else is how this happens a fourth time.
 */
@media (max-width: 1000px) {
  /* A full column to itself: take the top of the clamp back. */
  .hero-art { --cw: 148px; }
  /* And it goes first.
   *
   * In a single column the fan was the second grid item, so it landed under the
   * headline, the buttons *and* the figures -- off the bottom of a phone
   * screen. The best moment on the site was the one nobody on a phone ever saw.
   * Reordering rather than restructuring keeps the source order right for
   * anything reading the page, where the headline should still come first. */
  /* Scoped to a direct child of .hero, because the coming-soon page also
     carries body.landing and was getting this -- which put the fan above its own
     logo. There the document order is already the wanted order. */
  .hero > .hero-art { order: -1; }
  .hero { padding-top: var(--s-6); }
}
@media (max-width: 620px) {
  /* A phone cannot hold a 148px card seven times over at any spread, and
     shrinking the cards to postage stamps to make the geometry work would throw
     away the thing being shown. So the cards stay legible and the *fan closes
     up* instead -- which is also how a hand of cards actually looks when it is
     held rather than laid out. */
  /* Bigger cards *and* a tighter fan, which is not a contradiction: closing the
     spread from 0.30 to 0.26 buys back more width than growing the card spends,
     so the cards go from 84px to ~98px and the whole thing still fits inside a
     393px phone with room either side. It is the centrepiece here, so it should
     be as large as the geometry allows. */
  .hero-art { --cw: clamp(92px, 25vw, 116px); }
  .fan-card { --spread: calc(var(--cw) * 0.26); --tilt: 5.5deg; }
  .fan:hover .fan-card,
  .fan:active .fan-card { --spread: calc(var(--cw) * 0.32); --tilt: 7.5deg; }
}


/* On the coming-soon page the fan sits between the headline and the sub, so it
   needs its own rhythm rather than the hero grid's. And the big mark above it
   comes down, because a logo, a headline and a seven-card fan stacked on a phone
   is more than one screen holds. */
/* .soon-wrap is a flex column with align-items:center, so .hero-art was
   shrink-to-fit -- and its only in-flow content is the category caption, because
   every card is absolutely positioned. So the fan centred itself on the
   caption's widest line instead of on the page, which is the misalignment that
   got reported. Full width, and the centring comes from .fan's own left:50%. */
body.soon .hero-art { width: 100%; margin: var(--s-5) 0 var(--s-3); }
@media (max-width: 620px) {
  body.soon .hero-art { margin: var(--s-4) 0 var(--s-2); }
}

.fan-cats {
  list-style: none;
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 6px 0; margin: 26px 0 0; padding: 0;
  font-family: var(--mono); font-size: 11px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-2);
  animation: cats-in .6s ease-out .95s backwards;
}
.fan-cats li { padding: 0 10px; position: relative; white-space: nowrap; }
/* Separators drawn rather than typed, so one can never wrap onto a line alone. */
.fan-cats li + li::before {
  content: ""; position: absolute; left: 0; top: 50%; translate: 0 -50%;
  width: 1px; height: 9px; background: currentColor; opacity: .4;
}
@keyframes cats-in { from { opacity: 0; translate: 0 6px; } }

/* The aperture brackets from the logo, drawn around the fan — so the mark, the
   live overlay and this page are one idea seen three times. */
/* Insets are against .fan now, not .hero-art. Tighter, because the fan is
   the thing being framed and the outer box also holds the caption. */
.brackets { position: absolute; inset: 4% 6%; pointer-events: none; }
.brackets span {
  position: absolute; width: 42px; height: 42px;
  border: 2.5px solid var(--violet-lo); opacity: .45;
  animation: breathe 3.4s ease-in-out infinite;
}
.brackets span:nth-child(1) { top: 0; left: 0; border-right: 0; border-bottom: 0; border-radius: 8px 0 0 0; }
.brackets span:nth-child(2) { top: 0; right: 0; border-left: 0; border-bottom: 0; border-radius: 0 8px 0 0; animation-delay: .2s; }
.brackets span:nth-child(3) { bottom: 0; right: 0; border-left: 0; border-top: 0; border-radius: 0 0 8px 0; animation-delay: .4s; }
.brackets span:nth-child(4) { bottom: 0; left: 0; border-right: 0; border-top: 0; border-radius: 0 0 0 8px; animation-delay: .6s; }
@keyframes breathe { 50% { opacity: .95; } }

/* ------------------------------------------------------------------ bands */

.band { margin-top: var(--s-9); }
.band-h {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(24px, 3.2vw, 34px); letter-spacing: -.028em;
  margin: 0 0 var(--s-6); text-wrap: balance;
}

.features { display: grid; grid-template-columns: repeat(auto-fit, minmax(268px, 1fr)); gap: 18px; }
.feature {
  background: var(--glass-bg); border: 1px solid var(--glass-edge);
  border-radius: var(--r-md); padding: 26px 24px;
  backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur);
  transition: border-color .2s ease, transform .2s ease;
}
.feature:hover { border-color: rgba(155,93,229,.32); transform: translateY(-3px); }
.feature h3 { font-size: 16px; margin: 16px 0 10px; letter-spacing: -.01em; }
.feature p { margin: 0; font-size: 14px; line-height: 1.68; color: var(--ink-2); }
.feature-mark {
  display: inline-flex; align-items: center; padding: 4px 11px; border-radius: 999px;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase;
  background: var(--violet-dim); color: var(--violet-lo);
  border: 1px solid rgba(155,93,229,.26);
}
/* Cyan is reserved for live/data states, never for price movement. */
.feature-mark.live {
  background: rgba(94,231,247,.1); color: var(--live);
  border-color: rgba(94,231,247,.3);
}

.steps {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 28px;
}
.steps li { border-top: 1px solid var(--glass-edge); padding-top: 20px; }
.step-n { font-family: var(--mono); font-size: 11px; letter-spacing: .16em; color: var(--violet-lo); }
.steps h3 { font-size: 16px; margin: 12px 0 8px; }
.steps p { margin: 0; font-size: 14px; line-height: 1.65; color: var(--ink-2); }

.closing { margin-top: 120px; text-align: center; }
.closing h2 { font-family: var(--font-display); font-weight: 700;
              font-size: clamp(26px, 3.6vw, 38px); letter-spacing: -.03em;
              margin: 0 0 var(--s-4); }
.closing p { max-width: 52ch; margin: 0 auto 30px; color: var(--ink-2); font-size: 15px; line-height: 1.7; }

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

.reveal {
  opacity: 0; transform: translateY(26px);
  transition: opacity .7s ease, transform .7s cubic-bezier(.2,.7,.3,1);
}
.reveal.seen { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  /* The cards still have to *arrive*. Cancelling an animation whose 0% keyframe
     is opacity:0 with a backwards fill would leave an empty hero, which is a
     worse outcome than the motion was. */
  .fan-card, .fan-cats { animation: none; opacity: 1; }
  .fan-card { transform: translateX(var(--fx)) translateY(var(--fy)) rotate(var(--fr)); }
}

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .fan-card { animation: none; }
  .fan:hover .fan-card { transform:
    translateX(calc((var(--i) - 3) * 58px))
    translateY(calc((var(--i) - 3) * (var(--i) - 3) * 5px))
    rotate(calc((var(--i) - 3) * 7deg)); }
  .brackets span { animation: none; opacity: .7; }
}

/* --------------------------------------------------------------- pre-launch

   One centred column. No nav to explore with, no fan to hover — the page has
   exactly one thing to offer and pretending otherwise wastes the visit. */

body.soon .nav .navlinks,
body.soon .nav .searchbar { display: none; }
body.soon .nav { justify-content: center; }
body.soon .footer { justify-content: center; }

.soon-wrap {
  max-width: 620px; margin: 0 auto; text-align: center;
  padding: 6vh 0 40px;
  display: flex; flex-direction: column; align-items: center; gap: 0;
}
.soon-mark { display: block; margin-bottom: 26px; }
.soon-figures { justify-content: center; margin: 34px 0 40px; gap: 34px; }

.soon-form {
  display: flex; flex-direction: column; gap: 10px;
  width: 100%; max-width: 380px;
}
.soon-form input {
  width: 100%; padding: 13px 16px; border-radius: 10px;
  border: 1px solid var(--glass-edge); background: rgba(255,255,255,.05);
  color: var(--ink); font-family: var(--font); font-size: 14px;
}
.soon-form input::placeholder { color: var(--ink-3); }
.soon-form input:focus-visible { outline: none; border-color: rgba(155,93,229,.6); }
.soon-form .btn-primary { margin-top: 4px; border: 0; cursor: pointer; font-family: var(--font); }

.soon-note { font-size: 11.5px; color: var(--ink-3); margin: 16px 0 0; max-width: 42ch; line-height: 1.6; }

.soon-done {
  display: flex; flex-direction: column; gap: 5px;
  padding: 20px 26px; border-radius: var(--r-md);
  background: rgba(74,222,128,.08); border: 1px solid rgba(74,222,128,.28);
}
.soon-done strong { color: var(--up); font-size: 15px; }
.soon-done span { color: var(--ink-2); font-size: 13px; }

.soon-error {
  padding: 12px 18px; margin-bottom: 14px; border-radius: var(--r-sm);
  background: rgba(248,113,113,.09); border: 1px solid rgba(248,113,113,.3);
  color: #FCA5A5; font-size: 13px;
}
