/* =======================================================================
   TerraLandingPage.css
   -----------------------------------------------------------------------
   Styles for the PUBLIC MARKETING LANDING PAGE only (TerraLanding.html,
   served at "/").

   ⚠ NOT to be confused with styles/TerraLanding.css, which despite its name
   is the SHARED stylesheet for the other public pages (TerraLogin,
   TerraPricing, TerraCheckout, TerraDisclaimer, TerraSubscribed,
   TerraResetPassword, TerraAdmin).

   THE NAVBAR IS NO LONGER DEFINED HERE (changed 2026-08-11). Both this file and
   TerraLanding.css used to define .nav, .nav-brand, .btn, .btn-icon and
   .nav-t-icon — same names, two rule sets, two palettes. That is how the T mark
   ended up needing OPPOSITE recolouring rules in the two files, and why the
   duplicate .nav-brand here carried a comment asking the reader to hand-sync it.

   The bar now lives once in styles/TerraNav.css, which this page links FIRST.
   All that remains here is the sticky header shell and this page's --nav-*
   token values. To change how the nav looks on this page, change a TOKEN.

   Layout/typography come from the reference landing-page build (see
   terra-landing-notes.md). Two things were changed on the way in:
     1. The reference build was light-only. This version is THEME-AWARE:
        every colour is a CSS custom property, with a dark set as the default
        (matching the rest of the Terra site) and a light set applied when
        the shared theme preference says "light". See "Palette" below.
     2. The reference build pulled Source Serif 4 + Inter from the Google
        Fonts CDN. Both are now self-hosted from vendor/fonts/, matching the
        rest of Terra (no third-party requests from a visitor's browser).
   ======================================================================= */

/* =======================================================================
   Fonts — self-hosted, no CDN
   -----------------------------------------------------------------------
   Both files are VARIABLE fonts: a single file covers the whole weight
   range, which is why "font-weight" below is a RANGE (400 700) rather than
   a single number. The browser interpolates any weight in between, so 400,
   500, 600 and 700 all come from the one download.

   font-display: swap = show the fallback font immediately and swap in the
   real one when it arrives, rather than leaving invisible text.
   ======================================================================= */

/* Brand display face, used for the TERRA wordmark ONLY. Same file and same
   declaration as styles/TerraLanding.css, so the wordmark is pixel-identical
   to the one on the login/pricing/admin pages. */
@font-face {
  font-family: 'Days One';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../vendor/fonts/daysone-latin.woff2') format('woff2');
}

/* Display/heading serif. */
@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('../vendor/fonts/sourceserif4-var-latin.woff2') format('woff2');
}

/* Body/UI sans. Terra already vendors two STATIC Inter weights (500 and 700)
   for the app; this page needs 400 and 600 as well, so it uses the variable
   file, which covers the lot in one request. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('../vendor/fonts/inter-var-latin.woff2') format('woff2');
}

/* =======================================================================
   Palette
   -----------------------------------------------------------------------
   DARK IS THE DEFAULT, because that is what the rest of Terra does: the app
   ships dark, and TerraLanding.html's head script only sets
   data-theme="light" when the shared 'terraThemePreset' says otherwise. So
   :root below is the dark set, and :root[data-theme='light'] overrides it
   with the reference build's original "paper" palette.

   What each token is for:
     --paper         page background
     --alt / --alt2  the alternating band backgrounds (.section-alt, cards)
     --ink           headings and high-emphasis text
     --ink2          body copy
     --muted         captions, small print
     --line/--line2  hairline borders (line2 is the stronger of the two)
     --accent        FILLED accent surfaces: primary button, step numerals,
                     tick marks, eyebrow text
     --accent-hover  the filled accent on hover
     --accent-text   accent used AS TEXT (links, .kw keyword emphasis) — a
                     separate token because text needs to contrast with the
                     PAGE, while a filled button needs to contrast with the
                     text sitting on it. On dark these pull in opposite
                     directions, so one token cannot serve both.
     --on-accent     text colour that sits ON a filled accent surface
     --tint          a faint accent wash (highlighted tile, badge)
     --band          the deep "inverted" bands (positioning strip, final CTA)
     --band-ink      text on those bands
   ======================================================================= */
/* ---------------------------------------------------------------------------
   THE PALETTE LIVES IN styles/TerraTheme.css.

   Every token this file used to define (--paper, --alt, --alt2, --card, --ink,
   --ink2, --muted, --line, --line2, --accent*, --on-accent, --tint,
   --wordmark, --band*, --header-bg, --footer-*, --shadow*, --serif, --sans) is
   still available - it is now aliased there onto a shared role token. This
   sheet's rules are unchanged.

   That file is also where the note about --wordmark matching --brand-dark now
   belongs: both are the same role token, so they cannot drift apart any more.
   --------------------------------------------------------------------------- */

/* =======================================================================
   Base
   ======================================================================= */
* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* ⚠️ PROXIMITY, NOT MANDATORY, and the difference matters.
     `mandatory` forces the scroll to land on a snap point ALWAYS. Several
     sections on this page are taller than the viewport (the six-tile grids,
     the FAQ), and with mandatory snapping the browser drags you past their
     middle to the next boundary - so the content in between becomes genuinely
     unreachable by scrolling. `proximity` snaps only when you finish near a
     boundary anyway, which gives the same settled feel on the short sections
     and simply gets out of the way on the tall ones.

     Paired with `scroll-padding-top` so a snapped section starts BELOW the
     sticky header rather than underneath it. */
  scroll-snap-type: y proximity;
  /* The sticky header's height. A literal because the header has no fixed
     height of its own - it is sized by its padding and its tallest button - so
     there is no token to read. 72px matches it at the default font size; it
     only decides where a snapped section starts, so being a few pixels out
     costs nothing. */
  scroll-padding-top: 72px;
}

/* Someone who has asked their system for less motion should not be pulled
   around by snapping OR by smooth scrolling. Both are comfort features; the
   page loses nothing essential without them. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
    scroll-snap-type: none;
  }
}

body {
  margin: 0;
  font-family: var(--sans);
  color: var(--ink);
  background: var(--paper);
  line-height: 1.6;
  font-size: 17px;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3 {
  font-family: var(--serif);
  font-weight: 600;
  line-height: 1.12;
  margin: 0;
  letter-spacing: -0.01em;
}

p {
  margin: 0 0 1rem;
  color: var(--ink2);
}

a {
  color: var(--accent-text);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

/* Visible keyboard focus. The reference build relied on the browser default,
   which disappears against the dark palette; this makes it explicit and
   consistent everywhere. */
a:focus-visible,
summary:focus-visible,
.btn:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 3px;
  border-radius: 6px;
}

/* Centred content column, used by every section. */
.wrap {
  max-width: 1140px;
  margin: 0 auto;
  padding: 0 24px;
}

/* Small uppercase label that sits above a heading. */
.eyebrow {
  font-family: var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 12px;
  font-weight: 600;
  color: var(--accent);
  margin-bottom: 14px;
}

/* Keyword emphasis — used to make the SEO phrases stand out in body copy. */
.kw {
  color: var(--accent-text);
}

/* ---------------------------------------------------------------------------
   FULL-HEIGHT SECTIONS (2026-08-15)
   ---------------------------------------------------------------------------
   Each section fills the screen, so scrolling moves you from one idea to the
   next rather than through a continuous wall of text.

   ⚠️ min-height, NEVER height. Several sections here carry a six-tile grid or
   the whole FAQ, and on a laptop in landscape - or any phone - that content is
   taller than the viewport. A fixed `height: 100vh` would clip it with no way
   to reach it. `min-height` gives the short sections a full screen and lets the
   tall ones grow past it, which is the only version of this that is safe.

   ⚠️ 100svh, not 100vh. On mobile, `vh` is measured against the viewport with
   the browser's address bar HIDDEN, so a 100vh section is taller than what you
   can actually see and every section starts slightly clipped. `svh` is the
   SMALL viewport height - the visible area with the bar showing - which is the
   conservative choice. The `vh` line before it is the fallback for browsers
   without svh; they get the old behaviour rather than nothing. */
section {
  padding: 76px 0;
  min-height: 100vh;
  min-height: 100svh;
  /* Centre the content vertically within the screen it now occupies. Without
     this a short section pins its heading to the top and leaves a large void
     underneath, which reads as a rendering fault rather than as space. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Snap point, so a scroll settles on a section boundary rather than halfway
     between two. See the scroll-snap note on `html` below for why this is
     PROXIMITY and not `mandatory`. */
  scroll-snap-align: start;
}

/* A flex container makes each child a flex ITEM, and `align-items` defaults to
   `stretch` - which would stretch .wrap to the full width and undo its
   `margin: 0 auto` centring. Restated here so the change above cannot silently
   move every section's content to the left. */
section > .wrap {
  width: 100%;
}

/* Alternating band background, so consecutive sections separate visually.
   ⚠️ The shading ALTERNATES, and nothing enforces it - it is one class applied
   by hand, section by section. Two adjacent sections had both ended up plain,
   which is invisible until you look for it. If you add or remove a section,
   check the whole run: every other <section> from the #proof band down carries
   .section-alt. Deliberately not automated with :nth-of-type, because the hero
   and the final call-to-action are sections too and are not part of the
   rhythm. */
.section-alt {
  background: var(--alt);
}

.lead {
  font-size: 19px;
  color: var(--ink2);
  max-width: 56ch;
}

.center {
  text-align: center;
}

.center .lead {
  margin-left: auto;
  margin-right: auto;
}

/* =======================================================================
   Buttons
   ======================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--sans);
  font-weight: 600;
  font-size: 16px;
  padding: 13px 22px;
  border-radius: 10px;
  border: 1.5px solid transparent;
  cursor: pointer;
  transition: 0.15s;
  text-decoration: none;
}

.btn-primary {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}

.btn-primary:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
  color: var(--on-accent);
  text-decoration: none;
}

.btn-secondary {
  background: transparent;
  color: var(--ink);
  border-color: var(--line2);
}

.btn-secondary:hover {
  border-color: var(--accent);
  color: var(--accent-text);
  text-decoration: none;
  background: var(--tint);
}

.btn-row {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  align-items: center;
}

/* =======================================================================
   Header (sticky)
   ======================================================================= */
/* The sticky header shell. The BAR ITSELF (.nav, .nav-brand, .nav-links, the
   .btn family, .btn-icon, .nav-icon, .nav-t-icon and .badge) is defined once in
   styles/TerraNav.css, which this page links first. Only the sticky wrapper and
   this page's --nav-* token values live here.

   This page used to carry its own copy of all of that, with a comment on
   .nav-brand telling the reader to hand-sync it with TerraLanding.css. That is
   how the T mark ended up needing opposite recolouring rules in the two files. */
header.site {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--header-bg);
  backdrop-filter: saturate(180%) blur(10px);
  border-bottom: 1px solid var(--line);
}

/* This page's palette, mapped onto the nav's token contract.

   The T mark is the interesting one. Its button (.btn-solid) uses --accent,
   which on THIS page is a LIGHT green (#5aa87d) in the dark theme and a DARK
   green (#2f6a4f) in the light theme - the reverse of every other page. The
   .btn-solid rule in TerraNav.css always whitens the mark, which is right for
   the light theme; the dark theme needs it left dark, which is the one override
   below. Expressing it as a token means the rule is not duplicated, only the
   value. */
.nav {
  --nav-bg: transparent; /* header.site already paints the background */
  --nav-border: transparent; /* and the border */
  --nav-brand: var(--wordmark);
  --nav-link: var(--ink2);
  --nav-link-hover: var(--accent-text);
  --nav-accent: var(--accent);
  --nav-accent-hover: var(--accent-hover);
  --nav-on-accent: var(--on-accent);
  --nav-ghost: var(--ink);
  /* The "Private beta" pill keeps this page's tinted look. Its border cannot
     fall back to --nav-border here, because that is transparent above. */
  --nav-badge-text: var(--accent-text);
  --nav-badge-bg: var(--tint);
  --nav-badge-border: var(--line2);
  height: 66px;
}

/* This page's own .btn (below) is larger than the shared nav button and wins on
   load order, so the nav's compact sizing has to be restated. Kept here rather
   than in TerraNav.css because it is this page's button system that differs. */
.nav .btn {
  padding: 9px 16px;
  font-size: 15px;
}

.nav .btn.btn-icon {
  padding: 9px;
  line-height: 0;
}

/* .nav-links a sets a link colour; a button inside the nav must keep its own. */
.nav-links a.btn-primary,
.nav-links a.btn-primary:hover,
.nav-links a.btn-solid,
.nav-links a.btn-solid:hover {
  color: var(--on-accent);
}

/* Dark theme: the accent button is LIGHT green with near-black text, so the DARK
   artwork belongs on it - the opposite of TerraNav's always-white rule, which is
   right everywhere the accent button is dark.

   ⚠️ THESE PICK ARTWORK, THEY NO LONGER FILTER IT. The mark used to be one dark
   PNG recoloured by CSS, so this page inverted or un-inverted it; it is now
   three pieces of real artwork named by --nav-t-art-* in styles/TerraNav.css,
   and a page says WHICH rather than how to recolour. A filter left here would
   silently recolour whichever artwork the token had chosen. */
.nav .btn-solid .nav-t-icon,
.nav .btn-primary .nav-t-icon {
  background-image: var(--nav-t-art-dark);
}

/* Light theme: the accent button is DARK green with white text, so the white
   artwork goes back on. */
:root[data-theme='light'] .nav .btn-solid .nav-t-icon,
:root[data-theme='light'] .nav .btn-primary .nav-t-icon {
  background-image: var(--nav-t-art-white);
}

/* =======================================================================
   Hero
   ======================================================================= */
.hero {
  padding: 66px 0 40px;
}

.hero-grid {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: 56px;
  align-items: center;
}

h1 {
  font-size: 50px;
}

/* The lighter second line under the H1 — carries the keyword phrase. */
h1 .h1-sub {
  display: block;
  font-size: 23px;
  font-weight: 400;
  color: var(--muted);
  margin-top: 14px;
  letter-spacing: 0;
}

.hero .lead {
  margin-top: 22px;
}

.hero .btn-row {
  margin-top: 28px;
}

.beta-note {
  margin-top: 18px;
  font-size: 14.5px;
  color: var(--muted);
}

.beta-note a {
  font-weight: 600;
}

/* Frame for a real screenshot once one exists. */
.shot {
  border-radius: 14px;
  overflow: hidden;
  box-shadow: var(--shadow);
  border: 1px solid var(--line);
  background: var(--card);
}

.shot img,
.shot svg {
  display: block;
  width: 100%;
  height: auto;
}

/* -------- Holding-text placeholders --------
   Stand-ins where the real screenshots will go, so the page is deployable
   before they exist. Replace the markup with a real <img> (descriptive alt,
   width/height set, loading="lazy" below the fold) — see the notes. */
.ph {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  background: var(--alt);
  color: var(--muted);
  border: 1.5px dashed var(--line2);
  border-radius: 14px;
  padding: 24px;
}

.ph .lbl {
  font-family: var(--serif);
  font-size: 20px;
  color: var(--ink2);
}

.ph .sub {
  font-size: 12.5px;
  margin-top: 6px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.ph-hero {
  aspect-ratio: 3 / 2;
  box-shadow: var(--shadow);
}

/* Placeholder sitting inside a card, so it loses its own rounding/border. */
.ph-card {
  aspect-ratio: 7 / 4;
  border: 0;
  border-radius: 0;
  background: var(--alt2);
  border-bottom: 1px dashed var(--line2);
}

/* =======================================================================
   Positioning strip — the inverted one-line band under the hero
   ======================================================================= */
.strip {
  background: var(--band);
  color: var(--band-ink);
  padding: 30px 0;
}

.strip .wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
  text-align: center;
}

.strip p {
  margin: 0;
  color: var(--band-ink);
  font-family: var(--serif);
  font-size: 22px;
}

/* Fixed light green: this band is dark in BOTH themes, so the keyword colour
   is pinned rather than taken from --accent-text. */
.strip .kw {
  color: #9fd3ba;
}

/* =======================================================================
   Generic two-column section
   ======================================================================= */
.two {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 48px;
  align-items: center;
}

.mb-head {
  margin-bottom: 14px;
  font-size: 32px;
}

/* Two size variants of the section heading. The reference build set these with
   inline style="font-size:..." attributes; they live here instead so all the
   type sizing stays in one file. */
.mb-head.is-large {
  font-size: 34px;
}

.mb-head.is-small {
  font-size: 30px;
}

/* =======================================================================
   Social proof band (currently a placeholder quote — swap for a real
   beta-tester testimonial or professional credentials)
   ======================================================================= */
.proof {
  padding: 44px 0;
}

.proof-quote {
  font-family: var(--serif);
  font-size: 22px;
  color: var(--ink);
  max-width: 56ch;
  margin: 0 auto 8px;
}

.proof-note {
  font-size: 14px;
  color: var(--muted);
  margin: 0;
}

/* =======================================================================
   Screenshots section (app vs Excel)
   ======================================================================= */
.shots {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 28px;
  margin-top: 38px;
}

.shotcard {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.shotcard .cap {
  padding: 16px 20px 20px;
}

.shotcard .cap b {
  font-family: var(--serif);
  font-size: 18px;
  color: var(--ink);
}

.shotcard .cap p {
  margin: 6px 0 0;
  font-size: 14.5px;
  color: var(--muted);
}

.tagpill {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-text);
  background: var(--tint);
  border-radius: 6px;
  padding: 3px 9px;
  margin-bottom: 8px;
}

/* =======================================================================
   "How it works" steps
   ======================================================================= */
.steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 26px;
  margin-top: 40px;
}

.step {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 26px;
  box-shadow: var(--shadow-sm);
}

.step .n {
  font-family: var(--serif);
  font-size: 15px;
  font-weight: 700;
  color: var(--on-accent);
  background: var(--accent);
  width: 34px;
  height: 34px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 16px;
}

.step h3 {
  font-size: 19px;
  margin-bottom: 8px;
}

.step p {
  font-size: 15px;
  margin: 0;
}

/* =======================================================================
   Video block
   ======================================================================= */
.video-frame {
  margin-top: 34px;
  border-radius: 16px;
  overflow: hidden;
  box-shadow: var(--shadow);
  border: 1px solid var(--line);
  background: #0c1512;
  position: relative;
}

.video-frame video {
  display: block;
  width: 100%;
  height: auto;
}

/* The facade the visitor sees before the video is loaded - a poster image (when
   one is wired up) with a play button drawn over it. It is a real <button>, so
   the first block of declarations is a button reset: without it the browser's
   own chrome, font and 'auto' width would fight everything below.

   Deliberately dark in both themes - with no poster it stands in for a video
   frame, and with one it is the scrim the white label sits on. */
.video-poster {
  /* button reset */
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  border: 0;
  font: inherit;
  /* Not clickable until scripts/landingVideo.js finds a data-src and marks it
     [data-ready] - a placeholder must never look like it does something. */
  cursor: default;
  /* Anchors the absolutely-positioned poster image and scrim below. */
  position: relative;

  aspect-ratio: 16 / 9;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  background: linear-gradient(135deg, #1c2f27, #0e1a15);
  color: #dfe7e2;
  text-align: center;
  padding: 24px;
}

.play {
  width: 74px;
  height: 74px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.14);
  border: 2px solid rgba(255, 255, 255, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 16px;
}

/* The play triangle, drawn with CSS borders (no image needed). */
.play::after {
  content: '';
  border-left: 22px solid #fff;
  border-top: 14px solid transparent;
  border-bottom: 14px solid transparent;
  margin-left: 6px;
}

/* Caption lines inside the video poster. They reuse the .lbl/.sub names from
   the screenshot placeholders, but the poster is dark in both themes so the
   colours are pinned rather than themed. */
.video-poster .lbl {
  font-family: var(--serif);
  font-size: 20px;
  color: #dfe7e2;
}

.video-poster .sub {
  font-size: 13px;
  color: #9fb3a8;
  margin-top: 6px;
}

/* --- The facade once a recording IS wired up ------------------------------- */

/* The poster still, inserted by scripts/landingVideo.js as a lazily-loaded
   <img> rather than the video's own poster attribute - a video poster is
   fetched eagerly, which is the one cost a click-to-load video still has to
   avoid. Stacking is set explicitly on all three layers because they overlap:
   image (0) under scrim (1) under label (2). */
.video-poster-img {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Darkens the poster so the white play button and label stay legible whatever
   the still happens to show. */
.video-poster[data-ready]::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: rgba(12, 21, 18, 0.45);
}

.video-poster > .play,
.video-poster > .lbl,
.video-poster > .sub {
  position: relative;
  z-index: 2;
}

/* Interactive affordances are scoped to [data-ready] - see the cursor note on
   .video-poster above. */
.video-poster[data-ready] {
  cursor: pointer;
}

.play {
  transition:
    background 0.18s ease,
    transform 0.18s ease;
}

.video-poster[data-ready]:hover .play,
.video-poster[data-ready]:focus-visible .play {
  background: rgba(255, 255, 255, 0.26);
  transform: scale(1.06);
}

/* Matches the page's focus convention (see the a/summary/.btn rule near the top)
   but drawn INSIDE the frame, which clips its own overflow for the rounded
   corners and would otherwise cut an outset ring in half. */
.video-poster:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: -4px;
}

@media (prefers-reduced-motion: reduce) {
  .play {
    transition: none;
  }
  .video-poster[data-ready]:hover .play,
  .video-poster[data-ready]:focus-visible .play {
    transform: none;
  }
}

/* =======================================================================
   Tile grids (who it's for, benefits, where Terra fits)
   ======================================================================= */
.grid3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
  margin-top: 38px;
}

/* Two across, for a list of FOUR.

   ⚠️ FOUR TILES IN .grid3 IS THREE AND AN ORPHAN, which reads as a tile missing
   rather than as a deliberate list. This is the "Who it's for" section; "Where
   Terra fits" keeps .grid3 because it really is three things. */
.grid2 {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
  margin-top: 38px;
}

/* Four across, for the EIGHT tiles of "What you get" - two full rows and no
   orphan. Below 900px it drops to two across (four rows of two, still no
   orphan) rather than straight to one, because eight stacked tiles on a phone
   is a long scroll past the same-shaped card eight times. */
.grid4 {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
  margin-top: 38px;
}

.tile {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 22px;
  box-shadow: var(--shadow-sm);
}

.tile h3 {
  font-size: 17px;
  margin-bottom: 6px;
}

.tile p {
  font-size: 14.5px;
  margin: 0;
  color: var(--muted);
}

.tile .ic {
  color: var(--accent);
  font-weight: 700;
  font-family: var(--serif);
  font-size: 20px;
  margin-bottom: 8px;
}

/* The highlighted "Terra" tile in the middle of the positioning trio. */
.tile.hl {
  border: 1.5px solid var(--accent);
  background: var(--tint);
  box-shadow: 0 10px 30px rgba(47, 106, 79, 0.12);
}

.tile-label {
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 700;
  margin-bottom: 6px;
}

/* =======================================================================
   Transparency / feature split (copy + tick list on the left, quote right)
   ======================================================================= */
.feature {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 48px;
  align-items: center;
}

.feature ul {
  list-style: none;
  padding: 0;
  margin: 16px 0 0;
}

.feature li {
  position: relative;
  padding-left: 28px;
  margin-bottom: 12px;
  color: var(--ink2);
}

.feature li::before {
  content: '✓';
  position: absolute;
  left: 0;
  top: 0;
  color: var(--accent);
  font-weight: 700;
}

/* The pull-quote card — a rounded version of the inverted .strip band. */
.quote-card {
  background: var(--band);
  color: var(--band-ink);
  border-radius: 16px;
  padding: 40px 30px;
}

.quote-card p {
  margin: 0;
  color: var(--band-ink);
  font-family: var(--serif);
  font-size: 24px;
  line-height: 1.35;
}

.quote-card .kw {
  color: #9fd3ba;
}

.quote-note {
  margin-top: 20px;
  font-size: 15px;
  color: var(--muted);
}

/* =======================================================================
   Insights card
   ======================================================================= */
.insight {
  display: flex;
  gap: 22px;
  align-items: center;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 24px 26px;
  box-shadow: var(--shadow-sm);
  margin-top: 26px;
  flex-wrap: wrap;
  color: inherit;
}

.insight:hover {
  text-decoration: none;
  border-color: var(--accent);
}

.insight .tag {
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 600;
}

.insight h3 {
  font-size: 20px;
  margin: 4px 0 6px;
  color: var(--ink);
}

.insight p {
  margin: 0;
  font-size: 15px;
  color: var(--muted);
}

.insight .go {
  margin-left: auto;
  white-space: nowrap;
  font-weight: 600;
  color: var(--accent-text);
}

/* A small evidence / "read the working" link under a claim. Quieter than the
   lead it follows - its job is to be available to the sceptical reader, not to
   compete with the claim itself. Uses --accent-text (the link green) rather
   than --accent, so it matches every other inline link on the page. */
.mb-evidence {
  margin: 0.6em 0 0;
  font-size: 0.95rem;
}
.mb-evidence a {
  color: var(--accent-text);
  font-weight: 600;
  text-decoration: none;
}
.mb-evidence a:hover {
  text-decoration: underline;
}

/* =======================================================================
   FAQ accordion — native <details>/<summary>, works without JavaScript
   ======================================================================= */
/* The question list keeps its 820px cap for a comfortable reading measure, but
   is left-flush (margin-right auto, NOT auto both sides) so it starts on the same
   left edge as the "Questions" eyebrow and the heading above it. It used to be
   centred in the 1140px wrap, which left the heading and the questions on two
   different left edges once the heading stopped being centred too. */
.faq {
  max-width: 820px;
  margin: 34px auto 0 0;
}

.faq details {
  border-bottom: 1px solid var(--line);
  padding: 2px 0;
}

/* Question rows. Tightened 2026-08-15: .faq details contributed 6px top and
   bottom AND every summary another 18px, so each closed question stood nearly
   50px tall and a list of eight read as eight separate blocks rather than as
   one list. The tap target stays comfortably above the 44px accessibility
   guideline - 12px + 12px on a 19px serif line is ~48px. */
.faq summary {
  cursor: pointer;
  list-style: none;
  padding: 12px 4px;
  font-family: var(--serif);
  font-size: 19px;
  font-weight: 600;
  color: var(--ink);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}

/* Hide the browser's default disclosure triangle (Safari/Chrome). */
.faq summary::-webkit-details-marker {
  display: none;
}

/* Our own "+" indicator, which rotates 45° into an "×" when open. */
.faq summary::after {
  content: '+';
  color: var(--accent);
  font-size: 24px;
  font-weight: 400;
  transition: 0.2s;
}

.faq details[open] summary::after {
  transform: rotate(45deg);
}

.faq .a {
  /* Sits directly under its own question, so it needs less air above it than
     between two questions - the padding here is the gap before the NEXT one. */
  padding: 0 4px 14px;
  color: var(--ink2);
  font-size: 16px;
}

/* =======================================================================
   Final call to action
   ======================================================================= */
.final {
  background: var(--band);
  color: var(--band-ink);
  text-align: center;
}

.final h2 {
  color: #ffffff;
  font-size: 34px;
  max-width: 20ch;
  margin: 0 auto;
}

.final p {
  color: #c4cfc8;
}

.final-note {
  margin-top: 16px;
}

.final-actions {
  justify-content: center;
  margin-top: 24px;
}

/* This band is dark in both themes, so the outline button is pinned to white
   rather than following --ink. */
.final .btn-secondary {
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.35);
}

.final .btn-secondary:hover {
  background: rgba(255, 255, 255, 0.08);
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.6);
}

/* =======================================================================
   Footer
   ======================================================================= */
footer.site {
  background: var(--footer-bg);
  color: var(--footer-ink);
  padding: 52px 0 34px;
  font-size: 14.5px;
}

footer.site .cols {
  display: flex;
  justify-content: space-between;
  gap: 30px;
  flex-wrap: wrap;
}

/* The footer sits on a near-black band in BOTH themes, so the wordmark colour
   is pinned to the light green rather than following --wordmark (which goes
   near-black in the light theme and would vanish here). */
.footer-brand {
  font-size: 1.45rem;
  color: #9ad3b4;
}

footer.site a {
  color: #cdd6d0;
}

footer.site .fnav {
  display: flex;
  gap: 22px;
  flex-wrap: wrap;
}

.footer-blurb {
  margin: 10px 0 0;
  color: #8f9a93;
  max-width: 34ch;
  font-size: 14px;
}

/* The keyword line + trademark disclaimer at the very bottom. */
footer.site .seo-line {
  margin-top: 26px;
  padding-top: 20px;
  border-top: 1px solid var(--footer-line);
  color: #7f8a83;
  font-size: 13px;
}

/* =======================================================================
   Responsive — single column below 900px
   ======================================================================= */
@media (max-width: 900px) {
  .hero-grid,
  .two,
  .feature {
    grid-template-columns: 1fr;
    gap: 34px;
  }

  .shots {
    grid-template-columns: 1fr;
  }

  .steps,
  .grid3,
  .grid2 {
    grid-template-columns: 1fr;
  }

  .grid4 {
    grid-template-columns: repeat(2, 1fr);
  }

  h1 {
    font-size: 38px;
  }

  section {
    padding: 56px 0;
  }

  /* .hide-sm is handled once in TerraNav.css - deliberately not repeated here,
     or the bar would have two different breakpoints for the same behaviour. */
  .nav-links {
    gap: 16px;
  }
}

/* Respect a visitor's "reduce motion" setting. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  * {
    transition: none !important;
  }
}

/* Hero strapline - "Fast enough for initial pricing. Rigorous enough for
   investment committee."
   Sits between the h1 sub-line and the lead paragraph, so it is deliberately
   SUBORDINATE to the headline and slightly stronger than body copy: italic and
   muted so it reads as an aside, but at lead size so it is not skimmed past.
   max-width matches .lead so the hero column has one measure, not two. */
.hero-strap {
  margin: 14px 0 10px;
  font-size: 1.02rem;
  font-style: italic;
  line-height: 1.45;
  color: var(--muted, #5c6b64);
  max-width: 56ch;
}
/* The eight "What you get" tiles go to one column only at phone width. */
@media (max-width: 640px) {
  .grid4 {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 640px) {
  .hero-strap {
    font-size: 0.96rem;
  }
}

/* The "not trying to replace Excel" claim, under a section heading.
   Deliberately BETWEEN heading and lead in weight: larger and bolder than body
   copy so it is read, but not another heading - it is a sentence making an
   argument, not a label for what follows. Full text colour rather than the
   muted lead colour, because a line that disarms an objection should not look
   like an aside. */
.mb-claim {
  margin: 10px 0 6px;
  font-size: 1.15rem;
  font-weight: 650;
  line-height: 1.4;
  color: var(--ink);
  max-width: 62ch;
}
@media (max-width: 640px) {
  .mb-claim {
    font-size: 1.05rem;
  }
}

/* ---------------------------------------------------------------------------
   SCREENSHOT GALLERY (rebuilt 2026-08-14)
   ---------------------------------------------------------------------------
   A property-listing gallery: ONE fixed-size pane, every screenshot shown at
   the same size inside it, arrows to flick through, and a counter.

   The first attempt let each image keep its own proportions, and it looked a
   mess - the shots are 410x720 (portrait), 734x720 (square-ish) and 1280x612
   (wide), so the card jumped in height as you moved between them and the two
   panels never lined up.

   FIXED PANE + object-fit: contain fixes it. `contain` rather than `cover`
   because these are SCREENSHOTS: cropping one cuts off the numbers that are the
   whole reason to show it. Letterboxing onto a neutral ground is how every
   listing site handles photos of different shapes, and it reads as deliberate
   rather than broken.
   --------------------------------------------------------------------------- */
.shot-slider {
  position: relative;
  /* THE PANE, AND BOTH CARDS SHARE IT AGAIN.
     ⚠️ 16:10 IS THE SCREENSHOTS' OWN SHAPE, NOT A COMPROMISE. Every slide in
     both carousels is 1760x1100, so object-fit: contain has nothing to
     letterbox and the black behind never shows.

     It was 4:3 here with a 16:10 override on the app card, because the two
     sets were three different shapes and one ratio could only suit one of
     them. That override is gone. ⚠️ A REPLACEMENT SCREENSHOT OF ANY OTHER SIZE
     BRINGS THE BANDS BACK, and they are most visible as a change BETWEEN
     slides - a band that thickens halfway through a carousel reads as the
     content changing size. The export size and this ratio are one decision.

     The cards can still differ in height because .shots is a grid with
     align-items:start on each column, not a stretched row. */
  aspect-ratio: 16 / 10;
  width: 100%;
  overflow: hidden;
  border-radius: 14px 14px 0 0;
  border-bottom: 1px dashed var(--line2);
  /* Black, matching the slides. Anything lighter shows as a halo around a
     letterboxed screenshot. */
  background: #000;
}
.shot-slider:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* ⚠️ ABSOLUTELY POSITIONED, NOT `height: 100%` (fixed 2026-08-15).
   This was `display: grid; height: 100%` and the height silently did nothing.
   A percentage height needs a parent with a DEFINITE height, and .shot-slider's
   height comes from `aspect-ratio` - which the browser works out from the width
   during layout, too late to resolve a child's percentage against. So the track
   fell back to auto and took the height of its TALLEST slide.

   The damage was not obvious, because every slide still filled the track and
   `object-fit: contain` still letterboxed correctly - just inside a box 428px
   tall instead of the pane's 326px, with the pane clipping the overflow. The
   result: the wide vertical-cash-flow shot was centred in a box a third taller
   than the pane, so it appeared shoved to the BOTTOM with a black band above,
   and the portrait app shot was cropped rather than shown whole.

   `inset: 0` on the absolutely-positioned track takes its size from
   .shot-slider's padding box directly, which is a definite 326px by the time
   the slides are laid out. The arrows, dots and counter still paint above it:
   they are positioned too and come later in the markup.

   Measured before and after rather than eyeballed - a column of pixels through
   the pane gave 110px above the picture and 7px below, which is what put the
   428 figure beyond doubt. */
.shot-track {
  position: absolute;
  inset: 0;
}

.shot-track > [data-slide] {
  /* ⚠️ ABSOLUTE, NOT A GRID CELL WITH height: 100% (fixed 2026-08-15).
     These were stacked with `grid-area: 1 / 1` and sized with width/height
     100%. The WIDTH worked; the HEIGHT silently did not, and the computed
     value came back as the image's NATURAL height - 742.8px inside a 316px
     pane - which is what pushed the wide vertical-cash-flow shot to the bottom
     of the pane and cropped the portrait app shot instead of showing it whole.

     Two things had to be true for that, and both were:
       1. a percentage height needs a parent with a DEFINITE height, and the
          grid row was auto-sized, so 100% was cyclic and fell back to auto;
       2. `align-self: stretch` does NOT stretch a replaced element that has an
          intrinsic aspect ratio - it behaves as `start` - so nothing else
          pulled the image back to the pane's height.

     Neither is visible in the CSS: the rule reads as though it works. The
     computed height only shows up if you ask the DOM.

     `position: absolute; inset: 0` sizes each slide from the track's padding
     box directly, with no percentage to resolve and no grid alignment involved.
     It also gives the stacking that `grid-area: 1 / 1` was there for, so
     nothing is lost. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  opacity: 0;
  transition: opacity 0.2s ease;
  border: 0;
  border-radius: 0;
  /* ⚠️ EACH SLIDE IS OPAQUE IN ITS OWN RIGHT.
     `object-fit: contain` letterboxes: the <img> ELEMENT fills the pane, but
     the painted picture does not, and the margins are TRANSPARENT. So a
     narrower screenshot let the one underneath show through around its edges.
     A black ground on every slide means each one covers whatever is behind it,
     whatever shape it is - and black is the right colour for a screenshot of a
     dark app and for a white spreadsheet alike, reading as a frame either way. */
  background: #000;
}

/* NO JAVASCRIPT: show the first slide, so the card is never empty. */
.shot-track > [data-slide]:first-child {
  opacity: 1;
}

/* ⚠️ ONCE JAVASCRIPT IS RUNNING, .is-active alone decides.
   The rule above is why the previous slide was visible behind a smaller one:
   :first-child kept opacity 1 permanently, so slide 1 stayed lit underneath
   slides 2 and 3 and showed in their letterbox margins. The script adds
   .is-managed to the track, which hands control over completely - the no-JS
   fallback then stops applying instead of fighting the active slide. */
.shot-track.is-managed > [data-slide] {
  opacity: 0;
  /* 🔴 AN INVISIBLE SLIDE MUST NOT BE CLICKABLE. The slides are stacked
     absolutely and hidden with OPACITY, not display - which is what makes the
     crossfade possible and also leaves every one of them hit-testable. A click
     therefore landed on whichever slide was topmost in paint order, never the
     one on screen, so opening a screenshot always showed the same picture
     whatever the reader had scrolled to. Reported 2026-09-06.

     ⚠️ opacity:0 HIDES FROM THE EYE AND NOTHING ELSE. It does not remove an
     element from hit-testing, from the tab order, or from a screen reader -
     each needs saying separately, and the slider says the other two in
     landingSlider.js. */
  pointer-events: none;
}
.shot-track.is-managed > [data-slide].is-active {
  opacity: 1;
  pointer-events: auto;
}

/* Arrows - ALWAYS visible, not hover-only. On a touch screen there is no hover,
   and a control nobody can discover is not a control. */
.shot-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
  z-index: 2;
}
.shot-nav:hover {
  background: rgba(0, 0, 0, 0.72);
}
.shot-nav:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}
.shot-prev {
  left: 10px;
}
.shot-next {
  right: 10px;
}
.shot-nav[hidden] {
  display: none;
}

.shot-dots {
  position: absolute;
  bottom: 10px;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  gap: 7px;
  z-index: 2;
}
.shot-dots[hidden] {
  display: none;
}
.shot-dot {
  width: 9px;
  height: 9px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.4);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
  cursor: pointer;
}
.shot-dot.is-active {
  background: #fff;
}
.shot-dot:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* "2 / 3" - the listing-site convention. It says there is more to see before
   anyone touches a control. */
.shot-count {
  position: absolute;
  right: 10px;
  bottom: 8px;
  z-index: 2;
  padding: 2px 8px;
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 0.72rem;
  line-height: 1.6;
}
.shot-count[hidden] {
  display: none;
}

/* The HERO is a single image, not a gallery: keep it at its natural width so a
   410px-wide screenshot is never upscaled into softness. */
/* The hero screenshot and the line under it.

   ⚠️ THE <figure> IS THE GRID ITEM, NOT THE SHOT. .hero-grid is two columns, so
   a caption added as a sibling of .shot would be a THIRD item and would wrap
   onto its own row under the headline. The figure keeps the pair together in
   the right-hand column and carries the width cap that used to sit on .shot-hero
   alone, so the caption lines up with the image rather than with the column. */
/* ⚠️ 410px IS DECLARED ONCE, HERE, AND THAT IS THE POINT OF THE FIGURE. The cap
   used to sit on .shot-hero; with a caption underneath, two separate caps are
   two numbers that can drift, and the symptom of drift is a caption centred on
   something other than the image it describes. The figure carries the width and
   both the shot and the caption fill it, so they cannot disagree. */
.hero-figure {
  margin: 0 auto;
  max-width: 410px;
}

.hero-figure figcaption {
  margin-top: 8px;
  text-align: center;
  font-size: 12px;
  line-height: 1.4;
  color: var(--muted);
}

.shot-hero {
  /* Was 410px. The cap moved up to .hero-figure - see the note there. */
  max-width: 100%;
  margin-inline: auto;
}
.shot-hero img {
  display: block;
  width: 100%;
  height: auto;
}

/* =============================================================================
   SCREENSHOT LIGHTBOX
   -----------------------------------------------------------------------------
   The screenshots are shown at about 330px, which conveys the shape of the
   product and nothing a reader can actually read. Tapping one opens it over
   the page at full resolution. See scripts/client/landingLightbox.js.

   ⚠️ TWO STATES, AND THE SECOND IS WHY THIS WORKS ON A PHONE. By default the
   image is FITTED inside the viewport; .is-zoomed switches it to natural size
   inside a scrollable box so a reader can pan around a screenshot wider than
   their screen. Fitting alone would have made the feature useless on exactly
   the devices that need it most.
   ========================================================================== */
.shot-img {
  cursor: zoom-in;
}

/* Stop the page behind scrolling while the overlay is up. On a phone a
   scrolling background under a fixed overlay is disorienting and makes the
   close button feel broken. */
body.lightbox-open {
  overflow: hidden;
}

/* 🔴 THE hidden ATTRIBUTE MUST WIN, AND WITHOUT THIS RULE IT DOES NOT.
   `[hidden]` is `display: none` from the browser's own stylesheet, at the
   lowest specificity there is - so the `display: flex` below beats it and the
   element stays on screen after `overlay.hidden = true`.

   ⚠️ THE SYMPTOM WAS A TRAP, NOT A FLICKER. Closing the lightbox removed the
   image's src and set hidden; the overlay stayed visible showing an empty frame
   and its caption, and pressing the close button again did nothing at all,
   because hide() early-returns on `overlay.hidden`. There was no way out
   without reloading the page. Reported 2026-09-06.

   ⚠️ THE SAME TRAP IS WAITING FOR ANY ELEMENT GIVEN BOTH `display` AND
   `hidden`. Toggling `el.hidden` only works while nothing sets display on it. */
.lightbox[hidden] {
  display: none !important;
}

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  /* Near-black rather than the theme surface: the point is to take the page
     away, and a screenshot reads best against something neutral. */
  background: rgba(8, 10, 12, 0.92);
  overflow: auto;
  /* iOS momentum scrolling inside the zoomed state. */
  -webkit-overflow-scrolling: touch;
}

.lightbox-figure {
  margin: 0;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.lightbox-img {
  display: block;
  max-width: 100%;
  /* Leave room for the caption and the close button. */
  max-height: calc(100vh - 120px);
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 6px;
  background: #000;
  cursor: zoom-in;
}

/* Natural size, scrolled. `max-width: none` is the whole trick - the container
   is already overflow:auto, so the moment the image stops being constrained it
   becomes pannable. */
.lightbox.is-zoomed {
  align-items: flex-start;
  justify-content: flex-start;
}
.lightbox.is-zoomed .lightbox-img {
  max-width: none;
  max-height: none;
  cursor: zoom-out;
}

.lightbox-caption {
  max-width: 60ch;
  text-align: center;
  color: #e9edf0;
  font-size: 14px;
  line-height: 1.4;
}

.lightbox-close {
  position: fixed;
  top: 14px;
  right: 16px;
  width: 40px;
  height: 40px;
  /* A comfortable target on a phone, and above the image at every zoom. */
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 26px;
  line-height: 1;
  color: #e9edf0;
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid rgba(233, 237, 240, 0.35);
  border-radius: 8px;
  cursor: pointer;
}
.lightbox-close:hover,
.lightbox-close:focus-visible {
  background: rgba(0, 0, 0, 0.8);
  border-color: #e9edf0;
}

/* The visible caption under each slider, added 2026-09-06. Distinct from the
   alt text, which describes the picture for somebody who cannot see it and runs
   to a sentence or two; this tells a reader who CAN see it what they are
   looking at, in a few words. */
.shot-caption {
  margin: 8px 0 0;
  min-height: 1.2em; /* reserve the line so the card does not jump per slide */
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}

@media (max-width: 560px) {
  .lightbox {
    padding: 12px;
  }
  .lightbox-img {
    max-height: calc(100vh - 96px);
  }
}

/* =============================================================================
   REQUEST BETA ACCESS  -  the pop-up form
   -----------------------------------------------------------------------------
   A native <dialog>, so the focus trap, the Escape key, the inert background
   and the backdrop come from the browser rather than from JavaScript. See
   scripts/client/betaRequestForm.js.

   ⚠️ THE HEIGHT IS CAPPED AND THE BODY SCROLLS. Eight tick boxes plus five
   fields is taller than a phone in landscape, and a dialog taller than the
   viewport puts its submit button somewhere the person cannot reach - with no
   scrollbar to tell them it is there.
   ========================================================================== */
.beta-dialog {
  width: min(560px, calc(100vw - 32px));
  max-height: min(88vh, 900px);
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  color: var(--ink);
  overflow: auto;
}
.beta-dialog::backdrop {
  background: rgba(8, 10, 12, 0.6);
}

.beta-form,
.beta-done {
  padding: 22px 24px 24px;
}

.beta-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}
.beta-head h2 {
  margin: 0;
  font-size: 22px;
}
.beta-close {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  font-size: 22px;
  line-height: 1;
  color: var(--muted);
  background: none;
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}
.beta-close:hover,
.beta-close:focus-visible {
  color: var(--ink);
  border-color: var(--accent);
}

.beta-intro {
  margin: 10px 0 18px;
  color: var(--ink2);
  font-size: 15px;
  line-height: 1.5;
}

.beta-field {
  margin: 0 0 14px;
}
.beta-field label {
  display: block;
  margin-bottom: 4px;
  font-size: 14px;
  font-weight: 600;
}
.beta-field input,
.beta-field textarea {
  width: 100%;
  padding: 9px 11px;
  /* 16px, and not smaller: iOS Safari zooms the whole page when a focused
     field's text is under 16px, which throws the layout of a modal about. */
  font: inherit;
  font-size: 16px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-sizing: border-box;
}
.beta-field input:focus-visible,
.beta-field textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.beta-field input[aria-invalid='true'],
.beta-field textarea[aria-invalid='true'] {
  border-color: #c0392b;
}

.beta-error {
  margin: 5px 0 0;
  color: #c0392b;
  font-size: 13px;
}

.beta-fieldset {
  margin: 0 0 14px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 8px;
}
.beta-fieldset legend {
  padding: 0 6px;
  font-size: 14px;
  font-weight: 600;
}
.beta-hint {
  margin: 0 0 8px;
  color: var(--muted);
  font-size: 13px;
}
.beta-check {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  /* A comfortable target on a phone - the label is part of it, because a 13px
     tick box on its own is not something a thumb can reliably hit. */
  padding: 4px 0;
}
.beta-check input {
  margin-top: 3px;
  flex: 0 0 auto;
}
.beta-check label {
  font-size: 14.5px;
  line-height: 1.4;
  cursor: pointer;
}

/* ⚠️ THE HONEYPOT. Off-screen rather than display:none - some bots skip a field
   they can see is not rendered, and the whole point is that they fill it in.
   It carries aria-hidden and tabindex="-1" in the markup, so no person meets
   it either way. */
.beta-pot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.beta-privacy {
  margin: 0 0 16px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
}

.beta-actions {
  display: flex;
  gap: 10px;
}
.beta-submit {
  padding: 11px 20px;
  font: inherit;
  font-size: 15px;
  color: #fff;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 8px;
  cursor: pointer;
}
.beta-submit:hover:not(:disabled) {
  filter: brightness(1.08);
}
.beta-submit:disabled {
  opacity: 0.6;
  cursor: progress;
}

.beta-status {
  margin: 12px 0 0;
  min-height: 1.2em; /* reserve the line so nothing shifts when it fills */
  font-size: 14px;
  color: var(--muted);
}
.beta-status.is-error {
  color: #c0392b;
}

.beta-done h2 {
  margin: 0 0 10px;
  font-size: 22px;
}
.beta-done p {
  margin: 0 0 18px;
  color: var(--ink2);
  font-size: 15px;
  line-height: 1.5;
}

/* A button that has to read as a link, because it sits mid-sentence in an FAQ
   answer. It is a <button> and not an <a> because it opens a dialog on this
   page rather than going anywhere - a link that navigates nowhere tells a
   keyboard or screen-reader user the wrong thing about what will happen. */
.linkbutton {
  padding: 0;
  font: inherit;
  color: var(--accent);
  background: none;
  border: none;
  text-decoration: underline;
  cursor: pointer;
}
.linkbutton:hover,
.linkbutton:focus-visible {
  text-decoration: none;
}

/* ⚠️ THE APP CARD'S OWN aspect-ratio HAS BEEN REMOVED, NOT MOVED. It existed
   only because the two cards' screenshots were different shapes; both sets are
   1760x1100 now, so the base rule above serves both and a second declaration
   here would be one more place to keep in step. */
