@import url("/static/logos.css");    /* third-party marks, where a provider is named */
@import url("/static/auth-sso.css"); /* the sign-on buttons, where one is configured */
@import url("/static/site.css");     /* the early-access sections (templates/landing.html) */
/* OVision — the public landing page's own additions.

   Kept apart from ovision.css so the page can move without touching the
   signed-in shell: every rule here is scoped to body.land, and nothing else
   loads this file. It is linked after ovision.css, so the two rules that
   override it (the headline width, the tile layout) win by order. */

/* ---------------------------------------------------------- the headline

   One line at desktop width. ovision.css caps the h1 at 20ch, which is
   narrower than "Discover something with ⊙vision" set at 3.45rem, so the
   line broke after "something". The cap goes; text-wrap: balance, still
   in force, tidies the two lines it becomes on a phone. */
body.land .hero-title { max-width: none; }

/* ------------------------------------------------------------- the tiles

   An icon over each name, chosen by what the provider is: a hosted model, an
   endpoint you run, a store, a folder, an image format, an imaging tool, a
   repository, literature, compute — or, where the vendored sets carry it, the
   provider's own mark (static/logos, CC0 1.0; painted by logos.css). Bright
   is built, dim is declared, and the icon follows the same rule as the name
   so the two states stay one state. */
body.land .tile {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: .42rem;
}
body.land .tile-ico { display: block; line-height: 0; color: #f1e7ec; }
body.land .tile-ico .ico { display: block; }
body.land .tile-name { display: block; }

@media (max-width: 560px) {
  body.land .tile { gap: .3rem; padding: .3rem; }
  body.land .tile-ico .ico { width: 18px; height: 18px; }
}

/* Narrow phones. A 1fr track will not shrink below its content's min width,
   so on anything under about 380px four tiles of "Azure OpenAI" and
   "OpenRouter" pushed the dark band past the viewport and the page scrolled
   sideways. minmax(0, 1fr) lets the tracks shrink; the names wrap at a word
   first and inside one only as a last resort; the type steps down a notch
   under 380px and the grid goes to three columns under 340px. */
body.land .tiles { grid-template-columns: repeat(5, minmax(0, 1fr)); }
body.land .tile-name { overflow-wrap: anywhere; }
@media (max-width: 560px) { body.land .tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 380px) { body.land .tile { font-size: .75rem; } }
@media (max-width: 340px) { body.land .tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* ------------------------------------------------- the marks, at full size

   The grid is a wall of logos, so the logo is the thing to look at and the
   name is its caption. At 20px in a 127px tile the mark was a detail inside a
   word; at 40px the wall reads as what it is from across a room, which is the
   whole job of this section. The tile grows with it and the caption steps
   down and lightens, so the tile's weight moves from the text to the mark
   without the name becoming unreadable — it is still the thing that says
   which service is meant, and the mark is still decorative beside it. */
body.land .tiles { gap: .6rem; }
body.land .tile { gap: .7rem; padding: .9rem .5rem; }
body.land .tile-ico .tile-logo,
body.land .tile-ico .ico { width: 40px; height: 40px; }
body.land .tile-ico .ico { stroke-width: 1.35; }
body.land .tile-ov .logo { width: 44px; height: 44px; }

@media (max-width: 900px) {
  body.land .tile-ico .tile-logo, body.land .tile-ico .ico { width: 32px; height: 32px; }
  body.land .tile-ov .logo { width: 34px; height: 34px; }
}
@media (max-width: 560px) {
  body.land .tile { gap: .45rem; padding: .5rem .3rem; }
  body.land .tile-ico .tile-logo, body.land .tile-ico .ico { width: 26px; height: 26px; }
  body.land .tile-ov .logo { width: 28px; height: 28px; }
}

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

   Sections lift into place as they reach the viewport. The hidden state is
   applied by script (`body.reveal-on`) and never by this sheet alone, so a
   browser with no script, a crawler, or a failed request shows the finished
   page rather than a blank one — the same rule the how-it-works list already
   follows with `how-enhanced`. Reduced motion gets the opacity and none of
   the travel, and every item is revealed on its own rather than as a block,
   so a slow connection cannot leave half a grid invisible. */
/* Travel only — no scale. A section arriving at .985 is a section whose text
   is rendered at a fraction of its real size for the length of the animation,
   and the browser resamples every glyph on every frame to do it. That shimmer
   is what reads as roughness; the lift alone reads as smooth, and it is the
   same gesture. Sixteen pixels rather than twenty-two for the same reason:
   less distance to cover in the same time is less speed to have to smooth. */
body.reveal-on .reveal {
  opacity: 0; transform: translateY(16px);
  transition: opacity .5s cubic-bezier(.16, 1, .3, 1), transform .72s cubic-bezier(.16, 1, .3, 1);
  /* Staggered, but capped. The provider grid is twenty-five tiles, and an
     uncapped 45ms step put the last one 1.08s behind the first — long enough
     to read as a page still loading rather than as a page arriving. Eight
     steps is a sweep across the grid; everything after rides the last one. */
  transition-delay: calc(min(var(--reveal-i, 0), 8) * 45ms);
  will-change: opacity, transform;
}
body.reveal-on .reveal.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  body.reveal-on .reveal { transform: none; transition: opacity .01s linear; transition-delay: 0s; }
}
@media print {
  body.reveal-on .reveal { opacity: 1 !important; transform: none !important; }
}

/* ------------------------------------------- the same reveal, driven by scroll

   The reference does this natively: its background carries
   `animation: background-parallax auto linear`, and an `auto` duration is the
   signature of a scroll-driven animation — the scroller advances it, not a
   clock. Where the browser supports it, so do we, and the script above stands
   down (it checks the same `CSS.supports` and returns).

   This is not only closer to the reference, it removes a whole class of bug.
   The script path depends on IntersectionObserver and requestAnimationFrame,
   and neither runs in a tab that is not being painted, which is how the
   Numbers band came to render blank. A scroll timeline is evaluated by the
   scroller itself: no observer, no frame callback, nothing to miss, and it
   works with script disabled entirely.

   The range is `cover`, deliberately, and not `entry`. `entry` ends when the
   element has *fully* entered the scrollport, which for anything taller than
   the viewport — the provider wall, the numbers band — never happens, so the
   range collapses and the element can sit at 0% forever. That is the same
   stranding failure in a new mechanism. `cover` runs from the element's first
   pixel to its last and always progresses. The first quarter is the entrance;
   after that it holds. */
@keyframes reveal-rise {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

@supports (animation-timeline: view()) {
  /* Eased, not linear. On a scroll timeline the easing maps scroll distance
     to animation progress, so `linear` ties the section to the wheel one to
     one: it is still visibly moving when it is most of the way up the screen,
     and it moves at exactly the speed the scroll does, however jerky that is.
     An ease-out spends the entrance early and settles, so the section is
     finished and still by the time it is somewhere a reader is looking, and
     a coarse scroll step no longer shows as a jump in the content.

     Starting at 4% rather than 0% for the same reason: the first pixel of an
     element crossing the bottom edge is not a reason to start moving it. */
  body.land .reveal {
    animation: reveal-rise both cubic-bezier(.16, 1, .3, 1);
    animation-timeline: view();
    animation-range: cover 4% cover 30%;
  }
  /* A grid row enters together and reads as one sweep, because its tiles
     share a scroll position — the stagger the script had to compute is what
     the layout already says. */
  @media (prefers-reduced-motion: reduce) {
    body.land .reveal { animation: none; opacity: 1; transform: none; }
  }
  @media print {
    body.land .reveal { animation: none; opacity: 1; transform: none; }
  }
}

/* ---------------------------------------------------------- the close and foot

   The last band and the footer were two slabs of different creams meeting on
   a hard line, with the footer's rounded top corners floating over the band's
   square ones. One ground, one rule: the footer sits on the page rather than
   on top of it, its columns line up on a single baseline grid, and the
   research-use line — which is a launch-envelope condition, not small print —
   is set in the body colour rather than the faintest grey on the page. */
/* The wash used to fade back to the page's ground over its last 16%, so that
   the band ended on a fade rather than on a ruled line. That solved the ruled
   line by never showing the end of the gradient: the ramp reached its most
   saturated stop behind an opaque layer of canvas, and the page finished on
   the same cream it started on.

   The ruled line is gone for a better reason now — the wash spans the close
   and the footer together (`.page-end`), so there is no edge between them to
   rule — and the ramp is allowed to finish. Nothing is laid over it here;
   `--wash-closing` in theme-dark.css carries the full ramp for both schemes,
   which is also why this rule is not restated in literal hexes: written that
   way it outranked the token and painted a light wash on a dark page. */

body.land .foot {
  padding: 2.6rem 2rem 2rem;
}
body.land .foot-inner { gap: 2.4rem; }
@media (min-width: 760px) { body.land .foot-inner { grid-template-columns: 180px 1fr; } }
body.land .foot-brand { display: flex; align-items: flex-start; }
body.land .foot-cols { gap: 1.2rem 2rem; }
body.land .foot-cols h4 {
  margin: 0 0 .75rem; font-size: .75rem; font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase; color: var(--text-3);
}
body.land .foot-cols a, body.land .foot-cols span { margin: 0; padding: .22rem 0; font-size: .85rem; }
body.land .foot-cols a { color: var(--text); }
body.land .foot-cols span { color: var(--text-2); }
body.land .foot-ruo {
  margin-top: 2.4rem; padding-top: 1.1rem;
  font-size: .78rem; line-height: 1.55; color: var(--text-2);
  align-items: center;
}
body.land .foot-ruo .ico { flex: none; color: var(--ink-700); }
@media (max-width: 560px) {
  body.land .foot { padding: 2rem 1.1rem 1.6rem; }
  body.land .foot-ruo { align-items: flex-start; }
}

/* The bar on the same phones: the two buttons keep their words on one line,
   and the bar gives up some of its gap and padding so both still fit beside
   the wordmark. Nothing is hidden; the same links are there at every width. */
body.land .land-login, body.land .land-cta { white-space: nowrap; }
@media (max-width: 380px) {
  body.land .land-bar { gap: .6rem; padding: .7rem .9rem; }
}
@media (max-width: 340px) {
  body.land .land-login, body.land .land-cta { padding: .45rem .6rem; font-size: .82rem; }
}

/* ======================================================= the reference's proportions

   Measured off lovable.dev at 1440 and 1864 rather than eyeballed, because
   "looks like it" is not a spec. What the reference actually does:

     page rhythm      160px top and bottom per section (104px on a phone)
     content column   1344px inside a 1536px container
     header           static, transparent, no blur — it scrolls away
     hero             a full viewport tall, everything centred in it
     headline         48px, 600, -0.04em; the sub 4px under it, 18px/24px
     composer         608px wide, 28px radius, 12px padding, a 1px ring and
                      one long soft shadow; the send key a 32px circle
     cards            white, 16px radius, no border, one soft shadow, a fixed
                      425px height with the art on top and the copy at the foot
     dark band        not full-bleed — an inset card, 16px radius, 160px/48px
     stats            the number first, its label under it at 14px

   Everything below is that, in OVision's palette and type. Nothing here
   changes what the page says, only the space it says it in. Scoped to
   body.land, so the signed-in shell is untouched. */

/* ------------------------------------------------------------------- the bar

   The reference's header is `position: static` and fully transparent: it sits
   on the wash and scrolls away with it. Ours was sticky and frosted, which
   over a hero this saturated meant a permanent bar of blurred pink. The
   specificity here (body.land .land-bar) is deliberate — theme-dark.css loads
   after this file and repaints .land-bar, and body.land beats it. */
body.land .land-bar {
  position: static;
  max-width: 1344px; margin: 0 auto;
  padding: 1.15rem 1.5rem;
  gap: 1.25rem;
  background: transparent;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
body.land .land-nav { gap: .1rem; }
body.land .land-nav a {
  padding: .25rem .4rem; font-size: .9375rem; line-height: 1.6; font-weight: 400;
}
body.land .land-login, body.land .land-cta {
  border-radius: 9px; padding: .44rem .8rem; font-size: .875rem; line-height: 1.5;
}

/* ------------------------------------------------------------------ the hero

   A full viewport, as the reference has it, so the first screen is the
   question box and nothing else. svh rather than vh: on a phone vh counts the
   space behind the browser's own chrome, which pushed the composer under the
   address bar. */
body.land .hero { min-height: 100svh; padding: 1rem 1.25rem 3rem; }
body.land .hero-inner { max-width: 1344px; }

/* 48px at the top end, matching the reference; ours ran to 55px and the
   headline stopped being one held line and became a banner. The tracking and
   the weight stay as they are — that is OVision's voice, not the reference's. */
body.land .hero-title { font-size: clamp(2.1rem, 4.6vw, 3rem); margin-bottom: .3rem; }
body.land .hero-sub { font-size: 1.125rem; line-height: 1.35; margin-bottom: 1.75rem; }
/* The line under the headline sits on the wave's lilac band, where the soft
   grey measured 2.4–2.7:1; the page's own text colour is 5.5:1 or better. */
body.land .hero-sub { color: var(--text); }

/* The composer, to the reference's measurements. The ring and the long shadow
   are what lift it off the wash; the old 720px box read as a search bar
   spanning the page rather than one object sitting on it. */
body.land .hero-composer {
  max-width: min(608px, 100%);
  border-radius: 28px;
  padding: 3px;
  /* The token, not a literal white. theme-dark.css repaints .hero-composer
     through --glass-strong, and body.land here is the more specific selector,
     so a hard #fff would win and leave a white slab on a dark page. The
     fallback is only for a browser that never got the token sheet. */
  background: var(--glass-strong, #fff);
  box-shadow: 0 0 0 1px rgba(23, 19, 26, .08), 0 25px 50px -12px rgba(98, 16, 44, .28);
}
/* `align-items: center` because the row's default `stretch` was pulling the
   send key to the full height of the bar: it kept its 32px width and grew to
   40px tall, so a `border-radius: 50%` circle rendered as an ellipse. The
   signed-in composer never showed this because home.css centres its own bar
   and this one is not that bar. */
body.land .hero-composer .composer-bar { padding: .3rem .55rem .55rem; align-items: center; }
body.land .hero-composer .composer-send { width: 32px; height: 32px; }

/* ------------------------------------------------------------ the weight

   Measured against the reference at the same viewport width rather than
   matched by eye (1864px, 2026-09-15):

     reference        hero 48px/600/-0.04em   section 36px/600/-0.04em
     ours, before     hero 48px/700/-0.04em   section 38.4px/700/-0.03em

   The size was within a hair; the weight was a whole step heavy and the
   tracking a hair loose, and that is most of what made our page read as
   denser than the one it is modelled on. Semibold at this size holds the
   same presence with less ink, and -0.04em is what closes the gaps a
   600-weight leaves at 36px and up.

   Scoped to `body.land` and to the onboarding title: the signed-in shell
   sets its headings in the serif at 400 and is a different typographic
   system on purpose. */
body.land .hero-title,
body.land .band h2,
body.land .closing h2,
.ob-title { font-weight: 600; }
body.land .band h2 {
  font-size: clamp(1.7rem, 3.4vw, 2.25rem);
  letter-spacing: -.04em;
}
/* The logo-wall cell's heading is the reference's "Trusted by top brands":
   a secondary band head at 28px, not a card title at 19.5px. */
body.land .part-copy h2 { font-size: 1.75rem; letter-spacing: -.04em; line-height: 1.12; }

/* The pull-quote in the reviews band. The reference sets its customer quote
   at 24px/480/1.33 — lighter and airier than a heading, because it is
   somebody speaking, not the page shouting. Ours was 21.7px/600/1.28. */
body.land .review-quote {
  font-size: clamp(1.15rem, 2.1vw, 1.5rem);
  font-weight: 500;
  line-height: 1.34;
}

/* ------------------------------------------------------------- the rhythm

   160px between sections at desktop, 104px on a phone. This is most of what
   makes the reference feel unhurried, and it was the single biggest
   difference: ours topped out at 96px, so every band ran into the next. */
body.land .band { padding: clamp(4.5rem, 11vw, 10rem) 1.5rem; }
body.land .band-inner { max-width: 1344px; }
body.land .band-lede { font-size: 1rem; line-height: 1.5; margin-bottom: 2.6rem; }

/* --------------------------------------------------------------- the cards

   The reference's card is a fixed 425px box: art on top taking whatever is
   left, copy pinned to the foot, so three cards of unequal prose still line
   their headings up. White, one soft shadow, no border. */
body.land .trio { gap: .75rem; }
body.land .trio-card {
  border: 0;
  padding: 0;
  min-height: 425px;
  display: grid; grid-template-rows: minmax(0, 1fr) auto;
  overflow: hidden;
  box-shadow: 0 1px 1px rgba(23, 19, 26, .04), 0 4px 24px -6px rgba(98, 16, 44, .12);
}
body.land .trio-art { height: auto; border-radius: 0; padding: 1.5rem; }
@media (min-width: 860px) {
  /* Headings line up because the art above them is one height, not because
     the prose under them happens to wrap to the same number of lines. It
     did not: card three runs to two lines where the others run to three, and
     with the copy pinned to the foot its heading sat 23px below theirs. */
  body.land .trio-card { grid-template-rows: 284px auto minmax(0, 1fr); }
  body.land .trio-art { height: 284px; }
}
body.land .trio-card h3 { margin: 1.5rem 1.5rem .4rem; }
body.land .trio-card p { margin: 0 1.5rem 1.5rem; font-size: .95rem; line-height: 1.5; }

/* ----------------------------------------------------------- the dark band

   The reference's dark section is an inset card, not a full-bleed stripe: it
   is inset from the page edge and rounded, so the ground shows down both
   sides and the band reads as an object on the page. Ours ran edge to edge
   and butted against the bands above and below with no seam. */
body.land .how {
  max-width: 1440px;
  margin: 0 auto;
  padding: clamp(4.5rem, 11vw, 10rem) clamp(1.25rem, 3vw, 3rem);
  border-radius: 16px;
  box-shadow: 0 1px 1px rgba(23, 19, 26, .2), 0 4px 34px -5px rgba(23, 19, 26, .18);
}
@media (max-width: 1472px) { body.land .how { margin: 0 1rem; } }
@media (max-width: 560px)  { body.land .how { margin: 0 .6rem; border-radius: 14px; } }
body.land .how-inner { max-width: 1248px; }

/* --------------------------------------------------------------- the stats

   The reference puts the figure first and its label under it, which is the
   right way round: the number is the thing, the label is its caption. The
   markup stays <dt> then <dd>, because that is the correct pairing for a
   description list and a screen reader should hear the label first; only the
   painted order is flipped. */
body.land .stats { gap: 1.6rem; }
body.land .stats div { display: flex; flex-direction: column-reverse; gap: .25rem; }
body.land .stats dd { font-size: clamp(1.6rem, 2.4vw, 2rem); }
body.land .stats dt { font-size: .875rem; line-height: 1.4; color: var(--text-2); }

/* ------------------------------------------------------------- the reviews

   The reference's social-proof card is a wide, quiet slab on the page ground
   with generous inner padding, and its heading block is centred above it.
   Ours keeps the band rhythm of every other section here; only the card's
   scale and its ground change, so it reads as an object rather than as one
   more bordered panel. */
body.land .reviews-head { margin-bottom: clamp(2rem, 4vw, 3.2rem); }
body.land .review-card { padding: clamp(1.5rem, 3.4vw, 2.6rem); border-radius: 20px; }
body.land .review-side { border-radius: 20px; }
body.land .reviews-rail { max-width: 1040px; }
/* With flanking cards the rail takes the full band width, as the reference's
   does. Written here as well as in ovision.css because `body.land` outranks
   the `:has()` selector there and would otherwise pin it back to 1040. */
@media (min-width: 1040px) { body.land .reviews-rail:has(.review-side) { max-width: none; } }

/* ------------------------------------------------------------- the closing */
body.land .closing { padding: clamp(4.5rem, 11vw, 10rem) 1.25rem clamp(3rem, 6vw, 5rem); }
body.land .closing-inner { max-width: 608px; }

/* --------------------------------------------------- single sign-on, if any

   Imported rather than linked: tests/discovery/test_theme.py:263 pins the
   landing page to exactly three <link> elements, in order, so a fourth
   stylesheet tag would fail the build. logos.css arrives the same way at the
   top of this file. The row itself renders only where the route supplies
   configured providers — see the template. */
/* --------------------------------------------------- the wash, where it lies

   The hero paints the wave at its own size, exactly as sign-up, sign-in and
   the first run do, so the landing and the modal over it are one picture.
   It used to be drawn half again as tall and hung above the hero's top edge,
   which lifted every band by a third of a screen; the user asked for the
   sign-up page's placement instead ("move it a bit down"). */
body.land .hero .hero-wash { inset: 0; }

/* --------------------------------------- the composer's bar, on one row

   ovision.css gives .composer-actions `width: 100%` under 620px, which is
   right for the signed-in composer — it carries several selects and they need
   the width. The landing's copy carries two controls, so that rule only
   dropped the send key onto a second row and left the plus sitting alone
   above it. Scoped to the hero composer, so the signed-in behaviour on a
   phone is unchanged. */
@media (max-width: 620px) {
  body.land .hero-composer .composer-bar { flex-wrap: nowrap; }
  body.land .hero-composer .composer-actions { margin-left: auto; width: auto; }
}

/* ============================================ the hero, on an actual phone

   THE BUG THIS FIXES. `.hero` is a grid with `place-items: center`, and a grid
   item's automatic minimum size is its min-content width — which beats
   `width: 100%`. So the widest thing inside the hero sized the track, and at
   390px that was a sign-on button: auth-sso.css pads it 2.9rem either side of
   a label that can be as long as "Continue with Demo identity provider
   (local)". The track went to 408px inside a 336px column and took the
   headline, the sub and the composer out with it. None of it showed as a
   sideways scroll, because `.hero` also sets `overflow: hidden` — so the right
   edge of the first screen was simply cut off, silently, on every phone.

   `min-width: 0` restores `width: 100%` as the binding constraint. The rest is
   making sure nothing inside can demand more than the column it now has. */
body.land .hero-inner { min-width: 0; max-width: min(1344px, 100%); }
/* Every hero child except the composer, which sets its own narrower cap above
   and is excluded here on purpose. This rule and that one have identical
   specificity, so as a blanket `> *` it simply won — later in the file — and
   the composer went back to spanning the full 1270px hero: the exact "search
   bar across the page" the 608px cap exists to prevent. The composer's own
   cap is written as a min() so it still cannot overflow a narrow screen. */
body.land .hero-inner > *:not(.hero-composer) { max-width: 100%; }

/* The narrow-screen rules for the hero's provider buttons went with the row
   itself — `.land-sso` is no longer in the markup, so they matched nothing.
   The sign-in sheet's buttons have their own rules further down this file,
   under .ask-gate, and those are still live. */

/* ===================================================== the reference's shapes

   The two things the reference does that we did not, both shape rather than
   colour: its controls are pills, and its buttons carry a hairline border and
   a single soft shadow instead of a hard 1px box. Applied here to the bar and
   to the sign-on row so the first screen reads as one set of controls; the
   composer already had the radius. Palette untouched — these are OVision's
   own tokens, and the four-colour Google mark and the H&E wash are what make
   the page ours rather than a copy of anyone's. */
/* The header pair's shape is declared once, further down, from a measurement
   off the reference (8px, not a pill). The sign-on buttons below the composer
   ARE pills — that is the reference's own distinction, not an inconsistency. */

/* The row needs air between it and the composer, and a line saying what it
   is — otherwise four unexplained buttons sit under a question box. */
/* ------------------------------------------------------------ the wordmark

   .wordmark-name is set in var(--serif) — Iowan Old Style, falling back to
   Palatino and Georgia — because the signed-in rail wants a bookish mark
   beside a dense working screen. On the landing it sat inches from four sans
   nav links and read as two different products sharing a bar: a serif, plum,
   slightly-too-large name against grey sans links.

   The name goes sans and takes the page's ink; the ring beside it keeps the
   brand colour, which is where the colour belongs. Scoped to body.land, so
   the rail's serif lockup is exactly as it was. */
body.land .wordmark-name {
  font-family: var(--sans);
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: -.022em;
  color: var(--heading, #17131a);
}
body.land .wordmark { gap: .55rem; }

/* The bar's own spacing, to the reference's: the mark is a held object with
   air after it, not the first item in a list of links. */
body.land .land-bar { gap: 2.25rem; }
@media (max-width: 380px) { body.land .land-bar { gap: .6rem; } }

/* ===================================================== the landing composer

   The hero composer is a real <form> with a <textarea>, not an anchor
   dressed as one. It starts at one line and grows with the question up to
   about eight, which is the reference's behaviour and keeps the hero from
   jumping as somebody types. The old `.hero-placeholder` span is gone, so
   the compact metrics move onto the textarea itself. */
body.land .hero-composer .composer-input {
  min-height: 2.6rem;
  max-height: 220px;
  padding: .95rem 1.05rem .15rem;
  overflow-y: auto;
}

/* The send button is quiet until there is something to send. Not disabled:
   an empty submit still focuses the box and says so, which is kinder than a
   control that silently does nothing. */
body.land .hero-composer .composer-send {
  width: 32px;
  height: 32px;
  /* `button, .btn` in ovision.css carries `min-height: 40px`, and a min-height
     beats a height — so the 32px circle was being stretched to 32×40 and drawn
     as an ellipse by `border-radius: 50%`. The height is the whole control
     here, so the floor has to come off. */
  min-height: 0;
  flex: none;
  background: var(--line-2);
  /* The arrow is drawn in currentColor, and the button inherits white from
     `.composer-send` in ovision.css — so the quiet state was a white glyph on
     a pale tint, about 1.9:1, which read as broken rather than as quiet.
     Setting the colour on the button itself makes the glyph follow the state
     whether or not it is wrapped in `.ico`. */
  color: var(--text-2);
  transition: background .16s ease, transform .16s ease;
}
body.land .hero-composer .composer-send .ico { color: var(--text-2); }
/* --send-bg/--send-fg, not --ink-900 + #fff. --ink-900 is the brand ACCENT
   token: it inverts to a pale pink (#f7e1ea) in dark, so a literal white on
   it measures 1.24:1. The send pair is defined for both schemes
   (theme-dark.css :33/:142/:255) and is what .composer-send itself uses. */
body.land .hero-composer.has-text .composer-send { background: var(--send-bg); color: var(--send-fg); }
body.land .hero-composer.has-text .composer-send .ico { color: var(--send-fg); }

/* ======================================================== the sign-in sheet

   Opens when a question is submitted. Laid out like the reference's dialog:
   the mark and the close on one line, a quiet line over a loud one, then the
   ways in — providers as full-width rows, a rule reading OR, then the
   credential form. Nothing is invented for the shape: every control is the
   real one the sign-in page renders, from the same `sso.links()` and
   `sso.password_login()` the route passes, so a provider the project has
   switched off is absent here exactly as it is there. On a deployment with
   every provider off, the rows and the rule are simply not drawn and the
   form stands alone — the honest version of this layout, rather than a row
   of buttons that answer "Unsupported provider".

   Scoped here rather than pulled from auth-sso.css: the landing links
   exactly three stylesheets and the shell pins that order. */
.ask-gate {
  width: min(25.5rem, calc(100vw - 2rem));
  padding: 1.35rem 1.5rem 1.4rem;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--paper);
  color: var(--text);
  box-shadow: 0 28px 80px -30px rgba(38, 10, 22, .5);
}
.ask-gate::backdrop { background: rgba(28, 12, 20, .5); backdrop-filter: blur(3px); }
.ask-gate[open] { animation: ask-gate-in .18s ease-out; }
@keyframes ask-gate-in { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) { .ask-gate[open] { animation: none; } }

.ask-gate-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.ask-gate-mark { display: inline-flex; }
.ask-gate-dismiss { margin: 0; }
.ask-gate-x {
  width: 28px; height: 28px; padding: 0; display: grid; place-items: center;
  border: 0; border-radius: 50%; background: none; color: var(--text-3); cursor: pointer;
}
.ask-gate-x:hover { background: var(--line); color: var(--text); }

.ask-gate-over {
  margin: 1rem 0 0; font-size: 1.05rem; font-weight: 600;
  letter-spacing: -.01em; color: var(--text-3);
}
.ask-gate-title {
  margin: 0 0 1rem; font-size: 1.05rem; font-weight: 640; letter-spacing: -.01em;
}

.ask-gate-q {
  margin: 0 0 1rem; padding: .65rem .8rem;
  border: 0; border-radius: 10px; background: var(--canvas-2);
  font-size: .875rem; line-height: 1.5; color: var(--text-2);
  max-height: 6.5rem; overflow-y: auto;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.ask-gate-q:empty { display: none; }

.ask-gate .auth-sso { display: grid; gap: .5rem; margin: 0; }
.ask-gate .auth-sso-btn {
  position: relative;
  display: flex; align-items: center; justify-content: center; gap: .5rem;
  padding: .66rem .9rem;
  border: 1px solid var(--line-2); border-radius: 10px;
  background: var(--paper); color: var(--text);
  font-size: .9rem; font-weight: 450; text-decoration: none;
}
.ask-gate .auth-sso-btn:hover { background: var(--canvas-2); }

/* The mark travels with the label instead of being pinned to the left edge.

   auth-sso.css pins it there, and had a reason: with provider names of very
   different lengths, an absolutely-placed mark keeps one optical centre line
   down the stack however ragged the labels are. The label that made the stack
   ragged was "Continue with Demo identity provider (local)", and that provider
   is gone. What the rule leaves behind is a mark sitting an inch from the words
   it belongs to — two things in a button rather than one control — which is
   not what the reference does and not what the eye groups.

   Scoped to the sheet on purpose. /login and /register keep the pinned mark:
   they can offer Supabase alongside the three, so the measurement that made
   this safe here has not been made there. */
.ask-gate .auth-sso-btn .brand-logo { position: static; }
/* And the height comes down with it. `min-height: 2.75rem` in auth-sso.css was
   sized for a button with an absolute mark inside it; with the mark in the flow
   the padding decides, and 44px of chrome around an 18px mark reads as a row of
   panels rather than a row of buttons. */
.ask-gate .auth-sso-btn { min-height: 0; padding: .62rem 1rem; }
.ask-gate .auth-sso-btn.is-last-used { border-color: var(--ink-600); }
/* This browser's own record of the provider it used last, floated onto the
   border as the reference does. Never an authorisation — see the note in
   _sso_buttons.html; with storage unavailable it simply never appears. */
.ask-gate .auth-sso-last {
  position: absolute; top: -.55rem; right: .6rem;
  padding: .06rem .35rem; border-radius: 5px;
  background: var(--canvas-2); color: var(--text-2);
  font-size: .75rem; font-weight: 550; line-height: 1.5;
}

/* The rule that reads OR, drawn only when something follows it: the macro
   emits a divider whenever there are providers, and with no credential form
   after it that rule would separate the buttons from nothing. */
.ask-gate .auth-sso-divider {
  display: flex; align-items: center; gap: .7rem;
  margin: .9rem 0; color: var(--text-3);
  font-size: .75rem; letter-spacing: .06em; text-transform: uppercase;
}
.ask-gate .auth-sso-divider::before,
.ask-gate .auth-sso-divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.ask-gate-sso:not(:has(+ .ask-gate-form)) .auth-sso-divider { display: none; }

.ask-gate .auth-form { display: grid; gap: .5rem; margin: 0; }
/* The macro titles its form "Or sign in with your … email", which is right
   under a row of buttons, redundant once the rule above says OR, and wrong
   when the form is the only way in. The inputs keep their own screen-reader
   labels, so nothing is lost with the visible line hidden. */
.ask-gate .auth-supabase-title {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.ask-gate .auth-form input {
  width: 100%; padding: .66rem .8rem; border: 1px solid var(--line-2);
  border-radius: 10px; font: inherit; font-size: .9rem;
  background: var(--paper); color: var(--text);
}
.ask-gate .auth-form input:focus {
  outline: none; border-color: rgba(196, 50, 99, .5); box-shadow: 0 0 0 3px rgba(222, 74, 123, .14);
}
.ask-gate .auth-go {
  padding: .68rem .9rem; border: 0; border-radius: 10px;
  background: var(--btn-bg); color: var(--btn-fg);
  font: inherit; font-size: .9rem; font-weight: 600;
  cursor: pointer; text-align: center; text-decoration: none;
}
.ask-gate .auth-go:hover { background: var(--btn-bg-hover); color: var(--btn-fg); }
.ask-gate .auth-supabase-fine {
  margin: .1rem 0 0; font-size: .75rem; line-height: 1.45; color: var(--text-3); text-align: center;
}

/* The fallback, drawn only where the deployment offers no control above. */
.ask-gate-alt { display: grid; }
.ask-gate-go {
  padding: .68rem .9rem; border-radius: 10px;
  background: var(--btn-bg); color: var(--btn-fg);
  font-size: .9rem; font-weight: 600; text-align: center; text-decoration: none;
}
.ask-gate-go:hover { background: var(--btn-bg-hover); color: var(--btn-fg); }

.ask-gate-foot { margin: .9rem 0 0; font-size: .82rem; color: var(--text-3); text-align: center; }
.ask-gate-foot a { color: var(--text-2); }

.ask-gate-fine {
  display: flex; align-items: center; justify-content: center; gap: .35rem;
  margin: .75rem 0 0; padding-top: .75rem; border-top: 1px solid var(--line);
  font-size: .75rem; color: var(--text-3);
}

/* ------------------------------------------------- the hero's bottom edge

   The hero wash ended on a ruled line: the section clips it, and the mesh's
   last stop is its most saturated, so hot rose met flat paper on a hard
   horizontal. The closing band already solved this — one transparent layer
   laid over whichever wash the theme chose, ending on the page's own ground.
   The same idea, applied to the top of the page.

   The wash fills the hero, so its own 100% is the hero's bottom edge and
   the fade lands there in both themes. Written against the token, never a
   literal, so this cannot overwrite dark the way the closing wash's first
   attempt did.

   It eases in from 70% rather than switching on at 86%. The wave's warm foot
   (the pink side blobs centred at 84%, the rose below them) is its most
   saturated band, and a short linear fade that began just under it cut it
   into a hot stripe sitting on a milky smear: the user's "fix that bottom
   back part". Starting above the band and easing (slow, then faster, then
   slow into the page) dissolves the foot instead, and the middle of the
   screen, where the question box is, keeps every colour.

   And it no longer lands at the hero's edge. Reaching the ground exactly
   there still drew a line — the fade was at its steepest when it stopped —
   and the "brought to you by" line under the hero sat on a blank strip after
   it (the user, 2026-09-27: "make it flow, make it not abrupt"). So the fade
   is one curve, half a cosine from the full wave to the ground, 24rem long.
   Its first 16rem are here and end on a quarter of the wave's foot over the
   ground (--fade-foot); `.affil`, the next section, draws the last 8rem
   (the end of this file) and lands on the ground itself. Both sides of the
   seam are that one colour, so it cannot show, and the curve is flat at
   both ends, so neither where it starts nor where it lands reads as an edge.
   The stops are in rem from the bottom, not percentages, so the two halves
   stay one curve whatever the height of the screen. */
body.land .hero .hero-wash::after {
  --fade-to: var(--land-canvas, var(--canvas, #fbf8f9));
  --fade-foot: color-mix(in srgb, var(--ov-wave-foot) 25%, var(--fade-to));
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(0deg, var(--fade-foot) 0,
                              color-mix(in srgb, var(--fade-foot) 92.2%, transparent) 1rem,
                              color-mix(in srgb, var(--fade-foot) 83.9%, transparent) 2rem,
                              color-mix(in srgb, var(--fade-foot) 75.4%, transparent) 3rem,
                              color-mix(in srgb, var(--fade-foot) 66.7%, transparent) 4rem,
                              color-mix(in srgb, var(--fade-foot) 49.4%, transparent) 6rem,
                              color-mix(in srgb, var(--fade-foot) 33.3%, transparent) 8rem,
                              color-mix(in srgb, var(--fade-foot) 19.5%, transparent) 10rem,
                              color-mix(in srgb, var(--fade-foot) 8.9%, transparent) 12rem,
                              color-mix(in srgb, var(--fade-foot) 2.3%, transparent) 14rem,
                              transparent 16rem);
  pointer-events: none;
}

/* ------------------------------------------------ a declared tile is legible

   Bright is built and dim is declared, and that distinction stays — it is the
   honest difference between a connector you can use today and one the build
   only names. But dim had become unreadable rather than secondary: #5c4f57 on
   the panel's near-black is under 2:1, so a declared tile read as a rendering
   fault instead of a state. Lifted to clear 4.5:1 on the same ground, which
   keeps it plainly quieter than a built tile while leaving it a tile a person
   can actually read. The name follows the icon, so the two stay one state. */

/* The shared macro titles its form "Or sign in with your … email", which is
   right under a row of SSO buttons and wrong when it is the only way in —
   and on a project with every OAuth provider switched off it always is. The
   selector matches a form NOT preceded by the button row; the inputs keep
   their own screen-reader labels from the macro, so nothing is lost with the
   visible line hidden. */
.ask-gate-form:not(.ask-gate-sso + .ask-gate-form) .auth-supabase-title {
  position: absolute;
  width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* ============================================ the provider wall, as a wall

   The reference's version of this block is a field of marks, not a table of
   them: tiles with no border and a barely-there fill, most marks greyed back,
   a handful left in their own colour, its own mark bright in the middle, and
   the whole field dissolving at its edges so it reads as a wall that
   continues past the frame. Ours was a 5x5 of bordered boxes with a bright
   caption under each, which read as a specification table.

   What does NOT change is what the wall means. Bright is a provider this
   build has code for and tests against; dim is one that is declared and fails
   closed. The reference has no such distinction to keep, we do, and dropping
   it to match a marketing page would be the exact dishonesty the integration
   grid exists to avoid. So the names stay — quieter, because the mark should
   carry the tile at a glance — and the built/declared split is now carried by
   opacity, which is what the reference happens to use for its own field. */

body.land .tile {
  background: rgba(255, 255, 255, .035);
  border: 0;
  border-radius: 12px;
}
body.land .tile-dim {
  background: rgba(255, 255, 255, .018);
}

/* The names: present, and clearly secondary to the mark. At .76rem and 82%
   they were the first thing the eye landed on; a wall of logos whose captions
   shout is a list, not a wall. */

/* Ours, bright, in the middle — the one tile that keeps its full weight,
   exactly as the reference does with its own mark. */

/* The field dissolves at its edges instead of stopping on a rule. Masked
   rather than faded with a gradient overlay, so it works over the dark card
   whatever the theme paints behind it. Two-axis: strong at the outside
   columns, gentler top and bottom, because the grid is wider than it is tall. */
body.land .tiles {
  -webkit-mask-image:
    linear-gradient(to right, transparent 0, #000 13%, #000 87%, transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 9%, #000 91%, transparent 100%);
  mask-image:
    linear-gradient(to right, transparent 0, #000 13%, #000 87%, transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 9%, #000 91%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
/* The mask hides the outer tiles' names as well as their edges, so the ones
   that survive need the room back. */
body.land .tiles { gap: .5rem; }

/* ------------------------------------------------------------- the buttons

   The reference's pair: an 8px radius on both, the quiet one white with a
   hairline and a single-pixel lift, the loud one near-black and semibold.
   Ours were a touch large and a touch round beside them. */
body.land .land-login {
  border-radius: 8px;
  border-color: rgba(23, 19, 26, .13);
  box-shadow: 0 1px 2px rgba(23, 19, 26, .05);
  font-weight: 500;
}
body.land .land-cta {
  border-radius: 8px;
  font-weight: 600;
  box-shadow: 0 1px 2px rgba(23, 19, 26, .12);
}
body.land .land-login, body.land .land-cta { padding: .42rem .78rem; font-size: .875rem; }

/* The centre tile keeps its gradient. `body.land .tile` above is more
   specific than `.tile-ov` in ovision.css, so the flat wall fill won and our
   own mark went as quiet as a declared provider — the one tile in the field
   that is supposed to be unmistakable. Restated at the wall's specificity. */
body.land .tile-ov {
  background: linear-gradient(160deg, #6b46a8, #d63c74 55%, #f4905e);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .14), 0 18px 44px -12px rgba(214, 60, 116, .7);
}
body.land .tile-ov .logo path { fill: #fff; }

/* ------------------------------------------------- the hero column's floor

   `.hero` is a grid and `.hero-inner` has `width: 100%` but no `min-width`,
   so its automatic minimum is min-content — the widest thing it contains that
   cannot break. With the sign-on row rendered, `.auth-sso-btn`'s wide side
   padding around a long provider name ("Continue with Demo identity provider
   (local)") sets that minimum and drags the whole column past the viewport on
   a phone. `.hero { overflow: hidden }` then trims it silently, which is why
   a document-scroll-width check comes back clean while the headline is being
   cut off: there is nothing to scroll to, it is clipped.

   min-width: 0 lets the column shrink to its container, and the row wraps
   instead. Credit to session 61 for finding it. */
body.land .hero-inner { min-width: 0; }
/* ---------------------------------------- the declared tile, measured

   Three rules in this file now set the declared tile's colour, and a fourth
   thing — `opacity: .5` on `.tile-dim` itself — multiplies whatever any of
   them chose. Measured in the browser against the panel's own ground
   (rgb(23,19,26)), the result was: name 1.40:1, icon 1.84:1. A built tile's
   name is 3.61:1. WCAG AA for text is 4.5:1, so the declared tiles were not
   quiet, they were invisible, and the state they encode could not be read.

   "Bright is built, dim is declared" stays — it is the honest difference
   between a connector you can use today and one this build only names, and
   nothing here collapses the two. What changes is that declared is now a
   colour rather than a transparency: the parent's opacity multiplier is
   removed, so one declared colour means one rendered colour instead of being
   halved again by an ancestor. Set once, here, for icon and name together.

   Appended rather than edited into the three rules above, deliberately: this
   file has had three sessions in it within the hour and a whole-file rewrite
   would lose someone's work. It is last, so it wins; the rules above are left
   exactly as their authors wrote them. They should be consolidated into this
   one when the file has a single owner again. */

/* ------------------------------------------- the composer's width, restored

   `body.land .hero-inner > *  { max-width: 100% }` a few rules up is the
   overflow guard for the hero column, and it is right to be there — but at
   (0,2,1) it outranks `body.land .hero-composer` at (0,2,0), so it also
   overwrote the composer's own 608px cap. The composer stretched the full
   column, 1240px at a 1280 viewport, and `margin: 0 auto` then had nothing
   left to centre: the reference's one object sitting on the wash became a
   search bar spanning the page, which is the exact thing the 608px was for.

   Both intentions in one value at matching specificity: min() keeps the
   608px and still never exceeds the container, so the guard is intact and
   nothing overflows on a phone. */
/* The composer's own cap is declared once, where the composer is described
   (`body.land .hero-composer`, above), and the blanket guard excludes it by
   name there. Repeating it here as well was how three rules came to assert one
   number: two sessions fixed this independently, agreed on the value, and left
   the next person to change 608 with two of the three to find. Only .land-sso
   needs a cap at this point in the file, because the guard does apply to it. */
/* Correction to the rule directly above, which overshot: lifting declared to
   6.28:1 put it ABOVE built at 3.61:1 and inverted the meaning — the tiles the
   build only names read louder than the ones you can use.

   The measurement showed the real constraint. Built names sit at 3.61:1
   because `.tile-name` carries `opacity: .82`, so there is no colour at which
   declared can clear AA's 4.5:1 for text and still stay below built. Both had
   to move: built goes to full strength, and declared takes a fixed colour
   comfortably under it. Order restored, and both are now readable. */

/* ============================================ the provider wall: one rule set

   CONSOLIDATION. Three sessions styled this grid inside an hour and, all
   appending rather than rewriting, left seven rules setting the same three
   properties on `.tile-name` and `.tile-dim`. Nothing was broken by that, but
   the colour written in the file had stopped predicting the colour on screen —
   `.tile-dim { opacity: .5 }` was multiplying whatever any later rule chose,
   which is how a declared tile ended up at 1.40:1 without anyone seeing it.
   Everything the three of us established is kept; this is the single place it
   is now said. Appended rather than merged in place because the file is still
   shared — it wins by being last, and the rules above it are inert.

   What is kept, and why:

   * the built / declared distinction, which is the whole point of the grid.
     A dim tile means "declared, not in this build" and must keep reading as a
     state rather than as a rendering fault. Measured against the panel's
     rgb(23,19,26): built 11.82:1, declared 4.82:1 — both clear of AA's 4.5:1
     for text, with a seven-point gap between them so the two states are still
     obviously two states. The earlier attempt that lifted declared to 6.28:1
     put it *above* built and inverted the meaning; that is the trap here.
   * opacity as a mechanism is dropped. Colour is stated once, directly, so
     the value in the file is the value that renders.
   * the caption comes back up to 12px. It had been taken to 10px, which is
     below where a name is comfortably readable at arm's length — and the name
     is the thing that says which service is meant. The mark beside it is
     decorative; the text is not, so it is the text that must not shrink. */
body.land .tile-name {
  font-size: .75rem; font-weight: 500; letter-spacing: 0; line-height: 1.25;
  opacity: 1;
}
body.land .tile,
body.land .tile-ico { opacity: 1; }
body.land .tile-dim,
body.land .tile-dim .tile-ico,
body.land .tile-dim .tile-name { color: #a3959d; opacity: 1; }   /* 5.6:1 on the tile; #8d7f87 was 4.4 */

/* The marks follow the same two states as the names, so a tile is one state
   and not two. A monochrome mark is painted by the filter in logos.css and
   takes its tone from here; a colour mark keeps its own fills and is dimmed
   instead, which is the only way to say "declared" about a mark that cannot
   be recoloured without destroying it. */
body.land .tile-ico .tile-logo { opacity: 1; }
body.land .tile-dim .tile-ico .tile-logo { opacity: .45; }

@media (max-width: 560px) {
  body.land .tile-name { font-size: .75rem; }
}


/* ===================================================== measured figures

   The provenance line under each figure. Small and quiet: it is there to be
   checkable, not to be read first. It is the page applying its own citation
   rule to itself, so it must survive a redesign rather than be the first
   thing dropped for being untidy. */
body.land .stats .stat-source {
  /* The figures sit in a column-reverse flex box, so the first item in order
     lands at the BOTTOM. Without this the provenance line was laid out above
     the number it belongs to, reading as a heading. `order: -1` sends it to
     the start of the sequence and therefore to the foot of the figure, which
     is where a citation goes. */
  order: -1;
  margin: .15rem 0 0;
  font-size: .75rem;
  line-height: 1.45;
  color: var(--text-3);
  max-width: 34ch;
}

/* The chart beside them: test functions per area, drawn from the same sweep
   that produced the figures. Bars rather than a curve, because a count is
   not a trend and the old curve implied one it had no data for. */
body.land .bars { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }
body.land .bar-row {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr) 3rem;
  align-items: center;
  gap: .7rem;
}
body.land .bar-name {
  font-size: .82rem; color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body.land .bar-track {
  height: 9px; border-radius: 5px; background: var(--canvas-2); overflow: hidden;
}
body.land .bar-fill {
  display: block; height: 100%; width: var(--w, 0%);
  border-radius: 5px;
  background: linear-gradient(90deg, #6b46a8 0%, #d63c74 62%, #f4905e 100%);
  /* Grows in as the section arrives, with the reveal rather than against it. */
  transform-origin: left center;
  animation: bar-grow .9s cubic-bezier(.22, .61, .36, 1) both;
}
body.land .bar-value {
  font-size: .82rem; font-variant-numeric: tabular-nums;
  color: var(--text-2); text-align: right;
}
@keyframes bar-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) {
  body.land .bar-fill { animation: none; }
}
@media (max-width: 560px) {
  body.land .bar-row { grid-template-columns: 6.5rem minmax(0, 1fr) 2.6rem; gap: .5rem; }
}

/* The composer keeps its own scrollbar out of the box.

   Making scrollbars visible globally (ovision.css) had one casualty here:
   ::-webkit-scrollbar applies to every scroll container, and this textarea
   carries `overflow-y: auto` so that a very long question can still scroll
   once the auto-grow has hit its 220px ceiling. Measured empty, it was not
   overflowing at all — scrollHeight 42 against clientHeight 42 — and was
   still reserving 11px, leaving a dead strip down the inside of the one
   control the page is built around.

   Zero-width rather than `overflow: hidden`: the wheel, the keyboard and a
   drag-select all still scroll a question past the ceiling, and only the
   chrome goes. The gutter the global rule reserves is for documents, not for
   a two-line input. */
body.land .hero-composer .composer-input::-webkit-scrollbar { width: 0; height: 0; }
body.land .hero-composer .composer-input { scrollbar-width: none; }

/* ------------------------------------------------- our own mark, on the tile

   The mark is two <circle> elements — a stroked ring and a filled dot, both
   painted from the same linearGradient. `.tile-ov .logo path { fill: #fff }`
   in ovision.css, and the copy of it I added here, therefore selected
   nothing at all: there is no <path> in this SVG. The mark kept its own H&E
   gradient and sat on a tile painted in the same three colours, so violet
   fell on violet and coral on coral and the one tile that has to be
   unmistakable read as a smudge.

   White, on both shapes this time. The dot clears the ring's inner edge by
   about half a unit at this radius, so the two stay legibly separate rather
   than fusing into a blob, and the gradient underneath does the colour. */
body.land .tile-ov .logo-ring { stroke: #fff; }
body.land .tile-ov .logo-dot  { fill: #fff; }

/* A little depth, so the mark sits ON the gradient rather than being cut out
   of it. Kept well under the tile's own glow — this is definition, not a
   second light source. */
body.land .tile-ov .logo { filter: drop-shadow(0 2px 6px rgba(70, 12, 40, .34)); }


/* ------------------------------------------------- the hero wash, deepened

   The shared mesh is built pale: a 97%-opaque white bloom 58% x 44% across the
   centre, over a base that is near-white paper for its first third. That keeps
   a near-black headline legible anywhere in the upper half, and it is why the
   landing hero read as milk rather than as colour.

   Deepened here for the landing hero only. The legibility pocket stays but is
   smaller and softer (42% x 30%, .88), so the headline still has somewhere to
   sit while the violets and magentas either side of it carry their real
   saturation. The top 18% is left near-paper because the nav's dark text sits
   on it. Scoped to `body.land .hero`, so the sign-in panel's use of the same
   mesh is untouched.

   Restated for all three theme states rather than written once. `body.land
   .hero .hero-wash` is (0,3,1) and theme-dark.css's `:root[data-theme="dark"]
   .hero-wash` is (0,2,1), so a light-only override here would out-specify it
   and paint a light mesh on a dark page — the exact failure the closing wash
   hit earlier in this file. The dark values below are theme-dark.css's own,
   unchanged: dark is already dark, and this change is about light. */

/* REVERTED, at the user's instruction, and here is the reasoning so it is not
   undone again. The saturated mesh above this one read as a dense violet
   slab: the headline sat dark-on-dark in the middle of it and the page lost
   the airiness that made it look like the reference in the first place.

   The white bloom was diagnosed as "bleaching the centre". It is not a
   bleach, it is the whole trick. The reference's hero is brightest exactly
   where its headline and composer sit and saturates as it travels outward
   and downward — measured off it: pale through the middle, periwinkle at the
   left edge, violet at the right, magenta across the foot, coral in the
   bottom corners. Light in the middle is what lets a near-black headline sit
   on a coloured hero at all; take it out and you are choosing between a
   washed-out headline and a washed-out hero.

   So: the original mesh's structure and its full-strength centre, with the
   edge blobs carrying more colour than the first version had. Richer at the
   rim, bright through the middle — which is the reference's actual shape,
   rather than either extreme we have shipped today. */
/* The hero paints the product's one wave (`--ov-wave`, ovision.css), so the
   landing and the signed-in home are the same picture. The dark values come
   with the token; nothing is restated here. */
body.land .hero .hero-wash { background: var(--ov-wave); }
/* On a phone the title and its line wrap wider than the wave's cream top, so
   the subtitle's ends sat on the blue band (4.2:1). A veil of the same cream
   under the two, on phones only; the desktop hero stays exactly the wave. */
@media (max-width: 640px) {
  body.land .hero .hero-wash {
    background:
      radial-gradient(90% 20% at 50% 42%, rgba(248, 247, 244, .82) 0%, rgba(248, 247, 244, .6) 55%, rgba(248, 247, 244, 0) 100%),
      var(--ov-wave);
  }
}

/* The sign-on row, sized for a hero rather than for a form.

   auth-sso.css draws these for the sign-in card, where a full-width button is
   right. Dropped into the hero under the composer, one of them — and the demo
   provider's name is the longest string on the page — became a white slab
   competing with the question box, which is the one thing the hero is for.
   Narrower, lighter, and clearly secondary to the composer above it. It still
   only renders where a provider actually resolves. */
/* ====================================== three deltas against the reference
   Measured against lovable.dev; handed over by session 61 with its agreement.

   1. THE HEADER SEAM. We drew a solid light bar across the top with a hard
      bottom edge, so the wash began below it and the top of the gradient was
      cut off by a cream band at y=0..73. The reference has no bar at all: its
      nav sits directly on the wash and the pale strip at the very top is the
      wash's own top, not a different surface. This is the other half of the
      `position: static` change made earlier for the same reason.

      The bar comes out of flow so the hero — and therefore the wash — starts
      at the document top and runs up behind the nav. The hero pays back the
      bar's height as padding, so the headline still centres in the space
      below it rather than under it. `--land-bar-h` keeps the two in step. */
body.land { --land-bar-h: 4.6rem; }
body.land .land-bar {
  position: absolute; top: 0; left: 0; right: 0;
  z-index: 20;
  background: transparent;
  border: 0;
  box-shadow: none;
}
body.land .hero { padding-top: var(--land-bar-h); }

/*   2. THE SEND KEY. Ours rested as a pale tint with a grey arrow. The
      reference's is a solid near-black circle with a white arrow at full
      contrast even on an empty field: it is the one dark object in the hero
      and it is what anchors the composer to the page. So the resting state
      becomes the filled one.

      Through --send-bg/--send-fg rather than a literal, for the reason the
      rule above this one already records: --ink-900 inverts to pale pink in
      dark, and a literal white on it measures 1.24:1. `.has-text` is left
      exactly as it is — it now shares the resting appearance and keeps its
      own hover, which is the right relationship when the control is always
      live. */
body.land .hero-composer .composer-send {
  background: var(--send-bg);
  color: var(--send-fg);
}
body.land .hero-composer .composer-send .ico { color: var(--send-fg); }
body.land .hero-composer .composer-send:hover { background: var(--send-bg-hover); }

/*   3. THE COMPOSER WAS SQUAT. Same 608 width and 28px radius as the
      reference — both already right — but the placeholder sat close to the
      top edge and the control row close to the bottom, so the box read as
      compressed where the reference reads open. The width and the radius do
      not move; only the air inside them. */
body.land .hero-composer { padding: 6px; }
body.land .hero-composer .composer-input {
  min-height: 3.1rem;
  padding: 1.15rem 1.15rem .35rem;
}
body.land .hero-composer .composer-bar { padding: .4rem .7rem .7rem; }

/* ------------------------------------------- the dark band's copy, pinned

   The reference's version of this section has a short copy column beside a
   much taller panel, and the copy is sticky: it stops and holds while the
   panel travels past it, so the sentence explaining the wall is still on
   screen when you reach the bottom of the wall. Ours scrolled away at the
   top and left the reader looking at a grid of marks with nothing saying
   what they were.

   Desktop only. Below the point where the two columns stack there is nothing
   to be sticky beside — the copy would pin over the wall it introduces. The
   offset clears the top of the viewport by a comfortable margin rather than
   sitting flush, which is what the reference does.

   `align-items: start` is the part that actually makes it work: the grid's
   default `stretch` makes both columns the height of the row, and a sticky
   element the full height of its container has nowhere to move. */
@media (min-width: 940px) {
  body.land .how-inner { align-items: start; }
  body.land .how-copy { position: sticky; top: 5rem; }
}

/* ================================================= the three cards, alive

   The reference's trio are not pictures, they are loops: a question types
   itself, a pointer walks in and selects a heading, a menu steps down its own
   items. Ours were three still frames of the same idea. Same three loops
   here, in CSS only — no script, nothing to strand in an unpainted tab, and
   each one is the real surface it depicts rather than an invented one.

   All three are gated on prefers-reduced-motion at the bottom of the block,
   where they resolve to the finished frame: typed, selected, highlighted.
   A loop that cannot be stopped is not decoration, it is a distraction. */

/* ---- 1. the question types itself --------------------------------------

   Rewritten, because every fault in the first version came from animating the
   wrong thing. It grew a block element's `width` from 0 to 100% in
   `steps(44, end)`, which cost it three ways:

     1. Forty-four steps across the BOX, not the string. A step advanced by a
        forty-fourth of the card's width rather than by a character, so glyphs
        arrived in uneven clumps, half-drawn at the clip edge.
     2. The caret was that element's right border, and at `width: 100%` the
        element is the card, not the sentence — so the caret came to rest a
        clear inch to the right of the last letter it had just typed. That is
        the artefact you see in a screenshot of the old card.
     3. It blinked while typing, which no text cursor does, and the reset
        ran the same forty-four steps backwards at four times the writing
        speed. A fade was added over that reset to hide it; a reset that has
        to be hidden is the wrong reset.

   What replaces it. The text is CLIPPED rather than resized, so nothing
   reflows and no glyph is ever half a glyph wide. The clip is stepped by the
   string's own character count, handed down by the template as
   `--typed-chars`, so one step is one character and the sentence can be
   edited without this file going stale. The caret is its own element, moved
   by `left` on the same stepped timeline, so it rides the clip edge and comes
   to rest exactly at the full stop. And the line is erased by a deliberate
   sweep back rather than snapped — `animation-timing-function` inside the
   keyframes is what lets one animation step through the typing and glide
   through the erase, which is why there is nothing left to cover up.

   The base state — no animation, reduced motion, print, or a browser that
   cannot animate a clip — is the sentence fully typed with the caret at its
   end. The loop is an enhancement on a legible frame, never the way into
   one. */
/* The field. As wide as the sentence where the card has room, and as wide as
   the card where it does not — `overflow: hidden` is what makes the second
   case work twice over: it clips a question longer than the field, the way a
   one-line input does, and it stops that question from widening the card it
   sits in: without it the nowrap string set the card's minimum width, and at
   every layout narrower than the sentence — a phone, and the 3-up cards
   between 860 and 1100px — the white box grew past its own container. */
/* Two declarations do the confining, and both are needed — measured at
   390px, where the card was 365px wide inside a 331px column:

     `overflow` on the field card zeroes what it contributes to its own grid
     track, so the track stretches to the column (283px) instead of being
     forced to the sentence's width (365px);

     `max-width` on the line caps it at the card's content box (248px) once
     that box is definite. A percentage is ignored while a box is being sized
     BY its content, which is why it did nothing on its own, and why an
     explicit `width: max-content` here was worse than useless: a specified
     width is a contribution, and it put the card straight back to 365px.

   The sentence is then longer than the field it sits in, and is cut off at
   the field's edge with the caret waiting there — which is what a one-line
   input does with a question too long for it. */
body.land .mini-composer { overflow: hidden; }
body.land .mini-line {
  position: relative;
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  vertical-align: bottom;
}
/* The layer the reveal walks across. A block, so it is exactly the field's
   width — which is the whole point: the clip below and the caret's `left` are
   then percentages of the same box, so they cannot drift apart at any width.
   Stepping the BOX while revealing the TEXT is the bug the old version had. */
body.land .mini-reveal {
  display: block;
  clip-path: inset(0 0 0 0);
  animation: mini-type 9s infinite;
}
body.land .mini-text { display: inline-block; white-space: nowrap; }
/* Outside the clipped layer, inside the field: a caret inside the reveal
   would be clipped away by it. `-1.5px` keeps its own width inside the field's
   right edge, so it is not the first thing `overflow: hidden` eats. */
body.land .mini-caret {
  position: absolute;
  left: 100%;
  margin-left: -1.5px;
  top: .1em;
  width: 1.5px;
  height: 1.15em;
  background: var(--ink-600);
  animation: mini-run 9s infinite, mini-blink 9s steps(1, end) infinite;
}
/* One timeline, four phases: type (0-38%), hold (38-82%), erase (82-92%),
   empty field (92-100%). */
@keyframes mini-type {
  0%         { clip-path: inset(0 100% 0 0); animation-timing-function: steps(46, end); }
  38%        { clip-path: inset(0 0 0 0);    animation-timing-function: linear; }
  82%        { clip-path: inset(0 0 0 0);    animation-timing-function: cubic-bezier(.4, 0, .2, 1); }
  92%, 100%  { clip-path: inset(0 100% 0 0); }
}
@keyframes mini-run {
  0%         { left: 0;    animation-timing-function: steps(46, end); }
  38%        { left: 100%; animation-timing-function: linear; }
  82%        { left: 100%; animation-timing-function: cubic-bezier(.4, 0, .2, 1); }
  92%, 100%  { left: 0; }
}
/* Solid while the caret is moving, blinking only while it waits — which is
   what a text cursor does, and the reason the old one read as restless. */
@keyframes mini-blink {
  0%, 38%    { opacity: 1; }
  41.5%      { opacity: 0; }
  46%        { opacity: 1; }
  50.5%      { opacity: 0; }
  55%        { opacity: 1; }
  59.5%      { opacity: 0; }
  64%        { opacity: 1; }
  68.5%      { opacity: 0; }
  73%        { opacity: 1; }
  77.5%      { opacity: 0; }
  82%, 100%  { opacity: 1; }
}

/* ---- 2. a pointer walks in and selects the guard ------------------------
   The dashed ring and the pointer arrive together, hold while the sentence
   can be read, and leave — the same beat as the reference's middle card,
   where the cursor selects the headline and the selection box appears around
   it. The pointer is drawn, not an image: a rotated square with one corner
   squared off is a passable arrow at this size and costs no request. */
body.land .trio-art:has(.mini-guard) { position: relative; overflow: hidden; }
body.land .mini-guard {
  position: relative;
  outline: 1.5px dashed transparent;
  outline-offset: 3px;
  animation: mini-select 9s ease-in-out infinite;
}
body.land .mini-guard::after {
  content: "";
  position: absolute;
  left: 58%; top: 74%;
  width: 13px; height: 13px;
  background: var(--pointer-fill, #17131a);
  /* A ring in the opposite value, so the arrow reads on a white panel
     and on a plum one without being redrawn for either. */
  outline: 1px solid var(--pointer-ring, transparent);
  clip-path: polygon(0 0, 0 100%, 28% 72%, 45% 100%, 62% 90%, 45% 62%, 82% 55%);
  filter: drop-shadow(0 1px 2px rgba(23, 19, 26, .35));
  /* The shadow is a filter on a thing that moves every frame, so the pointer
     asks for its own layer: rasterise the arrow and its shadow once, then
     move the layer. Without this the browser re-renders the blur on each
     frame of a nine-second loop, which is where the judder came from. */
  will-change: transform;
  animation: mini-pointer 9s infinite;
}
@keyframes mini-select {
  0%, 22%    { outline-color: transparent; }
  30%, 78%   { outline-color: var(--ink-500); }
  86%, 100%  { outline-color: transparent; }
}
/* One easing per gesture rather than `ease-in-out` over all of them: the
   approach decelerates into the target the way a hand does, the press is
   quick down and quick up, and the exit accelerates away. An ease-in-out
   shared by the travel and the click made the arrival drift and the click
   mushy — it read as a thing being dragged, not a thing being used. */
@keyframes mini-pointer {
  0%         { transform: translate(30px, 34px); opacity: 0;
               animation-timing-function: cubic-bezier(.22, .61, .36, 1); }
  14%        { opacity: 1; }
  26%        { transform: translate(0, 0); opacity: 1;
               animation-timing-function: cubic-bezier(.4, 0, 1, 1); }
  30%        { transform: translate(1px, 1px) scale(.88);
               animation-timing-function: cubic-bezier(0, 0, .2, 1); }
  34%, 74%   { transform: translate(0, 0) scale(1); opacity: 1;
               animation-timing-function: cubic-bezier(.55, 0, .85, .5); }
  88%, 100%  { transform: translate(30px, 34px); opacity: 0; }
}

/* ---- 3. the list steps down its own rows -------------------------------
   `.on` is fixed on one row in the markup, because without script that is
   the row a reader should see. The loop moves the same treatment down the
   five rows on a stagger, then hands it back. Each row animates itself, so
   there is no state to keep and nothing to get out of step. */
/* Two things were wrong with this and both of them were movement.

   The weight was animated, 400 to 600 and back, on text that sits beside an
   icon in a flex row. Every frame of that re-measured the glyphs and shifted
   the row's contents a fraction of a pixel, five rows at a time — the jitter
   you could see but not name. Weight is fixed now; the highlight is carried
   by the background and the colour, neither of which touches layout, which is
   also how a real list highlight behaves.

   The stagger was 0.7s into a 2.6s-long lit window, so four of the five rows
   were lit at once and the thing read as a blob breathing rather than a
   selection stepping. The lit window is now shorter than the stagger, so one
   row hands to the next with a frame to spare and the eye follows a single
   highlight down the list. */
body.land .mini-list span {
  animation: mini-step 10s ease-in-out infinite;
  border-radius: 8px;
}
body.land .mini-list span:nth-child(1) { animation-delay: 0s; }
body.land .mini-list span:nth-child(2) { animation-delay: 1.5s; }
body.land .mini-list span:nth-child(3) { animation-delay: 3s; }
body.land .mini-list span:nth-child(4) { animation-delay: 4.5s; }
body.land .mini-list span:nth-child(5) { animation-delay: 6s; }
/* The markup's own highlight is handed to the loop, so one row is not
   permanently lit while the others take turns. */
body.land .mini-list span.on { background: transparent; color: var(--text-2); font-weight: 400; }
body.land .mini-list span.on .ico { color: var(--text-3); }
@keyframes mini-step {
  0%, 2%    { background: transparent; color: var(--text-2); }
  7%, 16%   { background: var(--canvas-2); color: var(--heading, #17131a); }
  21%, 100% { background: transparent; color: var(--text-2); }
}

/* ---- the off switch ----------------------------------------------------
   Reduced motion gets the finished frame of each loop, not a blank one: the
   question fully typed, the guard selected, the row the markup marks. */
@media (prefers-reduced-motion: reduce) {
  body.land .mini-reveal { animation: none; clip-path: none; }
  body.land .mini-caret { animation: none; opacity: 1; }
  body.land .mini-guard { animation: none; outline-color: var(--ink-500); }
  body.land .mini-guard::after { animation: none; transform: none; opacity: 1; }
  body.land .mini-list span { animation: none; }
  body.land .mini-list span.on { background: var(--canvas-2); color: var(--heading, #17131a); font-weight: 600; }
  body.land .mini-list span.on .ico { color: var(--ink-600); }
}

/* ------------------------------------------------------- the bar, cleaned

   Measured against the reference rather than adjusted by eye. Two things
   were actually wrong; everything else already matched at 15px/400 with
   4px 6px of padding on each link.

   1. The links were touching. Our gap was 1.6rem/10 — 1.6px — against the
      reference's 14px, so "Platform How it works Numbers" read as one run of
      text with faint seams rather than three separate destinations. This is
      the whole difference in how crowded the two bars feel.

   2. The two buttons were different heights: 36px and 34px. `.land-login`
      carries a 1px border and `.land-cta` does not, and a border adds to the
      box — so the quiet button was two pixels taller than the loud one
      sitting next to it. A transparent border on the filled one puts them on
      the same box model and the same baseline, without drawing anything. */
body.land .land-nav { gap: .875rem; }
body.land .land-nav a { padding: .25rem .375rem; }
body.land .land-cta { border: 1px solid transparent; }


/* ======================================== the measured curve, and its arrival

   `landing_facts.curve()` counts the test functions in `tests/` at every
   commit in this repository's history and hands the template a path. This is
   how it is drawn and how it enters.

   The look, in the reference's terms: a thin ramp in the brand's three stops,
   a fill under it that fades away downward rather than sitting as a slab, no
   gridlines, no axis furniture, and the two ends labelled — which is the
   difference between a chart and a table of thirty-four numbers read aloud.

   The entrance is one gesture: the drawing is clipped to nothing and the clip
   opens left to right, so the line appears to draw itself and the fill to
   grow under it in the same movement. One clipped box, one property, and both
   mechanisms below animate the same thing — nothing can half-run.

   Why the clip sits on the <svg> and not on a <clipPath> inside it: an
   element in <defs> generates no box, so `animation-timeline: view()` has
   nothing to resolve against and the animation freezes on its first frame,
   which here is a chart clipped to zero width. An invisible chart is the one
   failure this section cannot have. */
body.land .trend { margin: 0 0 1.75rem; }
/* The box the marks are positioned against. It is exactly the drawing's box,
   so a percentage here is a percentage of the viewBox. */
body.land .trend-plot { position: relative; }

body.land .trend-svg {
  display: block;
  width: 100%;
  height: auto;
  /* The bottom edge of the viewBox IS the zero line — the scale puts a count
     of nought on y = 300 — so a 1px rule under the box is an honest axis and
     not a decoration. It also gives the fading fill something to end on. */
  border-bottom: 1px solid var(--line);
}
body.land .trend-line { stroke-width: 2.5; }
/* The uncommitted run. Same ramp, same weight, dashed — one state, said
   once in the drawing and once in the caption, never colour alone. */
body.land .trend-tail { stroke-width: 2.5; }
/* The fill's tone and the depth of its fade are theme tokens, not literals:
   the same alpha that reads as a wash on cream reads as mud on plum-black.
   The markup carries the light values as presentation attributes, so a page
   that loses this sheet still draws the chart correctly. */
body.land .trend-area { mix-blend-mode: var(--trend-blend, normal); }
body.land #trend-fade stop:nth-child(1) { stop-opacity: var(--trend-fill-top, .5); }
body.land #trend-fade stop:nth-child(2) { stop-opacity: var(--trend-fill-mid, .16); }
/* The end cap, ringed in the page's own ground so it reads as a marker on the
   line rather than a bead threaded on it. 9px across, because a marker any
   smaller stops being a marker. */
body.land .trend-dot {
  position: absolute;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;          /* centred on the point it marks */
  border-radius: 50%;
  background: #f4905e;
  box-shadow: 0 0 0 2.5px var(--land-canvas, var(--canvas, #fbf8f9));
}
/* Values wear the page's text colour, never the series colour: the ramp
   carries identity, the number carries the reading. */
body.land .trend-mark {
  position: absolute;
  transform: translateY(-1.55em);
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: -.01em;
  line-height: 1;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* The end number hangs off its own right edge, so it can sit on the last
   point without leaving the drawing. */
body.land .trend-mark-end { transform: translate(-100%, -1.55em); }
/* Every vertex is hoverable and names its commit. `transparent` rather than
   `none`, because `fill: none` is not a hit target. */
body.land .trend-hits circle { fill: transparent; pointer-events: all; }
body.land .trend-hits circle:hover { fill: rgba(214, 60, 116, .14); }

body.land .trend-caption {
  font-size: .8rem;
  line-height: 1.5;
  color: var(--text-3);
  margin: .55rem 0 0;
}
/* The dates and shas the curve runs between, sat under the ends they label. */
body.land .trend-axis {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: .45rem;
  font-size: .75rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
}

/* The chart's own source rows. Closed by default — it is provenance, not
   reading matter — and it is the accessible view of the drawing as well as
   the checkable one. */
body.land .trend-data { margin: .6rem 0 0; }
body.land .trend-data summary {
  cursor: pointer;
  font-size: .78rem;
  color: var(--text-2);
  padding: .2rem 0;
}
body.land .trend-data summary:hover { color: var(--text); }
body.land .trend-scroll { max-height: 15rem; overflow: auto; margin-top: .5rem; }
body.land .trend-data table {
  width: 100%;
  border-collapse: collapse;
  font-size: .76rem;
  font-variant-numeric: tabular-nums;
}
body.land .trend-data th {
  text-align: left;
  font-weight: 600;
  color: var(--text-3);
  padding: .3rem .6rem .3rem 0;
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  background: var(--land-canvas, var(--canvas, #fbf8f9));
}
body.land .trend-data td {
  padding: .26rem .6rem .26rem 0;
  color: var(--text-2);
  border-bottom: 1px solid var(--line);
}
body.land .trend-data td:last-child, body.land .trend-data th:last-child { text-align: right; }
body.land .trend-data code { font-size: .95em; color: var(--text-2); }

/* ---- the arrival, mechanism 1: the observer's class ---------------------
   Only ever narrowed under `reveal-on`, which is the class the script adds
   after it has decided it will be the one to widen it again. No script, no
   clip, whole chart — the same contract every other reveal on this page
   keeps. */
@keyframes trend-sweep {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}
body.reveal-on .trend-svg { clip-path: inset(0 100% 0 0); }
body.reveal-on .reveal.is-in .trend-svg {
  animation: trend-sweep 1.25s cubic-bezier(.22, .61, .36, 1) .12s both;
}
/* The dot and the two numbers are HTML siblings of the drawing, so the clip
   that sweeps the curve cannot carry them. They arrive on the tail of it
   instead — the curve draws, the end of it is named — which is one gesture
   even though it takes two properties to say. */
@keyframes trend-marks-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
body.reveal-on .trend-dot, body.reveal-on .trend-mark { opacity: 0; }
body.reveal-on .reveal.is-in .trend-dot,
body.reveal-on .reveal.is-in .trend-mark {
  animation: trend-marks-in .45s ease-out 1.05s both;
}

/* ---- mechanism 2: the scroller itself ----------------------------------
   Where the browser has scroll-driven animations the script stands down, and
   the curve draws as the band travels up the viewport — the reference's own
   behaviour, and nothing to miss in a tab that is not being painted.
   `cover` for the reason the reveal above uses it: `entry` never completes
   for an element taller than the viewport. */
@supports (animation-timeline: view()) {
  body.land .trend-svg {
    animation: trend-sweep both linear;
    animation-timeline: view();
    animation-range: cover 6% cover 34%;
  }
  body.land .trend-dot, body.land .trend-mark {
    animation: trend-marks-in both linear;
    animation-timeline: view();
    animation-range: cover 27% cover 37%;
  }
}

/* ---- the off switches --------------------------------------------------
   Reduced motion and print both get the finished frame: the whole chart, no
   clip, no animation. A chart is information; only its arrival was ever
   decoration. */
@media (prefers-reduced-motion: reduce) {
  body.reveal-on .trend-svg,
  body.reveal-on .reveal.is-in .trend-svg { animation: none; clip-path: none; }
  body.reveal-on .trend-dot, body.reveal-on .trend-mark,
  body.reveal-on .reveal.is-in .trend-dot,
  body.reveal-on .reveal.is-in .trend-mark { animation: none; opacity: 1; }
  @supports (animation-timeline: view()) {
    body.land .trend-svg { animation: none; clip-path: none; }
    body.land .trend-dot, body.land .trend-mark { animation: none; opacity: 1; }
  }
}
@media print {
  body.reveal-on .trend-svg,
  body.reveal-on .reveal.is-in .trend-svg { animation: none !important; clip-path: none !important; }
  body.land .trend-dot, body.land .trend-mark { animation: none !important; opacity: 1 !important; }
  body.land .trend-data[open] summary { list-style: none; }
  body.land .trend-scroll { max-height: none; overflow: visible; }
}

@media (max-width: 560px) {
  /* A phone's column makes the plot about 130px tall, and a number floated
     above the curve in that space either overlaps the line or leaves the box.
     Both figures are in the caption below it — "335 rising to 931" — so the
     marks come off rather than being shrunk into illegibility. The dot stays:
     it says where the series ends without needing room for type. */
  body.land .trend-mark { display: none; }
  body.land .trend-axis { font-size: .75rem; }
}

/* ==================================================== the parts (#parts)

   The reference's customer-logo band, with the logos replaced by the things
   a released passport is made of (the reasoning is in landing.html above the
   section). Structurally it is the reference's: three columns of cells and a
   taller cell of copy beside them, the whole thing ruled into a grid.

   The rules are drawn by the grid rather than by borders on the cells. The
   container is painted in the rule colour and every cell in the page's own
   ground, so a 1px gap IS the line: it stays hairline-thin at any zoom, it
   never doubles where two cells meet, and it re-draws itself correctly when
   the column count changes at a breakpoint — which is the part that borders
   on cells get wrong, because the cell that ends a row is different at three
   columns and at two. */
body.land .parts-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(0, 1.15fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 14px;
  /* `clip`, not `hidden`, and the difference is not cosmetic: `overflow:
     hidden` makes an element a scroll container even when nothing can scroll
     in it, and a scroll container is what `animation-timeline: view()`
     resolves against. Every `.reveal` cell in here was therefore timed
     against a box that never moves, so its entrance froze part-way and the
     row sat at a fraction of full opacity for the life of the page. `clip`
     crops exactly the same pixels without claiming to scroll, so the cells
     are timed against the page again. It is the only clip on this page with
     a `.reveal` inside it, and the corners still round. */
  overflow: clip;
}
body.land .part {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  padding: 1.5rem 1.35rem;
  background: var(--land-canvas);
  min-width: 0;
}
body.land .part-ico { display: block; line-height: 0; color: var(--ink-600); margin-bottom: .35rem; }
body.land .part h3 {
  margin: 0;
  font-size: .95rem;
  font-weight: 620;
  letter-spacing: -.012em;
  color: var(--text);
  overflow-wrap: anywhere;
}
body.land .part p { margin: 0; font-size: .84rem; line-height: 1.55; color: var(--text-2); }

/* The copy cell: column four, both rows, as the reference's is. */
body.land .part-copy {
  grid-column: 4;
  grid-row: 1 / span 2;
  gap: .75rem;
  padding: 1.75rem 1.5rem;
  justify-content: center;
}
body.land .part-copy h2 {
  margin: 0;
  font-size: 1.22rem;
  font-weight: 700;
  letter-spacing: -.022em;
  line-height: 1.2;
  color: var(--text);
  text-wrap: balance;
}
body.land .part-copy p { font-size: .88rem; }
body.land .part-note { color: var(--text-3); font-size: .82rem; }

/* The command, not a button. `ovision verify` is the thing this section is
   inviting, and printing it is both the invitation and the proof that the
   verifier is a program rather than a promise. */
body.land .part-cmd { margin-top: .2rem; }
body.land .part-cmd code {
  display: inline-block;
  padding: .4rem .6rem;
  border: 1px solid var(--line-2);
  border-radius: 8px;
  background: var(--canvas-2);
  color: var(--text-2);
  font-family: var(--mono);
  font-size: .78rem;
  overflow-wrap: anywhere;
}

/* Two columns, then one. The copy cell stops spanning and simply leads, which
   is what it does on the reference at the same widths. */
@media (max-width: 1040px) {
  body.land .parts-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  body.land .part-copy { grid-column: 1 / -1; grid-row: auto; order: -1; }
}
@media (max-width: 560px) {
  body.land .parts-grid { grid-template-columns: minmax(0, 1fr); }
  body.land .part { padding: 1.2rem 1.1rem; }
  body.land .part-copy { padding: 1.4rem 1.1rem; }
}

/* ================================================================ on the wave

   The wave follows the scheme (--ov-wave: light in light, dark in dark; the
   user reversed "dark in both" the same evening). What sits directly on the
   wave takes --on-wave, and its plates, tints and outlines --on-wave-plate,
   -hover and -line, so both schemes read. The composer keeps its own
   plate and its own text. "Get started" is filled with the on-wave colour
   (ink on the light wave, white on the dark one) and "Log in" is the quiet
   outline beside it, so the pair keeps its loud-and-quiet order.

   `:not(.market)`: pricing and writing share this header (market_base.html,
   `body.land market`) but sit on their own pale tint, where the scheme's dark
   text is right. */
body.land:not(.market) .land-bar .launch-brand,
body.land:not(.market) .land-bar .wordmark-name { color: var(--on-wave); }
body.land:not(.market) .land-bar .land-nav a { color: var(--on-wave-2); }
body.land:not(.market) .land-bar .land-nav a:hover,
body.land:not(.market) .land-bar .land-nav a[aria-current="page"] {
  color: var(--on-wave); background: var(--on-wave-hover);
}
body.land:not(.market) .land-bar .land-login {
  background: var(--on-wave-plate);
  border-color: var(--on-wave-line);
  box-shadow: none;
  color: var(--on-wave);
}
body.land:not(.market) .land-bar .land-login:hover { background: var(--on-wave-hover); color: var(--on-wave); }
body.land:not(.market) .land-bar .land-cta {
  background: var(--on-wave); border-color: var(--on-wave); color: var(--ov-wave-ground);
}
body.land:not(.market) .land-bar .land-cta:hover {
  background: var(--on-wave-3); border-color: var(--on-wave-3); color: var(--ov-wave-ground);
}
body.land:not(.market) .land-bar a:focus-visible { outline-color: var(--on-wave); }
body.land .hero .hero-title { color: var(--on-wave); }
body.land .hero .hero-sub { color: var(--on-wave-2); }
/* The close runs the same wave, so the page ends on the picture it opened
   with rather than on the pastel ramp the light scheme used to have. It rises
   out of the section above: the wave's top fades in from the canvas, the
   way the hero fades out at its foot. The fade is measured in lengths, not
   percentages, and ends just inside the band's top padding (clamp(4.5rem,
   11vw, 10rem), above), so the eyebrow and the heading always sit on the wave
   itself, never on the half-faded edge: as a share of the wrapper the fade
   grew with the footer, and on a phone it ran under the heading. theme-dark.css paints `.closing-wash`
   from --wash-closing at (0,1,0); this is (0,2,1). */
body.land .closing-wash {
  --close-fade: clamp(3.75rem, calc(11vw - .75rem), 9rem);
  background: var(--ov-wave);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, rgba(0, 0, 0, .55) calc(var(--close-fade) * .5), #000 var(--close-fade));
  mask-image: linear-gradient(180deg, transparent 0, rgba(0, 0, 0, .55) calc(var(--close-fade) * .5), #000 var(--close-fade));
}
body.land .closing h2 { color: var(--on-wave); }
body.land .closing-eyebrow { color: var(--on-wave-2); }

/* Who made it (templates/landing.html): a quiet "brought to you by" line
   between the hero and the first band. It sits in the tail of the hero's
   fade ("the hero's bottom edge", above): its background starts on the
   blush the hero ends on and draws the last 8rem of the same curve to the
   ground, so the colour runs down to the line and dissolves under the logo.
   What is left under the label is at most a sixth of the foot (6:1 for the
   label in light, 7.6:1 in dark). The logo is JHU's navy on transparent; on
   the dark ground it is drawn white, the reversed form, in the same three
   theme states logos.css uses. */

/* ------------------------------------------------------------ the bar, straight

   The brand is an inline link around a flex wordmark, so it stood on the line
   box's baseline, a few pixels above the centre the nav links sit on (the
   user, 2026-10-10: "make this straight"). Every item in the bar is now a
   centred flex box with one line height, so the wordmark, the links and the
   buttons share one centre line. */
.land-bar .launch-brand { display: inline-flex; align-items: center; line-height: 1; }
.land-bar .land-nav { align-items: center; }
.land-bar .land-nav a { display: inline-flex; align-items: center; line-height: 1.5; }
.land-bar .land-meta > * { display: inline-flex; align-items: center; line-height: 1.5; }

/* ------------------------------------------------------------ the demo button

   One way in, as a button under the headline (the user, 2026-10-10: "Should
   be like get a demo"). The product's ink button at a hero's size. */
body.land .hero-cta { margin: 0 0 2.4rem; display: flex; justify-content: center; }
body.land .lt-demo { min-height: 48px; padding: 0 1.5rem; font-size: 1rem; }

/* ------------------------------------------------------------ the product, live

   The question box as the product draws it, on a card over the wave, with
   the data it reads beneath (the user, 2026-10-10: "should show a live
   search"). The card is paper with a soft lift; the chips sit on it, quiet. */
body.land .hero-inner > .lt-live { max-width: 880px; margin: 0 auto; }
body.land .lt-live {
  padding: 1.1rem 1.1rem 1rem; border-radius: 24px;
  background: color-mix(in srgb, var(--paper) 78%, transparent);
  border: 1px solid color-mix(in srgb, var(--paper) 60%, transparent);
  -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
  box-shadow: 0 1px 2px rgba(98, 16, 44, .05), 0 30px 70px -30px rgba(98, 16, 44, .45);
}
body.land .lt-live .hero-composer { max-width: none; }
body.land .lt-sources {
  list-style: none; margin: .9rem 0 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: .4rem;
}
body.land .lt-sources li {
  display: inline-flex; align-items: center; gap: .35rem; white-space: nowrap;
  padding: .3rem .7rem; border-radius: 999px; font-size: .78rem; font-weight: 600; color: var(--text);
  background: var(--paper); border: 1px solid var(--line);
}
body.land .lt-sources .ico { color: var(--text-2); }
body.land .lt-sources .brand-tile { width: 16px; height: 16px; }
body.land .lt-sources li { padding: .32rem .75rem .32rem .6rem; font-size: .8rem; }

/* ------------------------------------------------------------ the numbers

   Six cells, three across (the user, 2026-10-10: "add big stats, look at
   glean.com", then "make this big number really good"): soft cards on the
   ground, a hairline of the brand colour along the top, the number large and
   tight in the heading ink with its unit lighter, the caption in the page's
   sans. The numbers count up as the grid comes into view (landing.js; still
   under reduced motion) and are the real values either way (LANDING_STATS). */
body.land .lt-stats { padding: clamp(4rem, 9vw, 7rem) 1.25rem 2.5rem; background: var(--canvas, #fbfbfa); }
body.land .lt-stats-title {
  font-family: var(--sans); font-size: clamp(1.7rem, 3.2vw, 2.4rem); font-weight: 700; letter-spacing: -.035em;
  line-height: 1.12; color: var(--heading, var(--text)); margin: 0 auto 2rem; max-width: 22ch; text-align: center;
  text-wrap: balance;
}
body.land .lt-stats-grid {
  margin: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem;
}
body.land .lt-stat {
  position: relative; overflow: hidden; padding: 2.1rem 1.8rem 1.9rem; border-radius: 18px;
  background: var(--paper); border: 1px solid var(--line);
  box-shadow: 0 1px 2px rgba(24, 24, 27, .04), 0 18px 40px -28px rgba(98, 16, 44, .35);
  display: flex; flex-direction: column; justify-content: flex-end; min-height: 11.5rem;
}
body.land .lt-stat::before {
  content: ""; position: absolute; inset: 0 auto auto 0; height: 3px; width: 100%;
  background: linear-gradient(90deg, var(--violet) 0%, var(--ov-wave-foot) 100%);
}
body.land .lt-stat dd {
  margin: 0; font-family: var(--sans); font-size: clamp(3rem, 5.6vw, 4.6rem); font-weight: 650;
  letter-spacing: -.055em; line-height: 1; color: var(--heading, var(--text)); font-variant-numeric: tabular-nums;
}
body.land .lt-stat dd small { font-size: .42em; font-weight: 600; letter-spacing: -.02em; color: var(--text-2); margin-left: .25em; }
body.land .lt-stat dt { margin: .8rem 0 0; font-size: .95rem; line-height: 1.45; color: var(--text-2); max-width: 24ch; }
body.land .lt-stats-note { margin: 1.1rem 0 0; font-size: .8rem; color: var(--text-3); text-align: center; }
@media (max-width: 900px) { body.land .lt-stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) {
  body.land .lt-stats-grid { grid-template-columns: 1fr; gap: .75rem; }
  body.land .lt-stat { min-height: 0; padding: 1.5rem 1.3rem 1.4rem; }
}

/* ------------------------------------------------------------ the steps, small

   The four steps as one row (the user, 2026-10-10: "make sure it's small"):
   an icon, a rule, a bold lead and a sentence or two, four across. */
body.land .lt-howband { padding-top: clamp(2.5rem, 6vw, 4.5rem); }
body.land .lt-steps-row {
  list-style: none; margin: 0 auto; padding: 0; max-width: 72rem;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 2rem;
}
body.land .lt-steps-row > li { padding-top: .9rem; border-top: 1px solid var(--line-2); }
body.land .lt-step-ico { display: inline-flex; color: var(--text); margin-bottom: .7rem; }
body.land .lt-steps-row p { margin: 0; font-size: .95rem; line-height: 1.5; color: var(--text-2); }
body.land .lt-steps-row b { color: var(--heading, var(--text)); font-weight: 650; }
@media (max-width: 900px) { body.land .lt-steps-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5rem; } }
@media (max-width: 560px) { body.land .lt-steps-row { grid-template-columns: 1fr; gap: 1.2rem; } }

/* ------------------------------------------------------------ the assistant

   A card in the corner (the user, 2026-10-10: "add the bottom AI
   assistant"): a head, two actions, a log of what was asked and answered,
   the ask box, and the fine print with the notice and the terms. Hidden
   until script shows it; on a phone it starts as a button. */
body.land .lt-helper {
  position: fixed; right: 1.1rem; bottom: 1.1rem; z-index: 30; width: min(21rem, calc(100vw - 2.2rem));
  padding: .9rem .95rem .8rem; border-radius: 16px; background: var(--paper); border: 1px solid var(--line);
  box-shadow: 0 1px 2px rgba(24, 24, 27, .05), 0 24px 60px -24px rgba(24, 24, 27, .35);
  display: flex; flex-direction: column; gap: .6rem;
}
body.land .lt-helper[hidden] { display: none; }
body.land .lt-helper { font-family: var(--sans); width: min(22.5rem, calc(100vw - 2.2rem)); padding: 1rem 1.05rem .9rem; gap: .7rem; }
body.land .lt-helper-head { display: flex; align-items: center; gap: .7rem; }
/* A rounded square, not a circle: the photo is a tight square crop and a
   circle took the top of her head (the user, 2026-10-10: "fix the image of
   her head"). */
body.land .lt-helper-face { width: 46px; height: 46px; border-radius: 13px; object-fit: cover; flex: none;
                            border: 1px solid var(--line); }
body.land .lt-helper-who { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
body.land .lt-helper-who b { font-size: 1.02rem; font-weight: 650; letter-spacing: -.015em; color: var(--heading, var(--text)); }
body.land .lt-helper-who span { font-size: .82rem; color: var(--text-2); }
body.land .lt-helper-act { font-size: .9rem; padding: .58rem .8rem; }
body.land .lt-helper-row { font-size: .88rem; }
body.land .lt-helper-ask input { min-height: 40px; font-size: .92rem; }
body.land .lt-helper-ask button { width: 40px; height: 40px; }
body.land .lt-helper-x {
  margin-left: auto; width: 26px; height: 26px; padding: 0; border-radius: 8px; display: inline-grid; place-items: center;
  border: 1px solid transparent; background: transparent; color: var(--text-3); cursor: pointer; box-shadow: none;
}
body.land .lt-helper-x:hover { background: var(--canvas-2); color: var(--text); box-shadow: none; transform: none; }
body.land .lt-helper-acts { display: flex; flex-direction: column; gap: .4rem; }
body.land .lt-helper-act {
  display: block; text-align: center; padding: .5rem .8rem; border-radius: 10px; font-size: .86rem; font-weight: 600;
  color: var(--violet); background: var(--violet-2); border: 1px solid var(--violet-line); text-decoration: none;
}
body.land .lt-helper-act:hover { background: color-mix(in srgb, var(--violet-2) 70%, var(--paper)); text-decoration: none; }
body.land .lt-helper-log { display: flex; flex-direction: column; gap: .45rem; max-height: 14rem; overflow-y: auto; }
body.land .lt-helper-log:empty { display: none; }
body.land .lt-helper-row { font-size: .84rem; line-height: 1.45; padding: .5rem .65rem; border-radius: 10px; }
body.land .lt-helper-row p { margin: 0; }
body.land .lt-helper-row a { display: inline-block; margin-top: .3rem; font-size: .8rem; font-weight: 600; color: var(--violet); }
body.land .lt-helper-row.you { background: var(--canvas-2); color: var(--text); align-self: flex-end; max-width: 90%; }
body.land .lt-helper-row.ov { background: var(--violet-2); color: var(--text); }
body.land .lt-helper-ask { display: flex; gap: .4rem; margin: 0; }
body.land .lt-helper-ask input {
  flex: 1 1 auto; min-width: 0; min-height: 38px; padding: 0 .8rem; border-radius: 10px; font-size: .88rem;
  border: 1px solid var(--line-2); background: var(--paper); color: var(--text);
}
body.land .lt-helper-ask button {
  width: 38px; height: 38px; min-height: 0; padding: 0; border-radius: 10px; display: inline-grid; place-items: center;
  background: #17131a; color: #fff; border: 0; cursor: pointer; box-shadow: none;
}
body.land .lt-helper-ask button:hover { background: #2b2229; color: #fff; transform: none; box-shadow: none; }
body.land .lt-helper-row.wait p { color: var(--text-3); }
body.land .lt-helper-open {
  position: fixed; right: 1.1rem; bottom: 1.1rem; z-index: 30; display: inline-flex; align-items: center; gap: .45rem;
  min-height: 44px; padding: 0 1.1rem; border-radius: 999px; font-size: .9rem; font-weight: 600;
  background: #17131a; color: #fff; border: 0; cursor: pointer;
  box-shadow: 0 1px 2px rgba(24, 24, 27, .08), 0 18px 40px -20px rgba(24, 24, 27, .5);
}
body.land .lt-helper-open[hidden] { display: none; }
body.land .lt-helper-open:hover { background: #2b2229; color: #fff; transform: none; }
@media (max-width: 560px) { body.land .lt-helper { right: .6rem; bottom: .6rem; width: calc(100vw - 1.2rem); } }
@media print { body.land .lt-helper, body.land .lt-helper-open { display: none; } }

/* ------------------------------------------------------------ Lovable's type
   The public site set the way lovable.dev's hero is (measured in Chrome,
   2026-09-28): the headline 48px at weight 600, -0.04em, line-height 1.1,
   near-black #1c1c1c; the line under it 18px regular in grey; the bar 15px.
   Lovable's face (Camera Plain) is licensed to Lovable and not ours to copy,
   so the face is the product's own sans, the one the Home draws its
   greeting in (--sans: Inter where installed, else the system's). Last in
   this sheet so it wins over the rules above. */
body.land { font-family: var(--sans); }
body.land h1, body.land h2, body.land .hero-title, body.land .band h2, body.land .closing h2 {
  font-family: var(--sans);
  font-weight: 600;
  letter-spacing: -0.04em;
}
body.land .hero .hero-title { font-size: clamp(2.25rem, 4.4vw, 3rem); line-height: 1.1; }
body.land .hero .hero-title { color: #1c1c1c; }
:root[data-theme="dark"] body.land .hero .hero-title { color: var(--on-wave); }
body.land .hero .hero-inner > .hero-sub { font-size: 1.125rem; line-height: 1.33; font-weight: 400; }
body.land .band h2 { font-size: clamp(1.9rem, 3.6vw, 2.5rem); line-height: 1.1; }
body.land .land-nav a { font-size: .9375rem; }
body.land .wordmark-name { font-family: var(--sans); font-weight: 700; font-size: 1.25rem; letter-spacing: -0.03em; }

/* ------------------------------------------------------------ the buttons
   One solid button and one quiet one, drawn the way lovable.dev's bar draws
   them (the user, 2026-10-02, of the pink pill with its lit edge and glow:
   "Very ugly … make this very good and beautiful"): near-black ink with
   white type, 10px corners, a hairline of shadow and nothing else. "Book a
   call", the request form's submit and the end's way in are the solid one;
   "Log in" and the calendar's own link are the quiet one, white with a
   hairline. Through --send-bg/--send-fg, the pair theme-dark.css flips
   together, so the type never loses its ground. */
body.land .book-call, body.land .lt-pink, body.land .lt-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  min-height: 38px; padding: .5rem 1rem; border-radius: 10px; border: 1px solid transparent;
  font-family: var(--sans); font-size: .875rem; font-weight: 600; line-height: 1.2; letter-spacing: -.005em;
  white-space: nowrap; text-decoration: none; cursor: pointer;
  color: var(--send-fg, #fff); background: var(--send-bg, #18181b); background-image: none;
  box-shadow: 0 1px 2px rgba(24, 24, 27, .14), inset 0 1px 0 rgba(255, 255, 255, .1);
  transition: background-color .15s ease, transform .15s ease;
}
body.land .book-call:hover, body.land .lt-pink:hover, body.land .lt-btn:hover {
  color: var(--send-fg, #fff); text-decoration: none; background: var(--send-bg-hover, #3f3f46); background-image: none;
}
body.land .book-call:active, body.land .lt-pink:active, body.land .lt-btn:active { transform: translateY(1px); }
body.land .book-call:focus-visible, body.land .lt-pink:focus-visible, body.land .lt-btn:focus-visible {
  outline: 2px solid var(--ink-500); outline-offset: 2px;
}
body.land .lt-btn-quiet {
  color: var(--text); background: var(--paper); border-color: var(--line-2);
  box-shadow: 0 1px 2px rgba(24, 24, 27, .05);
}
body.land .lt-btn-quiet:hover { color: var(--text); background: var(--canvas-2); }
body.land .lt-btn .ico { color: currentColor; }
/* "Log in" beside it: the same height and corners, so the pair reads as one
   control set rather than a pill next to a slab. */
body.land .land-bar .land-login { min-height: 38px; display: inline-flex; align-items: center;
  padding: .5rem .9rem; border-radius: 10px; font-size: .875rem; font-weight: 500; }
body.land:not(.market) .land-bar .land-login { background: var(--paper); border-color: var(--line-2); color: var(--text); }
body.land:not(.market) .land-bar .land-login:hover { background: var(--canvas-2); color: var(--text); }
@media (max-width: 380px) { body.land .land-bar .book-call, body.land .land-bar .land-login { padding: .45rem .7rem; } }

/* ------------------------------------------------------------ the fewest words
   How it works in four steps, the calendar (or the request form with only its
   two fields showing), three questions, and the end on the wave. */
body.land .lt-h2 { text-align: center; margin: 0 0 1.75rem; }
body.land .lt-steps { list-style: none; margin: 0 auto; padding: 0; max-width: 64rem; display: grid; gap: .9rem;
                      grid-template-columns: minmax(0, 1fr); }
@media (min-width: 560px) { body.land .lt-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 900px) { body.land .lt-steps { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
body.land .lt-steps > li { background: var(--paper); border: 1px solid var(--line); border-radius: 16px;
                           padding: 1.25rem 1.2rem 1.3rem; box-shadow: 0 1px 2px rgba(24, 24, 27, .04); }
body.land .lt-steps h3 { margin: .8rem 0 .25rem; font-size: 1.05rem; font-weight: 600; letter-spacing: -.02em; }
body.land .lt-steps p { margin: 0; color: var(--text-2); font-size: .95rem; line-height: 1.45; }

body.land .lt-access { max-width: 38rem; margin: 0 auto; display: block; }
body.land .lt-access .lt-h2 { margin-bottom: .6rem; }
body.land .lt-lede { text-align: center; color: var(--text-2); font-size: 1rem; line-height: 1.5; margin: 0 0 1.5rem; text-wrap: balance; }
body.land .lt-form { width: 100%; }
body.land .lt-form .lt-pink { width: 100%; min-height: 46px; font-size: .95rem; margin-top: .4rem; }
body.land .lt-more { margin: .2rem 0 .9rem; }
body.land .lt-more > summary { cursor: pointer; font-size: .9rem; font-weight: 600; color: var(--text-2);
                               list-style: none; display: inline-flex; align-items: center; gap: .35rem; }
body.land .lt-more > summary::-webkit-details-marker { display: none; }
body.land .lt-more > summary::before { content: "+"; font-size: 1.05rem; line-height: 1; }
body.land .lt-more[open] > summary::before { content: "−"; }
body.land .lt-more[open] > summary { margin-bottom: .75rem; }

/* ------------------------------------------------------------ the calendar
   The founder's Cal.com calendar where the request form was (landing.html
   #book, static/booking.js). Until Cal says it is drawn, and without script,
   the card holds a link to the same calendar on cal.com; once it is, the card
   steps back and Cal's own calendar is the surface. */
/* Wide enough for Cal's month view with its three columns, so the calendar
   never scrolls inside itself (the user, 2026-10-10: "fix the scroll thing,
   the scroll down and side"); the frame takes the width Cal asks for. */
body.land .lt-book .band-inner { max-width: 1200px; position: relative; }
body.land .lt-cal-frame iframe { width: 100% !important; max-width: 100%; }
body.land .lt-anchor { display: block; height: 0; }
body.land .lt-book .lt-lede { margin-bottom: 1.6rem; }
body.land .lt-cal {
  position: relative; min-height: 300px; border-radius: 16px;
  background: var(--paper); border: 1px solid var(--line);
  box-shadow: 0 1px 2px rgba(24, 24, 27, .04), 0 12px 32px -18px rgba(24, 24, 27, .18);
  transition: background-color .3s ease, border-color .3s ease, box-shadow .3s ease;
}
body.land .lt-cal-wait {
  position: absolute; inset: 0; z-index: 1; display: grid; place-content: center; justify-items: center;
  gap: .8rem; padding: 2rem 1.25rem; text-align: center; border-radius: inherit; background: var(--paper);
}
body.land .lt-cal-wait p { margin: 0; font-size: 1rem; color: var(--text); }
body.land .lt-cal-frame { position: relative; width: 100%; min-height: 300px; }
body.land .lt-cal-frame iframe { display: block; }
body.land .lt-cal.is-ready { background: transparent; border-color: transparent; box-shadow: none; }
body.land .lt-cal.is-ready .lt-cal-wait { display: none; }

/* ------------------------------------------------------------ the end, on the wave
   The wave rises out of the page and settles back into it (the user,
   2026-10-02: "Make it smooth"): a mask brings it in over the first few rem
   and lets it go over the last few, so neither edge is a line, and the foot
   sits on the page's own ground below it. The heading stays where the wave
   is still its pale top. */
body.land .lt-end { position: relative; isolation: isolate; overflow: hidden; text-align: center;
                    padding: clamp(6.5rem, 14vw, 10.5rem) 1.25rem clamp(7rem, 15vw, 11rem); margin-top: 0; }
body.land .lt-end-wash {
  position: absolute; inset: 0; z-index: -1; background-color: var(--ov-wave-foot);
  background-image: var(--ov-wave); background-size: 100% 100%; background-repeat: no-repeat;
  --rise: clamp(5rem, 12vw, 9rem); --settle: clamp(5rem, 11vw, 8rem);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, rgba(0, 0, 0, .5) calc(var(--rise) * .5), #000 var(--rise),
                                      #000 calc(100% - var(--settle)), rgba(0, 0, 0, .5) calc(100% - var(--settle) * .5), transparent 100%);
  mask-image: linear-gradient(180deg, transparent 0, rgba(0, 0, 0, .5) calc(var(--rise) * .5), #000 var(--rise),
                              #000 calc(100% - var(--settle)), rgba(0, 0, 0, .5) calc(100% - var(--settle) * .5), transparent 100%);
}
body.land .lt-end-inner { max-width: 40rem; margin: 0 auto; }
body.land .lt-end h2 { margin: 0; font-size: clamp(2rem, 4.4vw, 3rem); line-height: 1.1; color: #1c1c1c; }
:root[data-theme="dark"] body.land .lt-end h2 { color: var(--on-wave); }
body.land .lt-end-actions { display: flex; justify-content: center; align-items: center; gap: .7rem; flex-wrap: wrap;
                            margin-top: 1.6rem; }
body.land .lt-end .book-call, body.land .lt-end .lt-btn { min-height: 46px; padding: .75rem 1.4rem; font-size: .95rem; }

/* ------------------------------------------------------------ the foot
   Complete, and the width of the page (the user, 2026-10-02: "the footer
   make it complete and full"): what OVision is in one line, every page of
   the site in three columns, then the research-use boundary and the
   copyright on a hairline. On the page's own ground, not a card. */
body.land .lt-foot {
  width: auto; margin: 0; border-radius: 0; background: transparent;
  padding: clamp(2.5rem, 5vw, 3.75rem) 1.5rem 2rem;
}
/* Under a page of prose (pricing, privacy, terms, the writing) there is no
   wave to settle into, so a hairline sets the foot off instead. */
body.land.market .lt-foot { border-top: 1px solid var(--line); }
body.land .lt-foot-top {
  max-width: 72rem; margin: 0 auto; display: grid; gap: 2.25rem 3rem; grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 820px) { body.land .lt-foot-top { grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); } }
body.land .lt-foot-mark { display: inline-flex; color: var(--text); text-decoration: none; }
body.land .lt-foot-mark:hover { text-decoration: none; }
body.land .lt-foot-brand p { margin: .8rem 0 0; max-width: 28ch; font-size: .92rem; line-height: 1.5; color: var(--text-2); }
body.land .lt-foot-cols { display: grid; gap: 1.75rem 1.5rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 560px) { body.land .lt-foot-cols { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
body.land .lt-foot-col h2 {
  margin: 0 0 .85rem; font-size: .8125rem; font-weight: 600; letter-spacing: 0; line-height: 1.3; color: var(--text);
}
body.land .lt-foot-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .6rem; }
body.land .lt-foot-col a { font-size: .9rem; line-height: 1.4; color: var(--text-2); text-decoration: none; }
body.land .lt-foot-col a:hover { color: var(--text); text-decoration: none; }
body.land .lt-foot-bottom {
  max-width: 72rem; margin: 2.5rem auto 0; padding-top: 1.25rem; border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .75rem 2rem;
}
body.land .lt-foot .foot-ruo { margin: 0; padding: 0; border: 0; font-size: .8125rem; line-height: 1.5; color: var(--text-2); }
body.land .lt-foot-copy { margin: 0; font-size: .8125rem; color: var(--text-2); }

/* Closer sections: the page is short now, so the space between its parts is
   too. The form reads at a form's width, not the page's. */
body.land .band.ea-band { padding: clamp(2.75rem, 5.5vw, 4.5rem) 1.25rem; }
body.land .ea-band .band-inner.lt-access { max-width: 38rem; }
body.land .lt-form .ea-note { margin: 0 0 1.1rem; }
