/* ============ Design tokens ============ */
:root {
  /* This site is always a warm light theme, never dark — without this,
     WebKit/Chrome render native form UI (date pickers, <select>
     dropdowns, scrollbars) assuming a DARK page by default in some
     browser/OS combinations, which can show a jarring dark-mode-colored
     native control against this cream background (same reasoning as
     docs/ios-audit.md's original dark-theme note, just flipped). */
  color-scheme: light;
  --bg: #F7F6F3;
  --panel: #FFFFFF;
  --ghost: #1A1720;
  --coral: #FF8969;
  /* Plain --coral only clears ~2.15:1 on this cream background — nowhere
     near enough for text at any size. Two darkened variants, not one:
     --coral-text (4.5:1+) for genuinely small/body-size coral text (links,
     labels, badges), and --coral-text-large (3:1+, WCAG's large-text
     threshold) for big display type like the hero headline accent, which
     only needs 3:1 and reads noticeably closer to the real brand coral
     than the small-text-safe shade would. */
  --coral-text: #B33E20;
  /* #E16B4C cleared 3:1 against the flat --bg, but the hero headline sits
     over the aurora glow gradient, which blends lighter there in practice
     (measured ~#f8ece6, not flat #F7F6F3) — axe caught this at 2.82:1
     against the real rendered background. Darkened with margin against
     the actual measured worst case, not just the flat token. */
  --coral-text-large: #D35D3F;
  --mint: #0A7A53;
  --mint-soft: #8FF7C7;
  --muted: #6B6478;

  --font-display: 'Fraunces', serif;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono: 'IBM Plex Mono', monospace;

  --max-width: 1120px;
  /* Corner-radius scale — every rounded corner on the page pulls from here */
  --radius-xl: 36px;
  --radius-lg: 28px;
  --radius-md: 16px;
  --radius-sm: 10px;

  /* Spacing rhythm scale — deliberate, not ad-hoc */
  --space-section: clamp(5.5rem, 11vw, 8.5rem);
  --space-section-lg: clamp(7.5rem, 15vw, 12rem);
  --space-heading-gap: clamp(2.5rem, 6vw, 4rem);

  /* Depth — one soft, low card shadow, one deeper shadow for the phone */
  --shadow-card: 0 16px 40px -20px rgba(0, 0, 0, 0.55);
  --shadow-deep: 0 30px 80px -20px rgba(0, 0, 0, 0.7);
  --hairline: 1px solid rgba(26, 23, 32, 0.08);
}

/* ============ Reset ============ */
*, *::before, *::after { box-sizing: border-box; }
/* docs/ios-audit.md — un-overridden, this is WebKit's default gray/black
   tap flash on every button/link, confirmed live at rgba(0,0,0,0.4)
   against this app's cream theme. :focus-visible states (already
   set throughout) still give real keyboard/switch-control feedback; this
   only removes the touch-only flash. */
*, *::before, *::after { -webkit-tap-highlight-color: transparent; }
/* overflow-x needs to be on html, not just body — the root element governs
   the viewport's scroll box, and decorative absolutely-positioned .glow
   elements (deliberately offset partway off their section) were still
   letting the page scroll ~23px horizontally on mobile despite body's own
   overflow-x: hidden below. */
html { scroll-behavior: auto; overflow-x: hidden; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ghost);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* ============ Cinematic ambient background ============
   A fixed, slowly-morphing aurora of brand-colour light behind everything,
   site-wide. It lives on body's pseudo-elements so every page gets it with no
   markup, and position:fixed so the light drifts calmly behind the content as
   you scroll — the whole site feels alive and lit rather than flat black.
   Two counter-drifting layers keep the motion organic (never a single obvious
   loop); a third grain layer (.grain-overlay, already present) adds film
   texture over the top. */
body::before {
  content: "";
  position: fixed;
  inset: -30%;
  z-index: -3;
  pointer-events: none;
  background:
    radial-gradient(38% 44% at 18% 20%, rgba(255, 137, 105, 0.4), transparent 68%),
    radial-gradient(42% 46% at 84% 14%, rgba(168, 143, 255, 0.36), transparent 70%),
    radial-gradient(48% 52% at 74% 80%, rgba(143, 247, 199, 0.26), transparent 72%),
    radial-gradient(40% 44% at 22% 84%, rgba(255, 110, 92, 0.3), transparent 70%);
  filter: blur(52px) saturate(1.3);
  animation: aurora-drift 36s ease-in-out infinite alternate;
  will-change: transform;
}
body::after {
  content: "";
  position: fixed;
  inset: -30%;
  z-index: -3;
  pointer-events: none;
  background:
    radial-gradient(36% 40% at 60% 32%, rgba(255, 171, 133, 0.32), transparent 66%),
    radial-gradient(40% 44% at 30% 62%, rgba(184, 156, 255, 0.28), transparent 70%);
  filter: blur(64px);
  mix-blend-mode: screen;
  animation: aurora-drift-2 48s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes aurora-drift {
  0%   { transform: translate3d(-2%, -1%, 0) scale(1) rotate(0deg); }
  50%  { transform: translate3d(2.5%, 2%, 0) scale(1.14) rotate(3deg); }
  100% { transform: translate3d(-1%, 3.5%, 0) scale(1.07) rotate(-2.5deg); }
}
@keyframes aurora-drift-2 {
  0%   { transform: translate3d(2%, 1%, 0) scale(1.05); }
  100% { transform: translate3d(-3%, -2.5%, 0) scale(1.2); }
}
@media (prefers-reduced-motion: reduce) {
  body::before, body::after { animation: none; }
}

img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; cursor: pointer; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
input { font: inherit; color: inherit; }
h1, h2, h3, p { margin: 0; }

/* Visible keyboard focus everywhere */
a:focus-visible,
button:focus-visible,
input:focus-visible {
  outline: 2px solid var(--mint);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ============ Ghost glyph ============ */
/* A single solid-color silhouette (the path itself uses fill="currentColor"
   inline, no separate outline/stroke) — the logo mark, unchanged in shape
   or style from before the light-theme flip. Base color is the brand
   coral rather than the body-text --ghost token specifically so the mark
   stays on-brand and clearly visible wherever it appears, independent of
   whatever --ghost currently means for text. */
.ghost-glyph {
  width: 40px;
  height: 40px;
  color: var(--coral);
}
.ghost-glyph--tiny { width: 22px; height: 22px; }

/* ============ Buttons ============ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  padding: 0.85em 1.6em;
  border-radius: 999px;
  font-weight: 600;
  font-size: 0.95rem;
  transition: background 0.15s ease;
  white-space: nowrap;
}
.btn--coral {
  background: linear-gradient(135deg, #ffab85, var(--coral) 55%, #ff7a54);
  color: #1a0e08;
  box-shadow: 0 8px 24px -10px rgba(255, 137, 105, 0.55);
  transition: box-shadow 0.2s ease, background 0.15s ease;
}
.btn--coral:hover { box-shadow: 0 12px 32px -8px rgba(255, 137, 105, 0.7); }
.btn--ghost { background: rgba(26, 23, 32, 0.1); color: var(--ghost); border: 1px solid rgba(26, 23, 32, 0.25); }
.btn--ghost:hover { background: rgba(26, 23, 32, 0.16); }
.btn--outline { background: transparent; color: var(--ghost); border: 1px dashed rgba(26, 23, 32, 0.4); padding: 0.7em 1.3em; font-size: 0.9rem; }
.btn--outline:hover { border-color: var(--ghost); }
.btn[disabled] { opacity: 0.6; cursor: not-allowed; transform: none; }

/* ============ Header ============ */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.1rem clamp(1.25rem, 4vw, 3rem);
  /* docs/ios-audit.md — now that viewport-fit=cover opts every page into
     edge-to-edge rendering, this fixed header sits directly under the
     notch/Dynamic Island unless it reserves real space for it. max()
     keeps the original padding on non-notch devices/browsers that don't
     support env() at all. */
  padding-top: max(1.1rem, env(safe-area-inset-top));
  background: linear-gradient(to bottom, rgba(247, 246, 243, 0.9), rgba(247, 246, 243, 0));
  backdrop-filter: blur(6px);
}
.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.15rem;
}
.brand-logo {
  width: 22px;
  height: 22px;
  border-radius: 6px;
  object-fit: cover;
  flex-shrink: 0;
}
.header-cta { font-size: 0.85rem; padding: 0.6em 1.3em; }
.header-actions { display: flex; align-items: center; gap: 1.25rem; }
.header-login-link, .header-nav-link { font-size: 0.85rem; color: var(--muted); text-decoration: none; }
.header-login-link:hover, .header-nav-link:hover { color: var(--ghost); }
/* Same "you are here" convention as .footer-link--current — set by
   nav-transition.js on whichever header nav link matches the current
   page. */
.header-nav-link.header-nav-link--current { color: var(--coral-text); font-weight: 600; cursor: default; }
/* Only the purely informational links (About, Landlords on index.html) hide
   below 560px to keep the header from crowding — .header-login-link is the
   actual login link on every page and must stay reachable at every width;
   it used to share this same hide rule, leaving mobile visitors with no way
   to reach login at all. */
@media (max-width: 560px) { .header-nav-link { display: none; } }
/* Below ~480px, "House login" is the one item left crowding the header
   against the brand name and the Get started pill (a real device
   screenshot showed them nearly touching). It still has to stay reachable
   at every width — see the comment above — so it becomes a compact icon
   rather than being hidden. aria-label on the link itself keeps "House
   login" as the accessible name regardless of which of icon/text is
   visible. */
.header-login-icon { display: none; width: 20px; height: 20px; vertical-align: middle; }
@media (max-width: 480px) {
  .header-login-text { display: none; }
  .header-login-icon { display: inline-block; }
  .header-actions { gap: 0.75rem; }
}

/* ============ Section basics ============ */
main > section {
  padding: var(--space-section) clamp(1.25rem, 5vw, 3rem);
  max-width: var(--max-width);
  margin: 0 auto;
  position: relative;
}
.section-heading {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.6rem, 3.4vw, 2.4rem);
  text-align: center;
  max-width: 42ch;
  margin: 0 auto var(--space-heading-gap);
  color: var(--ghost);
}

/* ============ HERO ============ */
/* No longer scroll-pinned — the phone now runs its own self-looping
   swipe cycle (script.js's setupHeroLoop) instead of scrubbing a fixed
   sequence to scroll position, so .hero-scroll doesn't need the extra
   scroll-runway height a pinned section requires, and .hero-pin doesn't
   need to actually pin. Kept as plain, ordinary flow layout instead. */
.hero-scroll {
  position: relative;
  max-width: none;
  padding: 0;
}
.hero-pin {
  display: flex;
  align-items: safe center;
  justify-content: space-between;
  padding: 5.5rem clamp(1.25rem, 6vw, 6rem) 1.5rem;
  gap: clamp(3rem, 6vw, 6rem);
  /* Wider than the sitewide --max-width (1120px) on purpose — every other
     section is prose-width-constrained by design, but the hero is the
     first thing anyone sees, and on a wide desktop screen a 1120px cap
     left a large, unbalanced dead zone between the headline and the phone
     mockup instead of the two reading as one composed, full-width scene. */
  max-width: 1440px;
  margin: 0 auto;
  flex-wrap: wrap;
}
.hero-copy { flex: 1 1 420px; max-width: 640px; }
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.03em;
  color: var(--muted);
  background: rgba(26, 23, 32, 0.06);
  border: var(--hairline);
  border-radius: 999px;
  padding: 0.45em 0.95em 0.45em 0.75em;
  margin-bottom: 1.75rem;
}
.eyebrow::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--mint);
  box-shadow: 0 0 8px 1px rgba(143, 247, 199, 0.6);
  flex-shrink: 0;
}
.hero-headline {
  font-family: var(--font-display);
  font-weight: 400;
  font-style: normal;
  font-size: clamp(2.8rem, 7vw, 6rem);
  line-height: 1.14;
  letter-spacing: -0.03em;
  color: var(--ghost);
}
.headline-accent {
  /* Large display type (clamp 2.8rem-6rem) qualifies for WCAG's large-text
     3:1 threshold, not the stricter 4.5:1 small-text one — so it gets the
     variant closer to the real brand coral rather than the small-text-safe
     shade used everywhere else coral appears as text. */
  color: var(--coral-text-large);
  text-shadow: 0 0 28px rgba(255, 137, 105, 0.35);
}
.hero-sub {
  margin-top: 2rem;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.6;
  max-width: 42ch;
}

/* A quiet, scannable "everything it does" ticker for the very first
   viewport — the headline/subhead only gesture at chores/bills/deposits,
   and the full breadth (mediation, dashboard, Revolut, rent/policy
   reminders) otherwise only shows up several scrolls down in the "How it
   works" grid. Small mono type + a coral dot per item, echoing .eyebrow's
   own visual language, so it reads as a quiet fact strip, not a second
   headline competing with the real one. */
.hero-capabilities {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 1.5rem 0 0;
  padding: 0;
  list-style: none;
  max-width: 420px;
}
.hero-capabilities li {
  display: flex;
  align-items: flex-start;
  gap: 0.55em;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.01em;
  color: var(--muted);
  line-height: 1.4;
  text-align: left;
}
.hero-capabilities li::before {
  content: "";
  width: 4px;
  height: 4px;
  margin-top: 0.5em;
  border-radius: 50%;
  background: var(--coral);
  flex-shrink: 0;
}

/* ---- Phone mockup ---- */
.phone-stage { position: relative; flex: 0 0 auto; margin: 0 auto; }
.phone {
  /* 9:19.5-ish — matches a real modern phone's aspect ratio instead of a flat 1:2 rounded rectangle */
  width: 300px;
  height: 638px;
  background: #0B0A14;
  border-radius: var(--radius-xl);
  border: 8px solid #26223B;
  box-shadow:
    var(--shadow-deep),
    0 0 0 1px rgba(26, 23, 32, 0.04),
    inset 0 1px 0 rgba(26, 23, 32, 0.08);
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Same floating, tilted presentation as the pitch page's phone mockups —
     a slight 3D lean rather than a flat, static rectangle. */
  transform: perspective(1400px) rotateY(-9deg) rotateX(3deg);
  transform-style: preserve-3d;
  animation: phone-float 7s ease-in-out infinite;
}
@keyframes phone-float {
  0%, 100% { transform: perspective(1400px) rotateY(-9deg) rotateX(3deg) translateY(0); }
  50% { transform: perspective(1400px) rotateY(-7deg) rotateX(2deg) translateY(-9px); }
}
@media (prefers-reduced-motion: reduce) {
  .phone { animation: none; transform: none; }
}
.phone-notch {
  /* Dynamic-Island-style floating pill, not a wide notch band fused to the edge */
  position: absolute;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  width: 90px;
  height: 26px;
  background: #050409;
  border-radius: 999px;
  z-index: 5;
}
.phone-screen {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: var(--panel);
}
/* Two full scenes stacked in the same box — only .is-active is on-screen,
   the other sits translated fully off to the right until it's swiped in.
   Both are real, present DOM (not display:none) so a hidden scene's own
   stream can be silently reset while it's off-screen without a visible
   flash, and so the swipe itself has something to animate between. */
.phone-scenes { position: relative; flex: 1; overflow: hidden; }
.phone-scene {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  opacity: 0;
  transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.4s ease;
  will-change: transform, opacity;
}
.phone-scene.is-active { transform: translateX(0); opacity: 1; }
.phone-scene.is-leaving { transform: translateX(-100%); opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .phone-scene { transition: none; }
}
/* This whole chat mockup is a deliberate, close recreation of the real
   WhatsApp UI (iOS) — colors, layout, and chrome below are WhatsApp's own,
   not this site's palette, same reasoning .ghost-grab already hardcodes its
   own colors for. WhatsApp's real accent teal is #008069; its real sent-
   bubble green is #D9FDD3; its real wallpaper is a warm tan, not flat white. */
/* A real phone always shows its own status bar (time, signal, wifi,
   battery) above whatever app is open — WhatsApp's white nav chrome
   doesn't extend under it. 9:41 is the same idealized "always shown in
   phone mockups" time Apple's own marketing has used for years, matching
   the notify-lock-screen phone's clock elsewhere on this page. */
.status-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 2.5rem;
  padding: 0 1.1rem;
  font-size: 0.68rem;
  font-weight: 600;
  color: #1A1720;
  background: #FFFFFF;
  position: relative;
  z-index: 3;
}
.status-icons { display: flex; align-items: center; gap: 3px; }
.status-icons svg { display: block; }
.chat-header {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.6rem 0.9rem 0.7rem;
  border-bottom: 1px solid rgba(26, 23, 32, 0.08);
  background: #FFFFFF;
}
.chat-back { color: #008069; display: flex; margin-right: -0.3rem; flex-shrink: 0; }
.chat-avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  flex-shrink: 0;
  overflow: hidden;
}
.chat-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.chat-header-info { display: flex; flex-direction: column; flex: 1; min-width: 0; line-height: 1.25; }
.chat-title-row { display: flex; align-items: center; gap: 0.3em; }
.chat-title { font-weight: 600; font-size: 0.95rem; color: #111B21; }
.verified-badge { width: 14px; height: 14px; flex-shrink: 0; }
.chat-status { font-size: 0.7rem; color: #5B6770; }
.chat-header-icons { display: flex; align-items: center; gap: 0.9rem; color: #008069; flex-shrink: 0; }

.chat-thread {
  flex: 1;
  padding: 0.85rem 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  overflow: hidden;
  /* WhatsApp's real chat wallpaper — a warm tan, never a flat white/panel
     background. A faint repeating doodle pattern (data-URI, not a raster
     asset) approximates the real app's tiled icon wallpaper at a glance
     without the weight of vendoring one. */
  background-color: #E5DDD3;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cg fill='%23D8CEC0' fill-opacity='0.55'%3E%3Ccircle cx='20' cy='20' r='1.6'/%3E%3Ccircle cx='70' cy='40' r='1.6'/%3E%3Ccircle cx='45' cy='75' r='1.6'/%3E%3Ccircle cx='90' cy='85' r='1.6'/%3E%3Ccircle cx='10' cy='90' r='1.6'/%3E%3C/g%3E%3C/svg%3E");
}
.chat-message { display: flex; flex-direction: column; max-width: 85%; }
.chat-message--in { align-self: flex-start; }
.chat-message--out { align-self: flex-end; }
/* The time (+ read ticks, sent side only) sits INSIDE the bubble, trailing
   the message text — floated right so it tucks in beside the last line of
   text when there's room, exactly like the real app, instead of sitting on
   its own line below the bubble. */
.chat-meta {
  float: right;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin: 0.3em 0 -0.15em 0.5em;
}
.chat-timestamp {
  /* Was 0.62rem — against .chat-bubble's 0.82rem message text, that's a
     ~0.76 timestamp-to-message ratio, measurably bigger than real
     WhatsApp's own (~11px against ~16-17px body text, ~0.65) — confirmed
     against a live screenshot, not just eyeballed. 0.52rem brings this
     mockup back in line with the real app's proportions. */
  font-size: 0.52rem;
  /* A solid color, not alpha-blended — real WhatsApp's timestamp reads as
     translucent, but at a low enough alpha to still look authentic its
     effective composited color fails WCAG AA against both the white
     incoming bubble and the green sent one. This solid gray clears 4.5:1
     against both. */
  color: #5B6770;
}
.chat-ticks { color: #53BDEB; }
.chat-bubble {
  position: relative;
  font-size: 0.82rem;
  padding: 0.45em 0.55em 0.35em 0.6em;
  border-radius: 8px;
  line-height: 1.35;
  color: #111B21;
  box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.13);
}
.chat-bubble--in {
  background: #FFFFFF;
  border-top-left-radius: 2px;
}
.chat-bubble--calm {
  background: #D9FDD3;
  border-top-right-radius: 2px;
  opacity: 0;
  transform: translateY(10px);
  will-change: opacity, transform;
}
/* Plain sent-bubble styling for the hero's streaming scenes — same real
   WhatsApp green as .chat-bubble--calm, but with no baked-in hidden state,
   since streaming messages get their own entrance via .chat-message's
   hero-bubble-in animation below rather than one manually-tweened reveal. */
.chat-bubble--out {
  background: #D9FDD3;
  border-top-right-radius: 2px;
}

/* Real WhatsApp video-message bubble: the media itself IS the bubble (no
   padding/background), rounded corners, a bottom scrim carrying the
   timestamp, ticks and duration. Plays for real (autoplay/loop/muted —
   required for autoplay in-browser) rather than a paused, unreadable
   poster frame. */
.chat-media-bubble { position: relative; width: 172px; border-radius: 8px; overflow: hidden; background: #000; box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.15); }
.chat-media-bubble video { display: block; width: 100%; height: auto; }
.chat-media-bubble::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 30%; background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent); pointer-events: none; }
.chat-media-dur { position: absolute; left: 7px; bottom: 5px; font-size: 0.62rem; font-weight: 600; color: #fff; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); }
.chat-media-meta { position: absolute; right: 7px; bottom: 5px; display: flex; align-items: center; gap: 3px; font-size: 0.6rem; color: #fff; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); }
.chat-media-meta .chat-ticks { color: #53BDEB; }

/* Each new streaming message (hero phone scenes) fades/rises in on
   creation — the old rewrite mechanic only ever revealed one fixed bubble
   via a manual GSAP tween, so it never needed a generic per-message
   entrance; the new repeating stream does. */
@keyframes hero-bubble-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.chat-message { animation: hero-bubble-in 0.35s ease forwards; }

.typing-indicator {
  display: flex;
  align-items: center;
  gap: 4px;
  align-self: flex-end;
  background: rgba(26, 23, 32, 0.08);
  border-radius: var(--radius-md);
  padding: 0.6em 0.8em;
  opacity: 0;
  width: fit-content;
}
.typing-indicator span {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--muted);
  /* Paused by default — only running during the brief window the indicator
     is visible (toggled by JS) rather than looping for the page's entire
     lifetime, which measurably cost frame time during the hero scrub. */
  animation: typing-bounce 1s ease-in-out infinite;
  animation-play-state: paused;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.15s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.3s; }
@keyframes typing-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
  30% { transform: translateY(-3px); opacity: 1; }
}

.chat-input-bar {
  position: relative;
  margin: 0.6rem 0.7rem 0.8rem;
  padding: 0.6em 0.7em;
  background: #FFFFFF;
  border: 1px solid rgba(17, 27, 33, 0.14);
  border-radius: 999px;
  min-height: 2.5em;
  display: flex;
  align-items: center;
  gap: 0.5em;
}
.chat-input-emoji { color: #8696A0; flex-shrink: 0; display: flex; }
/* Static decorative placeholder text for the hero's streaming-scene input
   bars — real WhatsApp always shows an empty composer with a "Message"
   placeholder, not the live conversation's own text. */
.chat-input-placeholder { flex: 1; font-size: 0.78rem; color: #667781; }
.chat-send {
  color: #008069;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ============ PROBLEM ============ */
.problem { overflow: hidden; }
.dashboard-feature { overflow: hidden; position: relative; }
.dashboard-feature-sub {
  font-family: var(--font-mono);
  font-size: 0.9rem;
  color: var(--muted);
  text-align: center;
  max-width: 46ch;
  margin: 0 auto 2.5rem;
}
.problem-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1.5rem;
}
.problem-card {
  background: var(--panel);
  border: var(--hairline);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  padding: 1.75rem;
  opacity: 0;
  transform: translateY(24px);
  transition: transform 0.25s ease, border-color 0.25s ease;
}
.problem-card:hover {
  transform: translateY(-4px);
  border-color: rgba(255, 137, 105, 0.35);
}
.problem-label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--coral-text);
  margin-bottom: 0.9rem;
}
.problem-bubble {
  font-size: 1rem;
  line-height: 1.5;
  color: var(--ghost);
}

/* ============ TRY IT YOURSELF ============ */
.demo-lede {
  text-align: center;
  color: var(--muted);
  max-width: 46ch;
  margin: -1.5rem auto 2.5rem;
}
.demo-widget {
  max-width: 560px;
  margin: 0 auto;
  background: var(--panel);
  border: var(--hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: clamp(1.75rem, 4vw, 2.75rem);
}
.demo-form { display: flex; flex-direction: column; gap: 1.25rem; }
.demo-input {
  background: rgba(26, 23, 32, 0.06);
  border: 1px solid rgba(26, 23, 32, 0.15);
  border-radius: var(--radius-sm);
  padding: 0.75em 0.9em;
  color: var(--ghost);
  font-family: inherit;
  /* docs/ios-audit.md — anything under 16px makes iOS Safari auto-zoom
     the whole page on focus. */
  font-size: 16px;
  line-height: 1.5;
  resize: vertical;
}
.demo-input::placeholder { color: rgba(107, 100, 120, 0.6); }
.demo-input:focus {
  border-color: var(--mint);
  animation: input-glow-breathe 2.4s ease-in-out infinite;
}
.demo-form .btn { align-self: center; }
.demo-form .btn[disabled] { opacity: 0.55; cursor: wait; }
.demo-result { margin-top: 1.5rem; }
.demo-result-label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--mint);
  margin-bottom: 0.6rem;
}
.demo-result-bubble {
  opacity: 1;
  transform: none;
  font-size: 0.95rem;
  padding: 1em 1.15em;
  border-bottom-right-radius: var(--radius-md);
}
.demo-error {
  margin-top: 1rem;
  color: var(--coral-text);
  font-size: 0.9rem;
  text-align: center;
}
.demo-cta {
  margin-top: 1.5rem;
  padding-top: 1.5rem;
  border-top: var(--hairline);
  text-align: center;
}
.demo-cta p { color: var(--muted); margin-bottom: 1rem; }

/* ============ HOW IT WORKS ============ */
.how-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.5rem;
  perspective: 1000px;
}
@media (max-width: 720px) {
  .how-grid { grid-template-columns: 1fr; }
}
.how-card {
  position: relative;
  background:
    radial-gradient(120% 90% at 0% 0%, rgba(255, 137, 105, 0.10), transparent 45%),
    radial-gradient(120% 90% at 100% 0%, rgba(184, 156, 255, 0.09), transparent 48%),
    var(--panel);
  border: 1px solid rgba(26, 23, 32, 0.10);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: 2rem;
  opacity: 0;
  transform: translateY(24px);
  transform-style: preserve-3d;
  will-change: transform;
  /* transform is owned by the GSAP reveal, so hover motion uses the
     independent scale/translate properties (which compose with transform)
     rather than fighting it. */
  transition: scale 0.4s cubic-bezier(0.2, 0.7, 0.2, 1),
    translate 0.4s cubic-bezier(0.2, 0.7, 0.2, 1), box-shadow 0.4s ease,
    border-color 0.4s ease;
}
/* A soft gradient-lit border, brighter on hover, so cards read as crafted
   surfaces catching the ambient light rather than flat panels. */
.how-card::after,
.info-card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(135deg, rgba(255, 137, 105, 0.55), rgba(184, 156, 255, 0.4) 45%, transparent 72%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0.5;
  transition: opacity 0.4s ease;
  pointer-events: none;
}
@media (hover: hover) {
  .how-card:hover,
  .info-card:hover {
    scale: 1.022;
    translate: 0 -6px;
    border-color: rgba(255, 137, 105, 0.45);
    box-shadow: 0 36px 72px -28px rgba(0, 0, 0, 0.78), 0 0 48px -14px rgba(255, 137, 105, 0.3);
  }
  .how-card:hover::after,
  .info-card:hover::after {
    opacity: 1;
  }
}
@media (prefers-reduced-motion: reduce) {
  .how-card,
  .info-card {
    transition: none;
  }
}
.how-icon {
  width: 30px;
  height: 30px;
  color: var(--coral);
  margin-bottom: 1.25rem;
}
.how-card h3 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.2rem;
  margin-bottom: 0.6rem;
  color: var(--ghost);
}
.how-card p {
  color: var(--muted);
  font-size: 0.95rem;
  line-height: 1.55;
  text-wrap: pretty;
}
.how-card p a {
  color: var(--coral);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.how-card p a:hover {
  color: var(--ghost);
}
/* How-it-works has an odd card count (7), so the last one falls alone
   into the left column of the 2-col grid, leaving a lopsided gap on the
   right. Span it across both columns and center its content instead.
   Trust FAQ used to need this too, back when it had 5 cards — now that
   it's grown to 6 (even), the pair fills both columns on its own, and
   this same rule would be the thing CREATING the gap instead of fixing
   it, so it's scoped to how-it-works only. */
#how-it-works .how-card:last-child {
  grid-column: 1 / -1;
  text-align: center;
}
#how-it-works .how-card:last-child p {
  max-width: 760px;
  margin-left: auto;
  margin-right: auto;
}
#how-it-works .how-card:last-child .how-icon {
  margin-left: auto;
  margin-right: auto;
}

/* ============ GHOST SCENE ============ */
/* A single continuous 12s loop, always in motion — no dead stationary
   time anywhere in the cycle. (The first version dwelt at house 1 for
   60%-100% AND 0%-5% of a 16s loop — 7.2s of just sitting there, since
   those two ranges are adjacent across the loop boundary. Real, reported
   bug, not a one-off.) Structure: dwell at each house (8% ~1s), a short
   arcing transit to the next (12% ~1.4s), then instead of fading out and
   teleporting back, one continuous higher, banking swoop from house 4 all
   the way back to house 1 (32% ~3.8s) — visually distinct as an
   intentional flourish rather than a pause, and the ghost is never
   invisible. left (a % of the stage, matching each .scene-house's own
   left%) drives horizontal position; transform (px-based, self-relative so
   % ambiguity doesn't matter) drives the arc/bank/scale. */
.ghost-scene { overflow: hidden; }
.ghost-scene-stage {
  position: relative;
  height: clamp(200px, 26vw, 300px);
  max-width: 860px;
  margin: 0 auto;
}
.scene-ground {
  position: absolute;
  left: 4%;
  right: 4%;
  bottom: 18%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(26, 23, 32, 0.14) 12%, rgba(26, 23, 32, 0.14) 88%, transparent);
}
.scene-house {
  position: absolute;
  bottom: 18%;
  width: clamp(48px, 8vw, 78px);
  color: var(--muted);
  transform: translateX(-50%);
}
.scene-house-icon { width: 100%; height: auto; position: relative; z-index: 1; }
.scene-house--1 { left: 8%; }
.scene-house--2 { left: 36%; }
.scene-house--3 { left: 64%; }
.scene-house--4 { left: 92%; }

/* A soft window-glow pulse behind each house exactly while the ghost is
   there — a "someone's home and it's handled" beat, not just the bubble
   doing all the work. */
.scene-house-glow {
  position: absolute;
  left: 50%;
  top: 42%;
  width: 150%;
  height: 150%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(143, 247, 199, 0.4), rgba(143, 247, 199, 0) 70%);
  filter: blur(3px);
  opacity: 0;
  will-change: opacity;
  animation-duration: 12s;
  animation-timing-function: ease;
  animation-iteration-count: infinite;
}
.scene-house-glow--1 { animation-name: house-glow-1; }
.scene-house-glow--2 { animation-name: house-glow-2; }
.scene-house-glow--3 { animation-name: house-glow-3; }
.scene-house-glow--4 { animation-name: house-glow-4; }
@keyframes house-glow-1 { 0%, 4% { opacity: 0.9; } 8%, 100% { opacity: 0; } }
@keyframes house-glow-2 { 0%, 19% { opacity: 0; } 22%, 26% { opacity: 0.9; } 28%, 100% { opacity: 0; } }
@keyframes house-glow-3 { 0%, 39% { opacity: 0; } 42%, 46% { opacity: 0.9; } 48%, 100% { opacity: 0; } }
@keyframes house-glow-4 { 0%, 59% { opacity: 0; } 62%, 66% { opacity: 0.9; } 68%, 100% { opacity: 0; } }

.scene-ghost,
.scene-ghost-echo {
  position: absolute;
  bottom: 42%;
  width: 44px;
  height: 44px;
  color: var(--ghost);
}
.scene-ghost .ghost-glyph,
.scene-ghost-echo .ghost-glyph { width: 100%; height: 100%; }
/* Same white-body/black-outline cartoon treatment as .ghost-grab — the
   traveling illustration (and its faint motion-trail echoes) is a playful
   character moment, not the brand mark. */
.scene-ghost .ghost-glyph path,
.scene-ghost-echo .ghost-glyph path { fill: #FFFDF9; stroke: #1A1720; stroke-width: 4; stroke-linejoin: round; }
.scene-ghost {
  z-index: 3;
  filter: drop-shadow(0 0 16px rgba(255, 137, 105, 0.35));
  will-change: left, transform;
  animation: ghost-fly 12s ease-in-out infinite;
}
/* Two faint, blurred afterimages running the identical path on a fixed
   time lag (positive animation-delay against the same infinite 12s loop
   holds a constant phase offset forever, not just on the first cycle) —
   a cheap CSS-only trail/motion-blur effect with no JS and no extra
   elements to keep in sync by hand. */
.scene-ghost-echo {
  z-index: 2;
  pointer-events: none;
  will-change: left, transform;
  animation: ghost-fly 12s ease-in-out infinite;
  animation-fill-mode: backwards;
}
.scene-ghost-echo--1 { opacity: 0.22; filter: blur(1.5px); animation-delay: 0.15s; }
.scene-ghost-echo--2 { opacity: 0.1; filter: blur(3px); animation-delay: 0.3s; }

@keyframes ghost-fly {
  0%, 8%    { left: 8%;  transform: translateY(0) rotate(0deg) scale(1); }
  14%       { left: 22%; transform: translateY(-50px) rotate(-10deg) scale(1); }
  20%, 28%  { left: 36%; transform: translateY(0) rotate(0deg) scale(1); }
  34%       { left: 50%; transform: translateY(-50px) rotate(10deg) scale(1); }
  40%, 48%  { left: 64%; transform: translateY(0) rotate(0deg) scale(1); }
  54%       { left: 78%; transform: translateY(-50px) rotate(-10deg) scale(1); }
  60%, 68%  { left: 92%; transform: translateY(0) rotate(0deg) scale(1); }
  /* The "swoop home" — higher and more dramatic than the short hops
     above, banking hard both ways, so it reads as a deliberate flourish
     rather than the ghost just finding its way back. */
  76%       { left: 70%; transform: translateY(-95px) rotate(14deg) scale(1.08); }
  84%       { left: 42%; transform: translateY(-118px) rotate(-12deg) scale(1.08); }
  92%       { left: 16%; transform: translateY(-50px) rotate(-6deg) scale(1.02); }
  100%      { left: 8%;  transform: translateY(0) rotate(0deg) scale(1); }
}

.scene-bubble {
  position: absolute;
  bottom: 66%;
  transform: translateX(-50%) translateY(10px);
  /* width, not max-width — an absolutely-positioned box with only `left`
     set (no `right`) shrink-to-fits against the remaining distance to its
     containing block's edge, not the full max-width, when that distance is
     smaller. Bit the house-4 bubble (left: 92%, close to the stage's own
     right edge) hard: it silently wrapped into a narrow 4-line column
     instead of using the full 148px. An explicit width sidesteps the
     containing-block-edge calculation entirely; centering still happens
     purely via the transform above, which runs after layout. */
  width: 152px;
  background: var(--panel);
  border: var(--hairline);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-card);
  padding: 0.55em 0.8em;
  font-size: 0.76rem;
  line-height: 1.35;
  color: var(--ghost);
  text-align: center;
  opacity: 0;
  will-change: opacity, transform;
  animation-duration: 12s;
  animation-timing-function: ease;
  animation-iteration-count: infinite;
}
/* Speech-bubble tail, pointing down toward the house it belongs to — a
   plain rounded rectangle floating above a house read as an unrelated
   label; the tail visually anchors it as *that house's* moment. */
.scene-bubble::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -5px;
  width: 10px;
  height: 10px;
  background: var(--panel);
  border-right: var(--hairline);
  border-bottom: var(--hairline);
  transform: translateX(-50%) rotate(45deg);
}
.scene-bubble--1 { left: 8%;  animation-name: bubble-1; }
.scene-bubble--2 { left: 36%; animation-name: bubble-2; }
.scene-bubble--3 { left: 64%; animation-name: bubble-3; }
.scene-bubble--4 { left: 92%; animation-name: bubble-4; }

@keyframes bubble-1 {
  0%, 4%    { opacity: 1; transform: translateX(-50%) translateY(0); }
  8%, 100%  { opacity: 0; transform: translateX(-50%) translateY(10px); }
}
@keyframes bubble-2 {
  0%, 19%   { opacity: 0; transform: translateX(-50%) translateY(10px); }
  22%, 26%  { opacity: 1; transform: translateX(-50%) translateY(0); }
  28%, 100% { opacity: 0; transform: translateX(-50%) translateY(10px); }
}
@keyframes bubble-3 {
  0%, 39%   { opacity: 0; transform: translateX(-50%) translateY(10px); }
  42%, 46%  { opacity: 1; transform: translateX(-50%) translateY(0); }
  48%, 100% { opacity: 0; transform: translateX(-50%) translateY(10px); }
}
@keyframes bubble-4 {
  0%, 59%   { opacity: 0; transform: translateX(-50%) translateY(10px); }
  62%, 66%  { opacity: 1; transform: translateX(-50%) translateY(0); }
  68%, 100% { opacity: 0; transform: translateX(-50%) translateY(10px); }
}

/* Static snapshot instead of motion — same idea (every house, something
   different handled), all four beats visible at once rather than none. */
@media (prefers-reduced-motion: reduce) {
  .scene-ghost { animation: none; left: 50%; bottom: 60%; transform: translateY(0) rotate(0deg) scale(1); }
  .scene-ghost-echo { display: none; }
  .scene-house-glow { animation: none; opacity: 0.5; }
  .scene-bubble { animation: none; opacity: 1; transform: translateX(-50%) translateY(0); }
}

@media (max-width: 640px) {
  .scene-bubble { width: 112px; font-size: 0.68rem; padding: 0.45em 0.6em; }
}

/* ============ PRICING ============ */
.pricing {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding-top: var(--space-section-lg);
  padding-bottom: var(--space-section-lg);
  overflow: hidden;
}
.pricing-lede {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.5rem, 3.4vw, 2rem);
  letter-spacing: -0.01em;
  color: var(--ghost);
  text-align: center;
  max-width: 24ch;
  margin: 0 auto 1rem;
}
.pricing-deposit-anchor {
  max-width: 48ch;
  margin: 0 auto 2.75rem;
  text-align: center;
}
/* Side-by-side pricing: the configurator on the left, a bold glowing price card
   on the right — using the horizontal space instead of one tall centred stack. */
.pricing-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: clamp(1.75rem, 4vw, 3.5rem);
  align-items: center;
  width: min(1060px, 100%);
  margin: 0 auto;
  text-align: left;
}
.pricing-config {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
.pricing-config .pricing-founding-badge {
  max-width: none;
  margin: 0;
  text-align: left;
}
.pricing-config .pricing-controls {
  width: 100%;
  margin: 0;
}
.pricing-config .pricing-includes {
  max-width: none;
  margin: 0;
  justify-content: flex-start;
}
.pricing-config .pricing-footnote {
  margin: 0;
  max-width: none;
  text-align: left;
}
.pricing-summary {
  display: flex;
  justify-content: center;
}
.pricing-summary .receipt {
  max-width: none;
  width: 100%;
  padding: 2.9rem 2.5rem;
  border: 1px dashed rgba(255, 137, 105, 0.4);
  background:
    radial-gradient(130% 90% at 50% 0%, rgba(255, 137, 105, 0.16), transparent 62%),
    var(--panel);
  box-shadow: 0 44px 100px -34px rgba(0, 0, 0, 0.75), 0 0 80px -22px rgba(255, 137, 105, 0.26);
}
.pricing-summary .receipt-title {
  color: var(--coral-text);
}
.pricing-summary .receipt-line {
  font-size: clamp(1.25rem, 2.4vw, 1.5rem);
}
.pricing-summary .receipt-line--muted {
  font-size: 0.92rem;
}
@media (max-width: 860px) {
  .pricing-layout {
    grid-template-columns: 1fr;
    width: min(30rem, 100%);
    text-align: center;
  }
  .pricing-config {
    align-items: center;
  }
  .pricing-config .pricing-founding-badge,
  .pricing-config .pricing-includes,
  .pricing-config .pricing-footnote {
    text-align: center;
  }
  .pricing-config .pricing-includes {
    justify-content: center;
  }
}
.pricing-founding-badge {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--coral-text);
  text-align: center;
  max-width: 460px;
  /* No negative top margin: the deposit-anchor paragraph now sits directly
     above, so a pull-up would overlap its last line. */
  margin: 0.5rem auto 2rem;
  background: rgba(255, 137, 105, 0.1);
  border: 1px solid rgba(255, 137, 105, 0.3);
  border-radius: 999px;
  padding: 0.6em 1.2em;
}
/* Brand ghost glyph inline in the founding badge, in place of a generic emoji;
   inherits the badge's coral via currentColor. */
.badge-ghost {
  display: inline-block;
  width: 1.05em;
  height: 1.05em;
  vertical-align: -0.2em;
  margin-right: 0.5em;
}
.pricing-includes {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.6rem;
  max-width: 480px;
  margin: 2.5rem auto 0;
}
.pricing-includes span {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--muted);
  background: rgba(26, 23, 32, 0.06);
  border: var(--hairline);
  border-radius: 999px;
  padding: 0.5em 0.95em;
}
.pricing-footnote {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--muted);
  text-align: center;
  max-width: 420px;
  margin: 1.25rem auto 0;
}
.pricing-includes span::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--mint);
  flex-shrink: 0;
}
/* ============ Pricing configurator ============
   One tidy column of equal-width, labelled segmented controls, so the plan /
   billing / currency / size choices line up instead of sitting at ragged
   widths. The old per-toggle container classes are gone; every toggle is now
   just a .seg-toggle and every button a direct child styled uniformly. */
.pricing-controls {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  width: min(27rem, 100%);
  margin: 0 auto 2rem;
}
.seg-field {
  display: flex;
  align-items: center;
  gap: 0.9rem;
}
.seg-label {
  flex: 0 0 5.25rem;
  text-align: right;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}
.seg-toggle {
  position: relative;
  display: flex;
  flex: 1;
  gap: 0;
  background: rgba(26, 23, 32, 0.06);
  border: var(--hairline);
  border-radius: 999px;
  padding: 0.25rem;
}
/* The sliding coral "thumb" — a single element that glides between the two
   options rather than each button hard-swapping its background. JS measures the
   active button and sets the thumb's transform/size (see positionThumbs), so
   the transition is one continuous, fluid slide. */
.seg-thumb {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  border-radius: 999px;
  background: linear-gradient(135deg, #ffab85, var(--coral) 58%, #ff7a54);
  box-shadow: 0 6px 18px -6px rgba(255, 137, 105, 0.65);
  pointer-events: none;
  opacity: 0;
  transition: none;
}
.seg-toggle.thumbs-ready .seg-thumb {
  opacity: 1;
  transition: transform 0.36s cubic-bezier(0.5, 0.05, 0.2, 1),
    width 0.36s cubic-bezier(0.5, 0.05, 0.2, 1),
    height 0.36s cubic-bezier(0.5, 0.05, 0.2, 1);
}
.seg-toggle > button {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--muted);
  background: none;
  border: none;
  border-radius: 999px;
  padding: 0.55em 0.6em;
  min-height: 42px;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.28s ease;
}
.seg-toggle > button.active {
  color: #100e1b;
}
.pricing-controls-hint {
  margin: 0.5rem 0 0;
  text-align: center;
  font-size: 0.8rem;
  color: var(--muted);
}
.pricing-controls-hint strong {
  color: var(--mint);
}
/* ============ Under the hood (tech stack) ============
   Replaces the old founder-story section on the homepage — that story
   lives on /about.html now (linked here), reachable from the nav too, so
   it isn't duplicated in full on the homepage. This section does a
   different, more conversion-relevant job: proving the product is real,
   live infrastructure, not a prototype — same content as the investor
   pitch's own "Under the hood" section, logo marks included. */
.stack-lede {
  text-align: center;
  color: var(--muted);
  max-width: 52ch;
  margin: -1.5rem auto 2.5rem;
}
.stack-lede a {
  color: var(--mint);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.stack-lede a:hover {
  color: var(--coral-text);
}
.stack-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem;
  max-width: 720px;
  margin: 0 auto;
}
.stack-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.65rem;
  width: 132px;
  padding: 1.2rem 0.6rem;
  border-radius: 16px;
  transition: background 0.15s ease, transform 0.15s ease;
}
.stack-item:hover {
  background: rgba(255, 137, 105, 0.08);
  transform: translateY(-2px);
}
.stack-mark {
  width: 48px;
  height: 48px;
  border-radius: 14px;
  background: #fff;
  border: var(--hairline);
  box-shadow: var(--shadow-card);
  display: flex;
  align-items: center;
  justify-content: center;
}
.stack-mark svg {
  width: 25px;
  height: 25px;
}
.stack-wordmark {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.72rem;
  color: var(--ghost);
  letter-spacing: -0.01em;
}
.stack-item span {
  font-size: 0.76rem;
  font-weight: 600;
  color: var(--muted);
  text-align: center;
  line-height: 1.32;
}
.stack-explainer {
  max-width: 62ch;
  margin: 2.2rem auto 0;
  color: var(--muted);
  font-size: 0.92rem;
  line-height: 1.6;
  text-align: center;
}

/* ============ Trust strip ============
   A slow, seamless marquee of the real connections — every claim true to what
   the product actually does: WhatsApp is the channel, Stripe secures payments,
   Revolut is the payback deep-link, and the business is GDPR-compliant and
   UK-registered. Two identical sets scroll -50% for a seamless loop. */
.trust-strip {
  position: relative;
  overflow: hidden;
  padding: 1.35rem 0;
  border-top: var(--hairline);
  border-bottom: var(--hairline);
  background: rgba(26, 23, 32, 0.015);
  -webkit-mask: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.trust-track {
  display: flex;
  width: max-content;
  animation: trust-scroll 34s linear infinite;
}
.trust-strip:hover .trust-track {
  animation-play-state: paused;
}
.trust-set {
  display: flex;
  align-items: center;
  gap: clamp(2rem, 5vw, 4rem);
  padding-right: clamp(2rem, 5vw, 4rem);
  flex-shrink: 0;
}
.trust-item {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.01em;
  color: var(--muted);
  white-space: nowrap;
}
.trust-item svg {
  width: 22px;
  height: 22px;
  color: var(--coral);
  flex-shrink: 0;
}
/* Each real brand logo sits on a clean light chip so it reads on the dark strip
   regardless of the logo's own colour (a dark logo would otherwise vanish). */
.trust-logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 30px;
  padding: 0 0.6rem;
  background: #fff;
  border-radius: 7px;
  flex-shrink: 0;
}
.trust-logo img {
  height: 17px;
  width: auto;
  display: block;
}
/* Wordmark logos (e.g. Stripe) are wide, so ride shorter than icon glyphs. */
.trust-logo--word img {
  height: 12px;
}
/* Smaller chip for inline use in the signup reassurance line. */
.trust-logo--inline {
  height: 20px;
  padding: 0 0.45rem;
  vertical-align: middle;
  margin: 0 0.4em 0 0.1em;
}
.trust-logo--inline img {
  height: 11px;
}
.trust-logo--inline.trust-logo--word img {
  height: 9px;
}
.trust-item strong {
  color: var(--ghost);
  font-weight: 600;
}
@keyframes trust-scroll {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}
@media (prefers-reduced-motion: reduce) {
  .trust-track {
    animation: none;
    flex-wrap: wrap;
    justify-content: center;
    width: 100%;
  }
  .trust-set:last-child {
    display: none;
  }
}

/* ============ Notifications showcase ============
   A phone lock-screen where Ghost Tenant notifications stack in as the section
   scrolls into view (the .notify-stack is registered as a staggered reveal
   group, so each notification eases up one after another). */
.notify-showcase {
  position: relative;
  overflow: hidden;
  padding: var(--space-section) clamp(1.25rem, 5vw, 3rem);
}
.notify-inner {
  max-width: var(--max-width);
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr minmax(300px, 340px);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: center;
}
.notify-copy .section-heading {
  text-align: left;
  margin-bottom: 1.25rem;
}
.notify-sub {
  color: var(--muted);
  font-size: 1.05rem;
  line-height: 1.6;
  max-width: 42ch;
}
.notify-phone-stage {
  display: flex;
  justify-content: center;
}
.phone--notify {
  width: 300px;
}
.phone-screen--lock {
  position: relative;
  padding: 3.4rem 0.85rem 1.25rem;
  background:
    radial-gradient(120% 80% at 18% 0%, rgba(255, 137, 105, 0.3), transparent 55%),
    radial-gradient(120% 90% at 92% 100%, rgba(143, 247, 199, 0.2), transparent 55%),
    linear-gradient(180deg, #17132a, #0d0b18);
}
/* A real lock screen still shows its own status icons above the big
   clock, not just below the notch. Same signal/wifi/battery glyphs as
   the WhatsApp status bar elsewhere, in the lock screen's own light text
   colour instead of dark-on-white. */
.lock-status-icons {
  position: absolute;
  top: 1.2rem;
  right: 1.1rem;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 3px;
}
.lock-status-icons svg { display: block; }
.notify-clock-wrap {
  text-align: center;
  margin-bottom: 1.4rem;
  /* This mockup is a fixed dark phone lock-screen regardless of the site's
     own light/dark theme (see .phone-screen--lock below) — an explicit
     light color, not var(--ghost) (now dark, for the page's own text). */
  color: #F1EEFF;
}
.notify-clock {
  display: block;
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 3.4rem;
  line-height: 1;
  letter-spacing: -0.02em;
}
.notify-day {
  display: block;
  margin-top: 0.35rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: rgba(241, 238, 255, 0.7);
}
.notify-stack {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.notif {
  display: flex;
  gap: 0.65rem;
  padding: 0.7rem 0.75rem;
  border-radius: 16px;
  background: rgba(20, 18, 34, 0.62);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(241, 238, 255, 0.1);
  box-shadow: 0 6px 20px -10px rgba(0, 0, 0, 0.6);
}
.notif-icon {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  background: linear-gradient(135deg, #ffab85, var(--coral) 60%, #ff7a54);
  display: flex;
  align-items: center;
  justify-content: center;
}
.notif-icon svg {
  width: 20px;
  height: 20px;
}
.notif-body {
  flex: 1;
  min-width: 0;
}
.notif-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 0.15rem;
}
.notif-app {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: rgba(241, 238, 255, 0.85);
}
.notif-when {
  font-family: var(--font-mono);
  font-size: 0.64rem;
  color: rgba(241, 238, 255, 0.5);
}
.notif-text {
  font-size: 0.82rem;
  line-height: 1.4;
  /* Fixed light text — see .notify-clock-wrap comment above. */
  color: #F1EEFF;
}
@media (max-width: 820px) {
  .notify-inner {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
  }
  .notify-copy .section-heading {
    text-align: center;
  }
  .notify-sub {
    margin-left: auto;
    margin-right: auto;
  }
}
/* On narrow screens the label stacks above its control so nothing gets
   squeezed. */
@media (max-width: 460px) {
  .seg-field {
    flex-direction: column;
    align-items: stretch;
    gap: 0.3rem;
  }
  .seg-label {
    flex: none;
    text-align: left;
    padding-left: 0.7rem;
  }
}

.receipt {
  background: var(--panel);
  border: 1px dashed rgba(26, 23, 32, 0.25);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  padding: 2.5rem 2.25rem;
  font-family: var(--font-mono);
  text-align: center;
  max-width: 360px;
  width: 100%;
}
.receipt [data-reveal] {
  opacity: 0;
  transform: translateY(24px);
}
.receipt-title { font-size: 0.95rem; letter-spacing: 0.02em; margin-bottom: 0.75rem; }
.receipt-divider { color: rgba(26, 23, 32, 0.25); margin: 0.5rem 0; font-size: 0.85rem; }
.receipt-line { font-size: 1.1rem; margin: 0.75rem 0; }
.receipt-line--muted { color: var(--muted); font-size: 0.9rem; }
.receipt-cta { margin-top: 1.5rem; width: 100%; font-family: var(--font-body); }

/* ============ SIGNUP ============ */
.signup { overflow: hidden; }
.signup-sub {
  text-align: center;
  color: var(--muted);
  max-width: 40ch;
  margin: -2rem auto 2.5rem;
}
.signup-form {
  max-width: 560px;
  margin: 0 auto;
  background: var(--panel);
  border: var(--hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: clamp(1.75rem, 4vw, 2.75rem);
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
.field { display: flex; flex-direction: column; gap: 0.5rem; flex: 1; }
.field--referral { max-width: 360px; }
.field-label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.field-hint {
  font-size: 0.8rem;
  color: var(--muted);
  line-height: 1.4;
}
.field-hint--error { color: var(--coral); }

.field-checkbox {
  display: flex;
  align-items: flex-start;
  gap: 0.6em;
  font-size: 0.85rem;
  color: var(--muted);
  line-height: 1.4;
  cursor: pointer;
}
.field-checkbox input[type="checkbox"] {
  width: 1.1em;
  height: 1.1em;
  margin-top: 0.15em;
  flex: none;
  accent-color: var(--coral);
}
.field-checkbox a { color: var(--mint); text-decoration: underline; text-underline-offset: 2px; }
.field-checkbox a:hover { color: var(--ghost); }
/* Same treatment as .field-checkbox/.content-page a — the base `a { color:
   inherit }` reset (line 130) otherwise leaves an inline link invisible
   against its own paragraph text, which is exactly what the founder-trust
   section's "Read the full story" link did before this. */
.editorial-body a { color: var(--mint); text-decoration: underline; text-underline-offset: 2px; }
.editorial-body a:hover { color: var(--coral-text); }
/* .editorial-body's own paragraphs otherwise have no margin at all outside
   .content-page scope (that's where margin-bottom is normally set) — fine
   with exactly one paragraph either side of .pull-quote (its own margin
   supplies the visual gap), but two plain paragraphs back to back run
   together with zero gap, exactly what a 3rd paragraph here exposed. */
.editorial-body p { margin-bottom: 1.25rem; }
.editorial-body p:last-child { margin-bottom: 0; }

.password-strength { display: flex; align-items: center; gap: 0.6em; margin-top: 0.35em; }
.password-strength[hidden] { display: none; }
.password-strength-bars { display: flex; gap: 0.3em; flex: 1; }
.password-strength-bar {
  height: 4px;
  flex: 1;
  border-radius: 2px;
  background: rgba(26, 23, 32, 0.12);
  transition: background-color 0.2s ease;
}
.password-strength-label {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
  white-space: nowrap;
}
.field input {
  background: rgba(26, 23, 32, 0.06);
  border: 1px solid rgba(26, 23, 32, 0.15);
  border-radius: var(--radius-sm);
  padding: 0.75em 0.9em;
  color: var(--ghost);
}
.field input::placeholder { color: rgba(107, 100, 120, 0.6); }
.field input:focus {
  border-color: var(--mint);
  animation: input-glow-breathe 2.4s ease-in-out infinite;
}

@keyframes input-glow-breathe {
  0%, 100% { box-shadow: 0 0 0 3px rgba(143, 247, 199, 0.12); }
  50% { box-shadow: 0 0 0 6px rgba(143, 247, 199, 0.24); }
}

.flatmates-label {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
  margin-bottom: 0.25rem;
}
.flatmate-row {
  display: flex;
  gap: 1rem;
  align-items: flex-end;
  margin-bottom: 1rem;
  position: relative;
}
.signup-account-row {
  display: flex;
  gap: 1rem;
  align-items: flex-start;
  margin-bottom: 1.25rem;
}
.field--half { flex: 1 1 0; min-width: 0; }
/* Phone numbers need more room than most names — 40/60 rather than an even split */
.flatmate-row label.field--half:first-of-type { flex: 0 1 40%; }
.flatmate-row label.field--half:last-of-type { flex: 1 1 60%; }
.remove-flatmate {
  flex: 0 0 auto;
  width: 2em;
  height: 2em;
  /* The visible circle stays small and compact within the row, but the
     clickable box is expanded to the WCAG 2.5.8 minimum via min-width/
     min-height (2em ~= 35px at this font-size, under the 44px minimum). */
  min-width: 44px;
  min-height: 44px;
  border-radius: 50%;
  background: rgba(255, 137, 105, 0.15);
  color: var(--coral-text);
  font-size: 1.1rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.remove-flatmate:hover { background: rgba(255, 137, 105, 0.28); }

.phone-input-group { display: flex; gap: 0.5rem; }
.phone-country-select {
  flex: 0 0 auto;
  width: 6.5em;
  background: rgba(26, 23, 32, 0.06);
  border: 1px solid rgba(26, 23, 32, 0.15);
  border-radius: var(--radius-sm);
  padding: 0.75em 0.5em;
  color: var(--ghost);
  /* docs/ios-audit.md — anything under 16px makes iOS Safari auto-zoom
     the whole page on focus. width is in em so it grows with this,
     which is correct — the option text needs more room at 16px too. */
  font-size: 16px;
}
.phone-input-group input { flex: 1 1 auto; min-width: 0; }
.field input.input-invalid,
.phone-country-select.input-invalid {
  border-color: var(--coral);
  animation: none;
}
.field-error {
  color: var(--coral-text);
  font-size: 0.8rem;
}

.signup-submit-row { display: flex; justify-content: center; }
.signup-submit-row .btn { width: 100%; }
/* docs/checkout-ux-audit.md G4 — the last plan/price confirmation before
   the submit button that starts a real Stripe checkout. Deliberately more
   visible than .signup-reassure below it (a coral-tinted panel, not muted
   caption text) since this is the number that actually matters. */
.signup-price-recap {
  margin: 1.25rem auto 0;
  max-width: 42ch;
  padding: 0.7rem 1rem;
  border-radius: var(--radius);
  border: 1px solid rgba(255, 137, 105, 0.35);
  background: rgba(255, 137, 105, 0.08);
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--ghost);
  text-align: center;
}
.signup-price-recap:empty {
  display: none;
}

.signup-reassure {
  margin: 1rem auto 0;
  max-width: 42ch;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  line-height: 1.7;
  color: var(--muted);
  text-align: center;
}
.signup-reassure svg {
  display: inline-block;
  width: 16px;
  height: 16px;
  color: var(--mint);
  vertical-align: -0.22em;
  margin-right: 0.4em;
}
.signup-reassure strong {
  color: var(--ghost);
  font-weight: 600;
}
.signup-error {
  color: var(--coral-text);
  font-size: 0.9rem;
  text-align: center;
  background: rgba(255, 137, 105, 0.1);
  border-radius: var(--radius-sm);
  padding: 0.75em 1em;
}

/* ============ FOOTER ============ */
.site-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.25rem;
  padding: 3rem 1.5rem 4rem;
  color: var(--muted);
  font-size: 0.85rem;
}
.site-footer .ghost-glyph { color: var(--muted); }
.footer-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  font-family: var(--font-mono);
  font-size: 0.8rem;
}
.footer-links a { color: var(--muted); transition: color 0.2s ease; }
.footer-links a:hover { color: var(--ghost); }
/* The page you're already on: shown as the current page, not a live link. */
.footer-links .footer-link--current {
  color: var(--coral-text);
  font-weight: 600;
  cursor: default;
}
.footer-links span { color: rgba(107, 100, 120, 0.35); }
/* Social icons render as inert <span> (not <a>) until each account is
   genuinely live — see the HTML comment beside them. A dead/broken public
   link is worse than no link; swap span -> a href="..." target="_blank"
   rel="noopener" per icon once its real account exists. */
.footer-socials { display: flex; align-items: center; gap: 0.9rem; }
.footer-socials a,
.footer-socials span { color: var(--muted); transition: color 0.2s ease; display: flex; }
.footer-socials a:hover { color: var(--ghost); }
.footer-socials span { opacity: 0.4; cursor: default; }
.footer-socials svg { width: 18px; height: 18px; }
/* docs/accessibility-audit.md — 0.7 alpha measured 4.3:1 against --bg,
   just under WCAG AA's 4.5:1 for normal-size text. Previously unflagged
   only because axe-core couldn't resolve a confident background color
   behind body's old multi-layer grid gradient (and silently skipped the
   check, not because it genuinely passed) — removing that background
   layer made the check resolvable and surfaced this real, pre-existing
   shortfall. 0.78 clears it with real margin (~5:1). */
.footer-company { max-width: 32rem; text-align: center; font-size: 0.78rem; color: var(--muted); margin-top: -0.75rem; line-height: 1.5; }
.footer-company a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.footer-company a:hover { color: var(--ghost); }

/* ============ Content pages (privacy/terms/about/help/contact) ============ */
.content-page {
  position: relative;
  overflow: hidden;
  max-width: none;
  /* Bottom padding deliberately smaller than top: --space-section's full
     value (up to 8.5rem) makes sense below the header, but stacked on top
     of .site-footer's own 4rem top padding it left a ~180px dead gap
     before the footer on short pages (Contact, with just a few cards) —
     no border or visual break to justify that much whitespace. Longer
     pages (About, Landlords) have enough content that the difference is
     barely noticeable. */
  padding: var(--space-section) clamp(1.25rem, 5vw, 3rem) clamp(2.5rem, 5vw, 4rem);
}
/* Narrow reading column by default — right for a text-heavy page (Help's
   FAQ list, Terms/Privacy, Contact's short info cards), where a wide
   canvas with a handful of short cards on it reads as empty and adrift,
   not spacious. About and Landlords opt into a genuinely wide layout
   instead (.content-page--wide, set on <main>, matching the homepage's
   own --max-width) — they have enough real content (grids, a hero
   visual, multiple sections) to actually use that space rather than
   just stretching gaps between a few boxes. */
.content-page-inner {
  position: relative;
  z-index: 1;
  max-width: 720px;
  margin: 0 auto;
  animation: content-fade-up 0.7s ease both;
}
.content-page--wide .content-page-inner {
  max-width: var(--max-width);
}
.how-grid,
.card-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.card-grid {
  display: grid;
  gap: 1.5rem;
}
.content-page .how-grid,
.content-page .card-grid {
  margin-top: 1.75rem;
  margin-bottom: 1.75rem;
}
.card-grid > .info-card,
.card-grid > .contact-card,
.card-grid > .faq-item {
  margin-bottom: 0;
}
@media (max-width: 720px) {
  .how-grid,
  .card-grid { grid-template-columns: 1fr; }
}
@keyframes content-fade-up {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--muted);
  margin-bottom: 2.5rem;
}
.back-link:hover { color: var(--ghost); }
/* .eyebrow is inline-flex sitewide (fine inside .hero-copy, its only other
   home, which has no other inline sibling before it) — inside a content
   page it follows .back-link, another inline-flex element, so without
   this the two pills shared one line instead of the eyebrow starting its
   own. display:flex (block-level) + width:fit-content keeps the exact
   same pill sizing while actually starting a new line. */
.content-page .eyebrow { display: flex; width: fit-content; margin-top: 0.5rem; }
.content-page h1 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(2.2rem, 5.5vw, 3.2rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-bottom: 0.75rem;
  color: var(--ghost);
}
.page-subtitle {
  color: var(--muted);
  font-size: 1rem;
  margin-bottom: 3rem;
  max-width: 640px;
}
.content-page h2 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.4rem;
  margin: 2.5rem 0 1rem;
  color: var(--ghost);
}
/* max-width, not a second narrower container: keeps every paragraph at a
   comfortable reading measure now that .content-page-inner itself is wide
   (matches the homepage's --max-width) without re-introducing a second,
   independently-positioned column — a plain paragraph and a wide card sit
   in the exact same container, the paragraph just doesn't stretch to fill
   all of it. A no-op for any paragraph already inside a narrower card. */
.content-page p {
  color: var(--muted);
  line-height: 1.7;
  margin-bottom: 1.25rem;
  max-width: 720px;
}
/* Standalone cards (Help's FAQ list, Terms/Privacy's sections) get the
   same reading-width cap — but only when they're NOT already a grid cell
   (.content-page-inner's direct children only; a card inside .card-grid
   or .editorial-split is sized by its own track and shouldn't be capped
   again on top of that), AND only on the narrow (default) content pages.
   On a .content-page--wide page (About/Landlords) a standalone highlight
   card — Landlords' "The move-out condition report" — sits directly
   below a full-width how-grid row; capping it to 720px there made it
   noticeably narrower than the cards immediately above it, sitting
   short and alone rather than matching the row it's part of. */
.content-page-inner > .faq-item,
.content-page-inner > .info-card,
.content-page-inner > .contact-card {
  max-width: 720px;
}
.content-page--wide .content-page-inner > .faq-item,
.content-page--wide .content-page-inner > .info-card,
.content-page--wide .content-page-inner > .contact-card {
  max-width: none;
}
/* Now that the card itself is genuinely full-width (above), its own
   paragraph text was still capped by the general .content-page p rule
   further down this file — correct for a card sized to match its own
   text, but this card is deliberately sized to match the how-grid row
   above it instead, so a 720px-capped paragraph inside a ~1120px card
   left a wide dead strip of empty card down the right side. Let the
   text use the card's actual width instead of shrinking the card to
   match the text — the card's job here is to match its neighbours, not
   to hug its own content. */
.content-page--wide .content-page-inner > .info-card p {
  max-width: none;
}
/* A centered CTA line/button row (Landlords' closing buttons, the "maths
   is one-sided" line) is text-align: center — which only centers content
   WITHIN the element's own box. That box still had .content-page p's
   720px cap and no auto margins, so on a wide page it sat flush against
   the left edge at its own width, centering its text within that
   left-shifted box rather than within the actual page — confirmed live,
   the exact "buttons over to the side" symptom. margin: auto centers the
   (still width-capped, where relevant) box itself first. */
.content-page-centered {
  text-align: center;
  margin-left: auto;
  margin-right: auto;
}
.content-page-centered--lede { margin-top: 2.25rem; margin-bottom: 0.5rem; text-wrap: balance; }
.content-page p strong { color: var(--ghost); font-weight: 600; }
.content-page a { color: var(--mint); text-decoration: underline; text-underline-offset: 2px; }
.content-page a:hover { color: var(--ghost); }
.content-page a.btn--coral, .content-page a.btn--coral:hover { text-decoration: none; color: #1a0e08; }
.content-page a.btn--ghost, .content-page a.btn--ghost:hover { text-decoration: none; color: var(--ghost); }
.content-page ul { margin: 0 0 1.25rem; padding-left: 1.25rem; color: var(--muted); line-height: 1.7; }
.content-page li { margin-bottom: 0.5rem; }
/* Sub-groups within a single card (e.g. help.html's command reference,
   grouped into "Everyday" / "Money" / "Admin only" instead of one flat
   list) — deliberately smaller and quieter than .faq-item's own h2, so the
   card's actual heading still reads as the primary label. */
.content-page h3.group-label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--coral-text);
  margin: 1.5rem 0 0.6rem;
}
.content-page h3.group-label:first-child { margin-top: 0; }

/* .how-card is normally faded in by script.js's scroll-reveal (index.html
   only) — content pages don't load that script, so reused here as a plain
   static card, opacity/transform reset back to visible. */
.content-page .how-card { opacity: 1; transform: none; }
.content-page .how-grid { margin-bottom: 1.5rem; }

/* Shared card treatment for every content page (help FAQs, privacy/terms
   sections, contact methods, the about-page CTA) — one visual language
   instead of some pages using cards and others just floating bare
   h2/p pairs on the page background. */
.faq-item,
.contact-card,
.info-card {
  position: relative;
  background:
    radial-gradient(120% 90% at 0% 0%, rgba(255, 137, 105, 0.09), transparent 46%),
    radial-gradient(120% 90% at 100% 0%, rgba(184, 156, 255, 0.08), transparent 48%),
    var(--panel);
  border: 1px solid rgba(26, 23, 32, 0.10);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: 1.9rem;
  margin-bottom: 1.5rem;
  transition: scale 0.4s cubic-bezier(0.2, 0.7, 0.2, 1),
    translate 0.4s cubic-bezier(0.2, 0.7, 0.2, 1), box-shadow 0.4s ease,
    border-color 0.4s ease;
}
.faq-item::after,
.contact-card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(135deg, rgba(255, 137, 105, 0.55), rgba(184, 156, 255, 0.4) 45%, transparent 72%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0.5;
  transition: opacity 0.4s ease;
  pointer-events: none;
}
@media (hover: hover) {
  .faq-item:hover,
  .contact-card:hover {
    scale: 1.015;
    translate: 0 -5px;
    border-color: rgba(255, 137, 105, 0.45);
    box-shadow: 0 34px 68px -28px rgba(0, 0, 0, 0.78), 0 0 44px -14px rgba(255, 137, 105, 0.28);
  }
  .faq-item:hover::after,
  .contact-card:hover::after {
    opacity: 1;
  }
}
.faq-item h2,
.contact-card h2,
.info-card h2 {
  margin-top: 0;
}
.faq-item p:last-child,
.contact-card p:last-child,
.info-card p:last-child,
.info-card ul:last-child {
  margin-bottom: 0;
}
.contact-card a { font-family: var(--font-mono); font-size: 1.05rem; }
.contact-card p.contact-desc { color: var(--muted); font-size: 0.92rem; margin-top: 0.5rem; margin-bottom: 0; }

/* A single accent-colored line breaking up a long run of plain paragraphs
   (about.html) — same idea as .pricing-lede's display-font treatment,
   scaled down and left-aligned to sit naturally inside body copy. */
.pull-quote {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.3rem, 3vw, 1.6rem);
  line-height: 1.4;
  color: var(--coral-text);
  border-left: 2px solid rgba(255, 137, 105, 0.4);
  padding-left: 1.25rem;
  margin: 2rem 0;
  max-width: 680px;
}

/* ============ EDITORIAL CONTENT LAYOUT (about + landlords) ============
   These break the "card after card" monotony: a large standfirst lede, a
   two-column prose+aside split, scannable feature chips, stat callouts,
   numbered steps, a move-out report mock, and a distinct privacy banner. */

/* Large standfirst intro — sets the scene in display type, no box. */
.content-page .lede {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.2rem, 2.5vw, 1.55rem);
  line-height: 1.5;
  color: var(--ghost);
  margin: 0 0 2.5rem;
  max-width: 640px;
}

/* Two-column editorial split: main prose column + a narrower aside. */
.editorial-split {
  display: grid;
  grid-template-columns: 1.55fr 1fr;
  gap: 1.75rem;
  align-items: stretch;
  margin: 1.75rem 0;
}
.editorial-split .info-card { margin-bottom: 0; }
.editorial-split .editorial-body > p:last-child { margin-bottom: 0; }
/* The hero pairing (About's founder card, Landlords' report-mock) reuses
   .editorial-split's own grid — same visual language as the rest of the
   page — just align-items: start instead of stretch/center. Centering
   looked like real misalignment in practice: the visual card's top edge
   landed lower (or higher, depending which column was taller) than the
   eyebrow badge beside it instead of lining up with it. Top-aligning both
   columns gives a single shared top edge, which is what actually reads
   as "in line." (The container-width mismatch this rule used to also
   fight is gone now that .content-page-inner itself is the one shared
   width — see its own comment.) */
.hero-split { align-items: start; margin-top: 0.5rem; }
.hero-copy-block { display: flex; flex-direction: column; }

/* A compact aside card (founder bio, at-a-glance facts). Stretches to
   match its sibling text column's height (.editorial-split's
   align-items: stretch) and centers its own content within that height,
   so a shorter card (fewer facts) doesn't strand its content at the top
   with a dead gap below — instead it just centers with more breathing
   room, which reads as intentional at any content length. */
.aside-card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  background:
    radial-gradient(120% 90% at 100% 0%, rgba(184, 156, 255, 0.10), transparent 52%),
    var(--panel);
  border: 1px solid rgba(26, 23, 32, 0.10);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: 1.6rem;
}
.aside-card .aside-label {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--coral-text);
  margin: 0 0 0.75rem;
}
.aside-card .aside-name { color: var(--ghost); font-weight: 600; font-size: 1.05rem; margin: 0 0 0.15rem; }
.aside-photo-row { display: flex; align-items: center; gap: 0.9rem; }
.aside-photo-row .aside-label { margin: 0 0 0.2rem; }
.aside-photo-row .aside-name { margin: 0; }
.aside-photo {
  width: 62px;
  height: 62px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  border: 2px solid rgba(255, 137, 105, 0.4);
  box-shadow: var(--shadow-card);
}
.aside-card dl { margin: 0.9rem 0 0; display: grid; gap: 0.55rem; }
.aside-card dt { font-family: var(--font-mono); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--coral-text); }
.aside-card dd { margin: 0.1rem 0 0; color: var(--muted); font-size: 0.9rem; line-height: 1.45; }
/* LinkedIn's own brand blue, deliberately not this page's lavender/coral
   palette — the point is for it to read as "LinkedIn" at a glance, not
   blend into the rest of the card. Scoped as .aside-card .linkedin-link
   (not just .linkedin-link) so it actually beats .content-page a's mint —
   a plain .linkedin-link class alone loses that specificity fight and
   silently renders green, which is exactly the bug this fixes. */
.aside-card .linkedin-link { display: inline-flex; align-items: center; gap: 0.35rem; color: #0A66C2; font-weight: 600; }
.aside-card .linkedin-link:hover { color: #4996E0; }
.linkedin-link .linkedin-icon { width: 15px; height: 15px; flex-shrink: 0; }

/* Scannable feature chips — replaces a wall-of-text paragraph. Fixed
   3 columns, not auto-fit: there are exactly 6 of these (about.html), and
   auto-fit picks its column count from available width alone, with no
   idea how many items actually exist — on the wide .content-page--wide
   container it landed on 4 columns, which divides 6 items into a full
   row of 4 then an orphaned row of 2 sitting alone on the left with dead
   space beside them (confirmed live, exactly the "boxes sitting by
   themselves unevenly" complaint). 3 columns divides 6 evenly with no
   remainder — two full, symmetrical rows every time this list has a
   multiple-of-3 (or multiple-of-2, given the @media step below) count. */
.feature-chips {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.9rem;
}
@media (max-width: 900px) {
  .feature-chips { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .feature-chips { grid-template-columns: 1fr; }
}
.feature-chip {
  display: flex;
  gap: 0.8rem;
  align-items: flex-start;
  padding: 1.05rem 1.15rem;
  background: rgba(26, 23, 32, 0.03);
  border: 1px solid rgba(26, 23, 32, 0.09);
  border-radius: var(--radius-md);
}
.feature-chip svg { width: 22px; height: 22px; color: var(--coral); flex-shrink: 0; margin-top: 1px; }
.feature-chip strong { display: block; color: var(--ghost); font-size: 0.95rem; font-weight: 600; margin-bottom: 0.15rem; }
.feature-chip span { color: var(--muted); font-size: 0.84rem; line-height: 1.5; }

/* Big-number stat callouts. */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1rem;
}
.stat {
  padding: 1.25rem 1.4rem;
  background: rgba(26, 23, 32, 0.03);
  border: 1px solid rgba(26, 23, 32, 0.09);
  border-radius: var(--radius-md);
}
.stat b {
  display: block;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.7rem, 4vw, 2.3rem);
  color: var(--ghost);
  line-height: 1;
  margin-bottom: 0.45rem;
}
.stat span { color: var(--muted); font-size: 0.84rem; line-height: 1.45; }
.stat--coral b { color: var(--coral-text); }
.stat--mint b { color: var(--mint); }

/* Numbered process steps. */
.steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.25rem;
  counter-reset: step;
}
.step {
  padding: 1.4rem 1.3rem 1.3rem;
  background: rgba(26, 23, 32, 0.03);
  border: 1px solid rgba(26, 23, 32, 0.09);
  border-radius: var(--radius-md);
}
.step::before {
  counter-increment: step;
  content: counter(step, decimal-leading-zero);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--coral-text);
}
.step h4 { font-family: var(--font-display); font-weight: 500; color: var(--ghost); font-size: 1.05rem; margin: 0.5rem 0 0.4rem; }
.step p { margin: 0; font-size: 0.88rem; color: var(--muted); line-height: 1.6; }

/* Feature spotlight — text beside a supporting visual. */
.spotlight {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: 1.9rem;
  align-items: center;
  background:
    radial-gradient(120% 90% at 0% 0%, rgba(255, 137, 105, 0.09), transparent 46%),
    radial-gradient(120% 90% at 100% 100%, rgba(184, 156, 255, 0.08), transparent 48%),
    var(--panel);
  border: 1px solid rgba(26, 23, 32, 0.10);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: 2rem;
}
.spotlight h2 { margin-top: 0 !important; }
.spotlight .spotlight-body > p:last-child { margin-bottom: 0; }

/* The move-out condition report mock. */
.report-mock {
  background: var(--panel);
  border: 1px solid rgba(26, 23, 32, 0.1);
  border-radius: var(--radius-md);
  padding: 1.25rem 1.35rem;
  font-family: var(--font-mono);
}
.report-mock-title { color: var(--ghost); font-weight: 600; font-size: 0.78rem; margin-bottom: 0.35rem; }
.report-mock-sub { color: var(--muted); font-size: 0.68rem; margin-bottom: 0.9rem; }
.report-row { display: flex; justify-content: space-between; align-items: center; gap: 0.75rem; padding: 0.55rem 0; border-top: 1px solid rgba(26, 23, 32, 0.07); }
.report-room { color: var(--muted); font-size: 0.8rem; }
.verdict { font-size: 0.68rem; padding: 0.18rem 0.6rem; border-radius: 999px; white-space: nowrap; }
.verdict--ok { background: rgba(143, 247, 199, 0.14); color: var(--mint); }
.verdict--wear { background: rgba(107, 100, 120, 0.16); color: var(--muted); }
.verdict--new { background: rgba(255, 137, 105, 0.16); color: var(--coral-text); }
.report-dates { margin-top: 0.95rem; padding-top: 0.75rem; border-top: 1px solid rgba(26, 23, 32, 0.07); color: var(--muted); font-size: 0.68rem; line-height: 1.5; }

/* Privacy-boundary banner — mint accent, distinct from the coral cards. */
.reassure-banner {
  display: flex;
  gap: 1.15rem;
  align-items: flex-start;
  padding: 1.7rem 1.9rem;
  border: 1px solid rgba(143, 247, 199, 0.28);
  border-radius: var(--radius-lg);
  background: radial-gradient(130% 130% at 0% 0%, rgba(143, 247, 199, 0.09), transparent 55%), var(--panel);
}
.reassure-banner > svg { width: 30px; height: 30px; color: var(--mint); flex-shrink: 0; margin-top: 2px; }
.reassure-banner h2 { margin-top: 0 !important; }
.reassure-banner p:last-child { margin-bottom: 0; }

.feature-chips,
.spotlight,
.steps,
.stat-row {
  margin: 1.75rem 0;
}

@media (max-width: 760px) {
  .editorial-split,
  .spotlight { grid-template-columns: 1fr; }
  .steps { grid-template-columns: 1fr; }
}

.content-page .glow--1 { width: 40vw; height: 40vw; max-width: 480px; max-height: 480px; top: -8%; right: -10%; }

/* ============ HELP PAGE visual lift ============ */
/* Iconed section dividers give the long FAQ clear, scannable structure and
   outrank the individual question cards. */
.help-page .help-section {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: clamp(1.5rem, 3.6vw, 1.9rem);
  margin: 3rem 0 1.3rem;
  padding-top: 1.9rem;
  border-top: 1px solid rgba(26, 23, 32, 0.08);
}
.help-page .help-section:first-of-type { border-top: none; padding-top: 0; margin-top: 1.5rem; }
.help-page .help-section svg {
  width: 36px;
  height: 36px;
  padding: 7px;
  flex-shrink: 0;
  color: var(--coral);
  background: rgba(255, 137, 105, 0.12);
  border: 1px solid rgba(255, 137, 105, 0.25);
  border-radius: 10px;
}
/* Questions sit one rung below the section headings. */
.help-page .faq-item h2 { font-size: 1.12rem; }

/* Command reference rendered as scannable rows with a monospace command pill. */
.help-page .cmd-list {
  list-style: none;
  padding: 0;
  margin: 0.5rem 0 1.25rem;
  display: grid;
  gap: 0.5rem;
}
.help-page .cmd-list li {
  margin: 0;
  padding: 0.7rem 0.9rem;
  background: rgba(26, 23, 32, 0.03);
  border: 1px solid rgba(26, 23, 32, 0.08);
  border-radius: var(--radius-md);
  color: var(--muted);
  line-height: 1.6;
}
.help-page .cmd-list li strong {
  font-family: var(--font-mono);
  font-size: 0.82em;
  font-weight: 600;
  color: var(--coral-text);
  background: rgba(255, 137, 105, 0.12);
  border: 1px solid rgba(255, 137, 105, 0.22);
  padding: 0.14em 0.5em;
  border-radius: 6px;
  margin-right: 0.15em;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .content-page-inner { animation: none; }
}

.not-found-inner {
  text-align: center;
  max-width: 480px;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 2rem;
}
.ghost-glyph--large { width: 96px; height: 96px; color: var(--coral); margin-bottom: 1.5rem; }
.not-found-eyebrow {
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--mint);
  letter-spacing: 0.08em;
  margin-bottom: 0.75rem;
}
.not-found-inner .page-subtitle { margin-bottom: 2rem; }

/* ============ Scroll-reveal base state ============ */
[data-reveal] { will-change: transform, opacity; }

/* ============ Ambient glow shapes ============ */
/* z-index: -1 so glows always sit behind normal-flow content in their
   stacking context (.problem/.pricing/.signup are position:relative via
   `main > section`) — no per-section content elevation needed. */
.glow {
  position: absolute;
  border-radius: 50%;
  filter: blur(70px);
  pointer-events: none;
  z-index: -1;
  will-change: transform, translate, scale;
  /* Slow ambient drift so the background feels alive rather than static.
     Uses the independent translate/scale properties (not `transform`) so it
     composes cleanly with the glows that use `transform: translateX(-50%)`
     for centering, instead of overriding them. */
  animation: glow-drift 28s ease-in-out infinite;
}
/* Offsets so the blobs never move in lockstep — reads as organic, not a loop. */
.glow--2 { animation-duration: 37s; animation-delay: -11s; }
.glow--coral { animation-duration: 32s; animation-delay: -6s; }
.glow--mint { animation-duration: 41s; animation-delay: -18s; }
@keyframes glow-drift {
  0%,
  100% { translate: 0 0; scale: 1; }
  30% { translate: 3.5% -4%; scale: 1.09; }
  55% { translate: -2% 3.5%; scale: 0.94; }
  80% { translate: -4% -1.5%; scale: 1.04; }
}
@media (prefers-reduced-motion: reduce) {
  .glow { animation: none; }
}
/* docs/ios-audit.md — a real GPU/battery cost on phones, independent of
   the reduced-motion preference most users won't have set, and this runs
   on every single marketing page (the homepage hero alone has 7). Lighter
   blur radius (the single biggest cost factor for this effect) rather
   than removing the animation outright — keeps the ambient feel, cuts
   the compositing cost. */
@media (max-width: 480px) {
  .glow { filter: blur(38px); }
}
.glow--lavender { background: radial-gradient(circle, rgba(26, 23, 32, 0.22), rgba(26, 23, 32, 0) 70%); }
.glow--coral { background: radial-gradient(circle, rgba(255, 137, 105, 0.20), rgba(255, 137, 105, 0) 70%); }

.hero-scroll .glow--1 { width: 46vw; height: 46vw; max-width: 620px; max-height: 620px; top: 6%; left: -8%; }
.hero-scroll .glow--2 { width: 34vw; height: 34vw; max-width: 480px; max-height: 480px; bottom: 8%; right: -6%; }
.problem .glow--1 { width: 32vw; height: 32vw; max-width: 440px; max-height: 440px; top: -6%; right: -8%; }
.pricing .glow--1 { width: 40vw; height: 40vw; max-width: 520px; max-height: 520px; top: -10%; left: 50%; transform: translateX(-50%); }
.dashboard-feature .glow--1 { width: 36vw; height: 36vw; max-width: 480px; max-height: 480px; top: -14%; right: -10%; }
.signup .glow--1 { width: 34vw; height: 34vw; max-width: 460px; max-height: 460px; bottom: -12%; left: -8%; }

/* ============ Intro splash ============ */
/* Plays on every full page load, unconditionally — deliberately not
   gated behind sessionStorage/"seen before" logic. That once-per-session
   version made this untestable in practice (a developer reloading to
   check a change would almost never see it, since the flag from the
   previous load was still set) and real visitors get the same fresh
   entrance each time they arrive, which is a reasonable trade for a
   ~4s moment, not an annoyance. fill-mode: both keeps the overlay fully
   opaque for the hold before the fade-out keyframe starts, then removes
   it from hit-testing and the accessibility tree at the very end via the
   (non-interpolated, so it just snaps) visibility/pointer-events
   properties in the `to` state. */
.intro-splash {
  display: flex;
  flex-direction: column;
  position: fixed;
  inset: 0;
  z-index: 300;
  align-items: center;
  justify-content: center;
  gap: 1.25rem;
  overflow: hidden;
  background: var(--bg);
  animation: intro-splash-out 0.6s ease 3.4s both;
}
/* Same drifting brand aurora as the site (body::before/::after), rendered
   as the splash's own layer so the opening screen feels as alive and lit as
   the rest of the site rather than a flat black hold. */
.intro-splash::before,
.intro-splash::after {
  content: "";
  position: absolute;
  inset: -30%;
  z-index: 0;
  pointer-events: none;
}
.intro-splash::before {
  background:
    radial-gradient(38% 44% at 18% 20%, rgba(255, 137, 105, 0.4), transparent 68%),
    radial-gradient(42% 46% at 84% 14%, rgba(168, 143, 255, 0.36), transparent 70%),
    radial-gradient(48% 52% at 74% 80%, rgba(143, 247, 199, 0.26), transparent 72%),
    radial-gradient(40% 44% at 22% 84%, rgba(255, 110, 92, 0.3), transparent 70%);
  filter: blur(52px) saturate(1.3);
  animation: aurora-drift 36s ease-in-out infinite alternate;
  will-change: transform;
}
.intro-splash::after {
  background:
    radial-gradient(36% 40% at 60% 32%, rgba(255, 171, 133, 0.32), transparent 66%),
    radial-gradient(40% 44% at 30% 62%, rgba(184, 156, 255, 0.28), transparent 70%);
  filter: blur(64px);
  mix-blend-mode: screen;
  animation: aurora-drift-2 48s ease-in-out infinite alternate;
  will-change: transform;
}
.intro-splash-run-stage,
.intro-splash-tagline { position: relative; z-index: 1; }
.intro-splash-word {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
  font-size: clamp(2rem, 9vw, 3rem);
  color: var(--ghost);
  text-shadow: 0 0 34px rgba(255, 137, 105, 0.3);
  opacity: 0;
  animation: intro-word-in 0.6s ease 0.5s both;
  position: relative;
  z-index: 1;
}
/* The wordmark lands first, then the ghost itself sprints across a small
   scene on stick legs and arms and vanishes into the house's doorway —
   a literal little scamper home, in place of the previous abstract orbit.
   Three layers of animation, each independent so they never fight over
   the same CSS property: the outer .intro-run-ghost owns horizontal
   travel (a `left` keyframe) plus the final duck-and-shrink into the
   door; .intro-run-ghost-bob (a child) owns the vertical running bob
   (transform: translateY); each <g class="run-ghost-limb"> owns its own
   rotate() around its shoulder/hip transform-origin, phase-offset in
   pairs for a scissoring run cycle exactly like the old orbit trail's
   animation-delay trick. */
.intro-splash-run-stage {
  width: min(88vw, 460px);
  height: 118px;
  margin-top: 0.25rem;
}
.intro-run-ground {
  position: absolute;
  left: 4%;
  right: 4%;
  bottom: 10px;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(26, 23, 32, 0.22) 20%, rgba(26, 23, 32, 0.22) 80%, transparent);
}
.intro-run-house {
  position: absolute;
  right: 2%;
  bottom: 10px;
  width: 30%;
  height: auto;
  color: var(--ghost);
  filter: drop-shadow(0 0 16px rgba(255, 137, 105, 0.15));
}
.run-house-roof,
.run-house-body {
  fill: rgba(26, 23, 32, 0.07);
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linejoin: round;
}
.run-house-window { fill: rgba(26, 23, 32, 0.16); stroke: currentColor; stroke-width: 2; }
/* The doorway glows warm right as the ghost reaches it — light spilling
   out as it lets itself in — then settles, timed to line up with
   run-ghost-travel's own 78-100% "arriving" window below. */
.run-house-door {
  fill: var(--bg);
  stroke: currentColor;
  stroke-width: 2;
  animation: run-house-door-glow 2.1s ease 0.5s both;
}
@keyframes run-house-door-glow {
  0%, 78% { fill: var(--bg); }
  90% { fill: rgba(255, 137, 105, 0.55); }
  100% { fill: rgba(255, 137, 105, 0.2); }
}

.intro-run-ghost {
  position: absolute;
  bottom: 10px;
  left: -14%;
  width: 52px;
  height: 52px;
  margin-left: -26px;
  /* Solid brand coral (not the body-text --ghost token) — same reasoning
     as .ghost-glyph: keeps the mascot on-brand and visible independent of
     whatever --ghost currently means for text, and matches its original
     single-flat-color style (body, limbs and brows all one color, no
     separate outline). */
  color: var(--coral);
  filter: drop-shadow(0 0 14px rgba(255, 137, 105, 0.35));
  animation: run-ghost-travel 2.1s cubic-bezier(0.45, 0.05, 0.4, 0.95) 0.5s both;
}
@keyframes run-ghost-travel {
  0%   { left: -14%; opacity: 0; transform: scale(1); }
  6%   { opacity: 1; }
  78%  { left: 62%; opacity: 1; transform: scale(1); }
  90%  { left: 68%; opacity: 1; transform: scale(0.78); }
  100% { left: 71%; opacity: 0; transform: scale(0.42); }
}
.intro-run-ghost-bob {
  animation: run-ghost-bob 0.26s ease-in-out 0.5s infinite;
}
@keyframes run-ghost-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}
.run-ghost-svg { width: 100%; height: 100%; display: block; overflow: visible; }
.run-ghost-body { fill: currentColor; }
.run-ghost-limb line {
  stroke: currentColor;
  stroke-width: 5;
  stroke-linecap: round;
}
.run-ghost-limb { animation: run-limb-swing 0.26s ease-in-out infinite; }
.run-ghost-arm-back  { transform-origin: 18px 50px; animation-delay: 0.63s; }
.run-ghost-leg-back  { transform-origin: 40px 80px; animation-delay: 0.5s; }
.run-ghost-leg-front { transform-origin: 60px 80px; animation-delay: 0.63s; }
.run-ghost-arm-front { transform-origin: 82px 50px; animation-delay: 0.5s; }
@keyframes run-limb-swing {
  0%, 100% { transform: rotate(-26deg); }
  50% { transform: rotate(26deg); }
}
/* Face: eyebrows give it a determined little "going for it" look, each
   eye gets a glossy highlight dot plus its own blink on a lazy, offset
   loop (real characters don't blink in sync), and the mouth pants along
   with the leg cadence so it visibly reads as effort, not a static
   sticker glued onto a running body. */
.run-ghost-brow line { stroke: currentColor; stroke-width: 3; stroke-linecap: round; }
.run-ghost-eye { animation: run-ghost-blink 1.6s ease-in-out infinite; }
.run-ghost-eye--left { transform-origin: 36px 46px; }
.run-ghost-eye--right { transform-origin: 64px 46px; animation-delay: 0.35s; }
@keyframes run-ghost-blink {
  0%, 85%, 100% { transform: scaleY(1); }
  90% { transform: scaleY(0.12); }
}
.run-ghost-mouth {
  animation: run-ghost-pant 0.34s ease-in-out infinite;
  transform-origin: 50px 59px;
}
@keyframes run-ghost-pant {
  0%, 100% { transform: scale(1, 1); }
  50% { transform: scale(0.72, 1.15); }
}
/* Three dust puffs kicked up behind the back foot, same "fixed positive
   delay against one shared keyframe" trick as the old orbit trail used —
   each one is a full lap behind the last, so the loop always shows a
   little cloud mid-fade, not three puffs popping in unison. */
.intro-run-dust {
  position: absolute;
  left: 6px;
  bottom: 4px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(26, 23, 32, 0.5), rgba(26, 23, 32, 0) 70%);
  animation: run-ghost-dust 0.5s ease-out infinite;
  opacity: 0;
}
.intro-run-dust--1 { animation-delay: 0.5s; }
.intro-run-dust--2 { animation-delay: 0.67s; left: 2px; bottom: 8px; width: 11px; height: 11px; }
.intro-run-dust--3 { animation-delay: 0.83s; left: -2px; bottom: 2px; width: 16px; height: 16px; }
@keyframes run-ghost-dust {
  0% { opacity: 0; transform: translate(0, 0) scale(0.3); }
  20% { opacity: 0.6; }
  100% { opacity: 0; transform: translate(-20px, -8px) scale(1.4); }
}

.intro-splash-tagline {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  color: var(--muted);
  opacity: 0;
  /* Right after the wordmark, not timed to land just before the splash
     fades out — it needs to actually be readable for most of the hold,
     not flash on for a fraction of a second at the very end. */
  animation: intro-word-in 0.55s ease 0.9s both;
}
/* EOMI Group logo + name — pinned to the bottom edge of the splash screen
   itself (the fixed, full-viewport .intro-splash is its containing
   block), independent of the wordmark/ghost/tagline group centered above
   it. Deliberately NOT a flex child of that centered stack — it needs to
   sit at the bottom of the whole screen, not just below the tagline.
   left/right + margin auto (not left:50%/transform) so it doesn't fight
   the intro-word-in keyframe's own transform: translateY. */
.intro-splash-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 2.5rem;
  width: fit-content;
  margin: 0 auto;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.02em;
  color: rgba(107, 100, 120, 0.65);
  opacity: 0;
  z-index: 1;
  /* Same reasoning as the tagline above — appears early (right after it)
     so it's visible for most of the splash's hold, not just an instant
     before the fade-out starts. */
  animation: intro-word-in 0.55s ease 1.1s both;
}
.intro-splash-brand img { width: 22px; height: 22px; opacity: 0.85; }
@keyframes intro-word-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes intro-splash-out {
  0% { opacity: 1; }
  100% { opacity: 0; visibility: hidden; pointer-events: none; }
}
/* No separate "page fades in underneath" animation here — deliberately.
   A previous version put an opacity 0→1 animation on <body> itself,
   timed to start as the splash faded out, meant to read as one
   continuous crossfade. Real, serious bug: CSS opacity on a parent
   multiplies into every descendant's actual rendered pixels, including a
   position:fixed child — so while body sat at opacity 0, the splash
   (a child of body) was ALSO invisible for that entire window, no matter
   what the splash's own opacity said. getComputedStyle on the splash
   itself kept reporting "1" throughout testing, because that check only
   reads an element's own opacity property, not the final composited
   result after ancestor opacity — a real blind spot, and the actual root
   cause of "blank pause, then the site just appears" across this whole
   feature's testing. The splash's own fade-out (it's opaque, covers the
   page, then fades to transparent) already reveals the real page
   underneath on its own — a second, independent fade on body was
   redundant even before it was actively harmful. */

@media (prefers-reduced-motion: reduce) {
  .intro-splash { display: none; }
}

/* html.quick-nav is set by an inline pre-paint <head> script that checks
   sessionStorage, written by nav-transition.js right before a click on an
   internal link navigates away. Landing here via a typed URL, bookmark,
   external link, or refresh never sets the flag, so the full splash above
   still plays — this only fires for genuine in-site navigation. */
html.quick-nav .intro-splash { display: none; }

/* ============ Film grain overlay ============ */
.grain-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  pointer-events: none;
  opacity: 0.04;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

/* ============ Responsive: simplify hero below 768px ============ */
@media (max-width: 768px) {
  .hero-pin {
    flex-direction: column;
    text-align: center;
    padding: 7rem 1.25rem 3rem;
  }
  .hero-copy { max-width: 100%; }
  /* .hero-pin's text-align: center above centers inline content, not a
     block-level list — without this, the list sat flush-left under the
     now-centered headline/subhead, and each item's own left-aligned text
     re-centered itself line-by-line whenever it wrapped, so the dot and a
     wrapped second line no longer lined up. Centering the block and
     left-aligning text within each item fixes both at once. */
  .hero-capabilities { margin-left: auto; margin-right: auto; }
  .phone { width: 240px; height: 511px; border-width: 6px; border-radius: 29px; }
  .phone-notch { top: 11px; width: 72px; height: 21px; }
}

/* Stack name/phone vertically only on narrow screens — side-by-side above this */
@media (max-width: 600px) {
  .flatmate-row {
    flex-direction: column;
    align-items: stretch;
    gap: 0.75rem;
  }
  .flatmate-row label.field--half:first-of-type,
  .flatmate-row label.field--half:last-of-type {
    flex: 1 1 auto;
  }
  .remove-flatmate { align-self: flex-end; }

  /* Same fix, same reason — the password/confirm-password pair was missed
     when .flatmate-row got this treatment. Squeezed into two ~170px
     columns on a phone, both placeholders ("At least 8 characters",
     "Re-enter your password") clip mid-word, unreadable. */
  .signup-account-row {
    flex-direction: column;
    align-items: stretch;
    gap: 0.75rem;
  }
}

/* ============ Auth pages (dashboard/portfolio login, signup, reset) ============ */
/* A single-card, viewport-centered layout for the utility pages that sit
   outside the marketing site proper (house/landlord login, signup,
   password reset) — previously each shipped its own plain system-font
   inline <style> block with none of the site's actual identity (no
   Fraunces, no grain/glow, no ghost-glyph header), a stark contrast right
   at the first real interaction point after signing up or clicking
   through from the marketing pages. Reuses the same tokens/patterns as
   everywhere else rather than inventing a second design language. */
.auth-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 7rem 1.25rem 3rem;
  position: relative;
  overflow: hidden;
}
.auth-card {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 400px;
  background: var(--panel);
  border: var(--hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: clamp(1.75rem, 4vw, 2.5rem);
  animation: content-fade-up 0.7s ease both;
}
.auth-card h1 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.55rem;
  text-align: center;
  margin: 0 0 0.4rem;
  color: var(--ghost);
}
.auth-card .page-subtitle { text-align: center; font-size: 0.88rem; margin-bottom: 1.85rem; }
.auth-card .field-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: 0.4rem;
}
.auth-card input {
  width: 100%;
  font-family: var(--font-body);
  /* docs/ios-audit.md — anything under 16px makes iOS Safari auto-zoom
     the whole page on focus. */
  font-size: 16px;
  background: rgba(26, 23, 32, 0.06);
  border: 1px solid rgba(26, 23, 32, 0.15);
  border-radius: var(--radius-sm);
  color: var(--ghost);
  padding: 0.7em 0.85em;
  /* Measured ~40px tall with padding alone — a few px under the ~44px
     touch-target guideline. min-height guarantees the floor regardless
     of font metrics, same convention used everywhere else in this app. */
  min-height: 44px;
  margin-bottom: 1.15rem;
}
.auth-card input::placeholder { color: rgba(107, 100, 120, 0.6); }
.auth-card input:focus {
  border-color: var(--mint);
  animation: input-glow-breathe 2.4s ease-in-out infinite;
  outline: none;
}
.auth-card .btn { width: 100%; }
.auth-card .links { text-align: center; margin-top: 1.1rem; font-size: 0.85rem; color: var(--muted); }
.auth-card .links a { color: var(--mint); text-decoration: underline; text-underline-offset: 2px; }
.auth-card .links a:hover { color: var(--ghost); }
.auth-error-banner,
.auth-success-banner {
  padding: 0.65em 0.95em;
  border-radius: var(--radius-sm);
  margin-bottom: 1.15rem;
  font-size: 0.85rem;
  display: none;
}
.auth-error-banner { background: rgba(255, 89, 89, 0.15); color: #BC2525; }
.auth-success-banner { background: rgba(143, 247, 199, 0.15); color: var(--mint); }
.auth-card .back-link { display: block; text-align: center; margin-bottom: 1.85rem; }
.auth-page .glow--1 { width: 42vw; height: 42vw; max-width: 480px; max-height: 480px; top: -12%; right: -14%; }

@media (prefers-reduced-motion: reduce) {
  .auth-card { animation: none; }
}

/* ============ Content pages — hero & reveal system ============ */
/* .glow--mint was already used on index.html (notify-showcase) with no
   background of its own — .glow--lavender/.glow--coral both define one,
   this one never did, so it rendered as an invisible blob. Real gap,
   fixed here rather than left as an unused-looking class. */
.glow--mint { background: radial-gradient(circle, rgba(143, 247, 199, 0.20), rgba(143, 247, 199, 0) 70%); }

/* Every content page previously opened with a bare <h1> and one flat,
   static glow — comparatively lifeless next to the homepage's layered,
   ambient-drifting aurora. Reuses the exact same glow recipe (already
   ambient-animated via glow-drift, so this is "free" motion, no new
   keyframes) and the homepage's own eyebrow + accent-word heading
   pattern, so a secondary page reads as the same product, not a
   generic docs template bolted onto it. */
.content-page .glow--2 { width: 30vw; height: 30vw; max-width: 400px; max-height: 400px; bottom: 62%; left: -9%; }
.content-page .glow--3 { width: 26vw; height: 26vw; max-width: 340px; max-height: 340px; top: 38%; right: -8%; }

/* Lightweight, dependency-free scroll reveal for content pages —
   deliberately NOT the homepage's [data-reveal] (that's driven by GSAP/
   ScrollTrigger, which content pages don't load, purely for weight —
   they have no scrubbed/pinned animation that would need the extra
   power). A distinct attribute keeps the two systems from ever
   colliding; see reveal.js. Placed after .content-page .how-card's own
   opacity:1 reset (further up this file) so source order lets this win
   for any element that actually opts in via [data-fade-in] — everything
   else on a content page stays statically visible as before.
   Uses the standalone `translate` property, not `transform`, on purpose
   — same reasoning as .how-card's own hover motion and .glow's drift
   animation elsewhere in this file: several elements this applies to
   (.spotlight, .steps, .stat-row, .feature-chips) already use
   `transform: translateX(-50%)` for their own breakout-centering CSS,
   and `transform` can only ever hold one value — a second rule setting
   `transform: translateY(...)` here would silently replace that
   centering instead of composing with it (confirmed live: it did,
   pushing .spotlight fully off-screen). `translate` is independent of
   `transform` and composes with it correctly regardless of which
   element it's applied to. */
.content-page [data-fade-in] {
  opacity: 0;
  translate: 0 22px;
  transition: opacity 0.6s cubic-bezier(0.2, 0.7, 0.2, 1), translate 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.content-page [data-fade-in].is-visible {
  opacity: 1;
  translate: 0 0;
}
/* Grid children fan out with a short stagger — reveal.js sets this
   custom property per element based on its position within a
   [data-fade-group], rather than every card in a grid arriving in a
   single flat instant. */
.content-page [data-fade-in] { transition-delay: calc(var(--fade-i, 0) * 90ms); }

@media (prefers-reduced-motion: reduce) {
  .content-page [data-fade-in] { opacity: 1; translate: 0 0; transition: none; }
}

/* ============ Reduced motion ============ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .hero-pin { flex-direction: column; text-align: center; padding: 7rem 1.25rem 3rem; }
  .typing-indicator { display: none; }
  .chat-bubble--calm { opacity: 1; transform: none; }
  .chat-message { animation: none; }
  [data-reveal] { opacity: 1; transform: none; }
  .glow { animation: none !important; }
  .field input:focus {
    animation: none;
    box-shadow: 0 0 0 3px rgba(143, 247, 199, 0.18);
  }
}
