:root {
  /* Routed through variables rather than calling env() at each use site, for
     one reason: env() cannot be overridden in a test, so a headless browser
     (where every inset is 0) can never tell a correct implementation from one
     that forgot the insets entirely. Behind a variable, a test can set these
     to a real iPhone's 59px/34px and check the layout actually responds. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);

  --paper: #f5f1e9;
  --paper-2: #eee7dc;
  --paper-3: #fbf8f2;
  --ink: #151714;
  /* 4.97:1 on paper before — over the AA line by a hair, which is fine for a
     caption and tiring for the paragraphs this actually sets. Two shades down
     is 5.9:1. Nobody will spot the colour change; the difference shows up as
     not having to lean in. */
  --muted: #5b5e56;
  --muted-2: #878b82;
  --line: rgba(21, 23, 20, 0.12);
  --line-strong: rgba(21, 23, 20, 0.22);
  --white: #fffdf8;
  --ease: cubic-bezier(.22,.78,.2,1);
  --shadow: 0 30px 80px rgba(20, 20, 18, 0.08);
  --cool-paper: #eef1f4;
  --graphite: #181b20;
  --metal: #c2a267;
  --mist: #dfe6eb;
}

* { box-sizing: border-box; }
html {
  scroll-behavior: smooth;
  /* The app shell puts data-theme="default-dark" and an INLINE
     style="color-scheme: dark" on <html>, and <html> carried no background of
     its own — so everything painted from the root canvas came out dark: the
     safe areas and the rubber-band at the ends of the page. <body> was already
     cream, which is exactly why only the edges showed it.
     !important is load-bearing, not lazy: an inline style beats any normal
     stylesheet rule. This sheet loads only on the marketing route, so the
     admin route keeps the dark shell. */
  /* ── ROOT CANVAS ──────────────────────────────────────────────────────
     Fixing it at --paper was half a fix. iOS paints the canvas behind the
     page from <html>, and this page has TWO zones: .hero-scroll is #111315
     and 300vh tall (the entire opening), everything after it is cream. A
     cream canvas under a near-black hero is exactly the white flash at the
     top of the phone — rubber-band at the top of the page, which is where
     people actually overscroll, exposed paper against the photo.
     So the canvas follows the page instead of guessing. Dark is the default
     because the page OPENS dark; script.js flips data-chrome to "light" on
     the same trigger that flips the header and the theme-color, so all
     three change on one event and cannot drift apart.
     If script.js never runs, the canvas stays dark — correct for the hero,
     visible only in bottom overscroll, which overscroll-behavior-y already
     suppresses. Failing toward the first thing you see is the right way
     round. */
  background: #111315;
  color-scheme: light !important;
  overscroll-behavior-y: none;
}
html[data-chrome="light"] {
  background: var(--paper);
}
/* body carries a framework class (.bg-q-background-primary) that outranks a
   bare element selector; body[class] ties it and wins on order. */
body,
body[class] {
  margin: 0;
  color: var(--ink);
  background: var(--paper);
  font-family: "Instrument Sans", Arial, sans-serif;
  font-size: 18px;
  line-height: 1.58;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
button, input, textarea { font: inherit; }
a { color: inherit; text-decoration: none; }
button, a { -webkit-tap-highlight-color: transparent; }
img { display: block; max-width: 100%; }

.skip-link {
  position: fixed;
  z-index: 1000;
  top: 14px;
  left: 14px;
  transform: translateY(-150%);
  background: var(--white);
  color: var(--ink);
  padding: 10px 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
}
.skip-link:focus { transform: translateY(0); }

.section-shell { width: min(1280px, calc(100% - 48px)); margin: 0 auto; }

.site-header {
  position: fixed;
  z-index: 60;
  inset: 0 0 auto 0;
  height: 88px;
  padding: 0 clamp(20px, 4vw, 54px);
  display: flex;
  align-items: center;
  justify-content: space-between;
  color: var(--white);
  transition: color .35s var(--ease), background .35s var(--ease), box-shadow .35s var(--ease);
}
.site-header.is-light {
  color: var(--ink);
  background: rgba(245, 241, 233, 0.82);
  backdrop-filter: blur(18px);
  box-shadow: 0 1px 0 rgba(21, 23, 20, 0.06);
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.brand-mark {
  width: 18px;
  height: 18px;
  border: 1px solid currentColor;
  border-radius: 50%;
  position: relative;
  display: inline-block;
}
.brand-mark::after {
  content: "";
  position: absolute;
  width: 1px;
  height: 26px;
  background: currentColor;
  left: 8px;
  top: -5px;
  transform: rotate(45deg);
}
.header-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}
.header-actions a {
  font-size: 15px;
  opacity: .9;
}

.button {
  border: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 50px;
  padding: 0 22px;
  border-radius: 999px;
  cursor: pointer;
  font-size: 15px;
  font-weight: 500;
  transition: transform .3s var(--ease), background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease), box-shadow .3s var(--ease), opacity .3s var(--ease);
}
.button:hover { transform: translateY(-1px); }
.button:focus-visible,
.text-link:focus-visible,
.text-link-button:focus-visible,
.film-stage:focus-visible,
.dropzone:focus-within,
input:focus-visible,
textarea:focus-visible {
  outline: 2px solid rgba(255,255,255,.88);
  outline-offset: 2px;
}
.site-header.is-light .button:focus-visible,
.modal .button:focus-visible,
input:focus-visible,
textarea:focus-visible,
.film-stage:focus-visible,
.text-link-button:focus-visible {
  outline-color: rgba(21, 23, 20, 0.88);
}
.button-small { min-height: 44px; padding-inline: 18px; font-size: 14px; }
.button-large { min-height: 58px; padding-inline: 28px; font-size: 16px; }
.button-primary {
  background: var(--white);
  color: var(--ink);
  box-shadow: 0 12px 40px rgba(0,0,0,.12);
}
.site-header.is-light .button-primary {
  background: var(--ink);
  color: var(--white);
  box-shadow: none;
}
.button-secondary {
  background: rgba(255,255,255,.09);
  color: var(--white);
  border: 1px solid rgba(255,255,255,.34);
  backdrop-filter: blur(16px);
}
.site-header.is-light .button-nav,
.site-header.is-light .button-secondary {
  color: var(--ink);
  background: rgba(21,23,20,.03);
  border: 1px solid rgba(21,23,20,.12);
}
.button-nav {
  min-height: 44px;
  color: var(--white);
  background: rgba(255,255,255,.06);
  border: 1px solid rgba(255,255,255,.18);
}
.button-dark { background: var(--ink); color: var(--white); }
.button-outline { background: transparent; color: var(--ink); border: 1px solid var(--line-strong); }
.button-outline:hover { border-color: var(--ink); }
.button.full { width: 100%; }
.text-link,
.text-link-button {
  background: transparent;
  border: 0;
  color: inherit;
  font-size: 15px;
  padding: 0;
  cursor: pointer;
  opacity: .85;
}
.text-link-button { text-decoration: underline; text-underline-offset: .16em; }

.hero-scroll { height: 300vh; background: #111315; position: relative; }
.hero-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  /* iOS Safari: 100vh includes the collapsed URL bar, which makes the sticky
     hero taller than the visible screen and the scroll-pan feel broken. svh
     is the stable small-viewport height — no jumping as the toolbar hides. */
  height: 100svh;
  /* NEVER let the sticky pane exceed the visible viewport. On short iPhones
     (SE, mini) a flat 700px/640px min-height made the pinned hero taller than
     the screen, so its bottom half — and the sense of it being pinned at all —
     fell below the fold and the section read as a normal scrolling image. */
  min-height: min(700px, 100svh);
  overflow: hidden;
}
.hero-visual {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* Was #17191b. This colour is only ever seen in two situations: the image
     failing to load, or a sub-pixel sliver at an edge. The overscan below
     should mean the second never happens, but near-black made that sliver as
     visible as a colour possibly could be against a mid-grey photo. This tone
     sits close to the scrimmed plate, so a miss would be invisible rather than
     a hairline — and it is still dark enough to carry white text on its own if
     the photograph never arrives. */
  background: #3c3b38;
}
.hero-visual picture { display: contents; }
.hero-image {
  position: absolute;
  /* Overscanned by 2px on every edge — see OVERSCAN in script.js for the
     measurement. script.js overwrites these inline once it runs; these values
     matter for the first paint before it does, which is otherwise the one
     frame that can still show the hairline. */
  top: -2px;
  /* ANCHORED RIGHT ON PURPOSE. The pan starts with the image's right edge on
     the viewport's right edge and travels left-to-right as you scroll. When it
     was anchored left, the untransformed layout position was the END of the
     pan, so the very first paint — before any script had run — showed the
     wrong end of the move and then snapped. Anchoring right makes the
     un-transformed state identical to frame zero of the pan, so there is
     nothing to snap from. This is CSS only: it holds even if the script never
     runs, never loads, or runs late on a slow phone. */
  right: -2px;
  left: auto;
  width: auto;
  height: calc(100% + 4px);
  max-width: none;
  object-fit: cover;
  object-position: center center;
  will-change: transform;
  transform: translate3d(0,0,0);
  /* The contrast/saturate grade used to live here. A filter on a full-screen
     layer costs the GPU a whole extra pass on EVERY frame of the scroll-pan.
     It is baked into the image file instead — identical look, no per-frame cost. */
  backface-visibility: hidden;
}
.hero-wash {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, rgba(7,10,8,.58) 0%, rgba(7,10,8,.20) 48%, rgba(7,10,8,.18) 100%);
}
.hero-vignette {
  position: absolute;
  inset: 0;
  /* Was two inset box-shadows, one with a 34vh blur radius, directly over the
     layer the pan moves. A blur that size is an expensive raster and it could
     never be cached against a moving layer. Gradients give the same falloff
     for nothing. */
  background:
    linear-gradient(to top, rgba(8,10,8,.30) 0%, rgba(8,10,8,0) 34%),
    radial-gradient(120% 100% at 50% 50%, rgba(6,7,6,0) 55%, rgba(6,7,6,.16) 100%);
}
.hero-ui {
  position: relative;
  height: 100%;
  color: var(--white);
  padding-top: clamp(112px, 14vh, 150px);
  padding-bottom: clamp(34px, 6vh, 72px);
  z-index: 3;
}
.hero-kicker {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 38px;
  text-transform: uppercase;
  font-size: 11px;
  letter-spacing: .16em;
  opacity: .82;
}
.hero-kicker span { width: 24px; height: 1px; background: currentColor; }
.hero-copy-wrap {
  position: relative;
  width: min(820px, 74vw);
  height: min(48vh, 470px);
  display: flex;
  align-items: center;
}
.hero-copy {
  position: absolute;
  inset: auto 0;
  opacity: 0;
  transform: translateY(28px);
  transition: opacity .8s var(--ease), transform .8s var(--ease);
  pointer-events: none;
}
.hero-copy.is-active { opacity: 1; transform: translateY(0); pointer-events: auto; }
.eyebrow {
  margin: 0 0 18px;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .16em;
  font-weight: 600;
}
.eyebrow.dark { color: var(--muted); }
.hero-copy h1,
.hero-copy h2,
.section-heading h2,
.featured-plan h3,
.property-film-copy h3,
.final-cta h2,
.modal-head h2 {
  margin: 0;
  font-size: clamp(56px, 7vw, 108px);
  line-height: .96;
  letter-spacing: -.045em;
  font-weight: 600;
  max-width: 1000px;
  text-wrap: balance;
}
.hero-sub {
  max-width: 700px;
  margin: 24px 0 0;
  font-size: clamp(18px, 1.4vw, 22px);
  line-height: 1.6;
  color: rgba(255,255,255,.86);
}
.hero-bottom {
  position: absolute;
  left: 0;
  right: 0;
  bottom: clamp(32px, 6vh, 68px);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 32px;
}
.hero-cta-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 16px; }
.hero-cta-row { display: flex; gap: 12px; flex-wrap: wrap; }
.hero-proof-wrap { display: flex; flex-direction: column; gap: 14px; align-items: flex-end; max-width: 540px; }
.hero-trust-line { margin: 0; font-size: 15px; color: rgba(255,255,255,.86); text-align: right; }
.hero-proof {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: flex-end;
  font-size: 12px;
  letter-spacing: .04em;
  color: rgba(255,255,255,.75);
}
.hero-proof i { width: 4px; height: 4px; border-radius: 50%; background: currentColor; }
.scroll-meter {
  position: absolute;
  right: clamp(20px, 2vw, 28px);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.scroll-meter-label { writing-mode: vertical-rl; transform: rotate(180deg); opacity: .72; }
.scroll-meter-track { width: 1px; height: 130px; background: rgba(255,255,255,.24); position: relative; overflow: hidden; }
.scroll-meter-track i { position: absolute; inset: 0; background: var(--white); transform-origin: top; transform: scaleY(0); }
.scroll-meter-count { opacity: .85; }


/* How-it-works. Sits between the hero and pricing: the hero shows the motion,
   this explains where it comes from, and only then does pricing ask for money.
   It also earns the "six or more photos" requirement pricing leans on, which
   the page previously asked for without ever justifying. */
.method-section {
  position: relative;
  z-index: 1;
  /* A DIFFERENT GROUND, ON PURPOSE. Evidence, pricing, method, trust and the
     closing CTA all sat on the same --paper, so five full-height sections ran
     together with nothing marking where one argument ended and the next began.
     That flat run is most of what reads as monotonous — it is not the type, it
     is that the page never changes colour.
     --paper-3 is the same near-white the stat cards already use, so this
     introduces no new colour; it just lifts one band out of the run and gives
     the scroll a middle. Hairlines top and bottom so the edge is deliberate
     rather than a rendering seam. */
  background: var(--paper-3);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding: clamp(96px, 11vw, 148px) 0 clamp(88px, 10vw, 132px);
}
.method-head { max-width: 780px; }
.method-head h2 {
  font-size: clamp(32px, 4.2vw, 54px);
  line-height: 1.06;
  letter-spacing: -.025em;
  margin: 0;
}
/* Bigger and looser than body copy — this is the paragraph people actually
   read, so it gets room to breathe rather than being packed tight. */
.method-lead {
  margin: 20px 0 0;
  max-width: 660px;
  color: var(--muted);
  font-size: 19px;
  line-height: 1.62;
}
.method-steps {
  list-style: none;
  margin: clamp(36px, 4.5vw, 56px) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(40px, 6vw, 96px);
  row-gap: 0;
  border-top: 1px solid var(--line);
}
/* No boxes and no vertical dividers. Four identically-sized bordered cards a
   few pixels apart merge into one grey block and stop being four things; an
   open list with real space between rows reads as steps. */
.method-step {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  column-gap: 18px;
  padding: clamp(22px, 2.4vw, 30px) 0;
  border-bottom: 1px solid var(--line);
}
.method-num {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .14em;
  color: var(--metal);
  font-variant-numeric: tabular-nums;
}
.method-step h3 {
  grid-column: 2;
  margin: 0;
  font-size: clamp(19px, 1.5vw, 22px);
  line-height: 1.25;
  letter-spacing: -.015em;
  font-weight: 600;
}
.method-step p {
  grid-column: 2;
  margin: 6px 0 0;
  color: var(--muted);
  font-size: 16px;
  line-height: 1.5;
  max-width: 44ch;
}
@media (max-width: 760px) {
  .method-steps { grid-template-columns: 1fr; }
}
.method-tip {
  margin-top: clamp(38px, 5vw, 58px);
  padding: 26px 28px;
  /* --paper, not --paper-3: the section itself is --paper-3 now, so the tip
     was the same colour as its own background and survived only as a border.
     Going one step darker than the section makes it read as an inset panel —
     and it is the page's base colour, so no new tone enters the palette. */
  background: var(--paper);
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 26px;
  flex-wrap: wrap;
}
.method-tip p { margin: 0; max-width: 640px; color: var(--muted); font-size: 16px; line-height: 1.6; }
.method-tip b { color: var(--ink); font-weight: 600; }
.method-tip .button { flex: none; }

@media (max-width: 900px) {
  .method-steps { grid-template-columns: 1fr 1fr; }
  .method-step { padding: 26px 20px 28px 0; }
  .method-step:nth-child(2) { border-right: 0; padding-right: 0; }
  .method-step:nth-child(3) { padding-left: 0; border-top: 1px solid var(--line); }
  .method-step:nth-child(4) { border-top: 1px solid var(--line); }
}
@media (max-width: 560px) {
  .method-steps { grid-template-columns: 1fr; }
  .method-step,
  .method-step:not(:first-child) {
    padding: 24px 0 26px;
    border-right: 0;
    border-top: 1px solid var(--line);
  }
  .method-step:first-child { border-top: 0; }
  .method-tip { padding: 22px; }
  .method-tip .button { width: 100%; }
}

.pricing-section {
  position: relative;
  z-index: 1;
  margin-top: 0;
  padding: clamp(112px, 12vw, 158px) 24px clamp(104px, 10vw, 142px);
  background: var(--cool-paper);
  border-top: 1px solid rgba(24,27,32,.09);
  border-bottom: 1px solid rgba(24,27,32,.10);
  overflow: hidden;
}
.pricing-section::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: min(1040px, calc(100% - 48px));
  height: 1px;
  transform: translateX(-50%);
  background: linear-gradient(90deg, transparent, rgba(24,27,32,.20), transparent);
}
.pricing-shell {
  width: min(1040px, 100%);
  margin: 0 auto;
}
.pricing-heading {
  max-width: 720px;
  margin: 0 auto 50px;
  text-align: center;
  opacity: 0;
  transform: translate3d(0, 34px, 0);
  transition: opacity .82s var(--ease), transform .92s var(--ease);
}
.pricing-heading.is-visible {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}
/* 4.38:1 on --cool-paper before, which is under the 4.5 floor — and this is
   12px uppercase with wide tracking, the hardest shape to read on the page.
   One shade down clears it at 5.3:1. */
.pricing-heading .eyebrow { color: #5b646d; }
.pricing-heading h2 {
  margin: 0;
  font-size: clamp(42px, 5vw, 68px);
  line-height: .98;
  letter-spacing: -.043em;
  font-weight: 600;
  color: var(--graphite);
  text-wrap: balance;
}
.pricing-heading > p:last-of-type {
  max-width: 640px;
  margin: 20px auto 0;
  color: #626b74;
  font-size: 16px;
  line-height: 1.62;
}
.pricing-friction-note {
  margin: 24px auto 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 12px;
  padding: 10px 16px;
  border: 1px solid rgba(24,27,32,.12);
  background: rgba(255,255,255,.58);
  color: #4f5861;
  font-size: 12px;
  letter-spacing: .02em;
}
.pricing-friction-note i {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentColor;
  opacity: .42;
}
/* ── Why it matters ───────────────────────────────────────────────────────
   The evidence section. Every figure carries its source in the card, at a size
   you can actually read, because the whole point of the section is that these
   numbers survive being checked. Sources are NOT footnoted — an agent should be
   able to see where a number came from without hunting for it. */
.evidence-section {
  /* The bottom padding was 20-40px because a six-item explainer used to sit
     under the stat row and carry the section out. That block restated the four
     figures directly above it for a reader who sells houses for a living, and
     every operational fact in it was already on the page — turnaround and
     formats on the pricing cards, "nobody comes back out" in step 01. It is
     gone, so the row of figures now closes the section and needs real room
     beneath it instead of a hairline gap. */
  padding: clamp(84px, 11vh, 128px) 0 clamp(78px, 9vh, 116px);
  background: var(--paper);
}
.evidence-heading { max-width: 760px; }
.evidence-heading h2 {
  margin: 14px 0 18px;
  font-size: clamp(32px, 4.1vw, 52px);
  line-height: 1.04;
  letter-spacing: -.035em;
  font-weight: 600;
}
.evidence-heading > p:last-child {
  margin: 0;
  color: var(--muted);
  font-size: 17px;
  line-height: 1.62;
}
.evidence-grid {
  margin-top: clamp(38px, 5vh, 56px);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
}
.evidence-stat {
  background: var(--paper-3);
  padding: 26px 24px 24px;
  display: flex;
  flex-direction: column;
}
.evidence-figure {
  margin: 0 0 12px;
  /* Tabular figures so every digit occupies the same box while the number
     counts. Proportional digits make a ticking counter visibly wobble — a 1 is
     much narrower than a 7 in most faces, so the text shifts on every frame.
     The script also pins a min-width before it drops the value to zero; the two
     together are what make the count read as smooth rather than nervous. */
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  display: inline-block;
  font-size: clamp(44px, 5vw, 58px);
  line-height: .9;
  letter-spacing: -.055em;
  font-weight: 600;
  color: var(--ink);
}
.evidence-claim {
  margin: 0 0 16px;
  min-height: 88px;
  font-size: 14px;
  line-height: 1.52;
  color: rgba(21,23,20,.72);
}
.evidence-claim em { font-style: normal; font-weight: 600; color: var(--ink); }
.evidence-source {
  /* Reserves two lines so the four rules sit on one shared baseline even when
     one source name wraps and the others don't. */
  min-height: 46px;
  margin: auto 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  font-size: 11px;
  line-height: 1.4;
  letter-spacing: .05em;
  text-transform: uppercase;
  /* Was --muted-2, which measured 3.08:1 against the card — below the 4.5:1
     floor and, at 11px uppercase, the least legible text on the page. The
     section's whole argument is that these numbers can be checked, so the line
     saying where they came from cannot be the one nobody can read. */
  color: var(--muted);
}

@media (max-width: 1080px) {
  .evidence-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* The 700px query held nothing but the points-list column counts, and that
   list is gone, so the query went with it. */
@media (max-width: 620px) {
  .evidence-grid { grid-template-columns: 1fr; }
  .evidence-stat { padding: 22px 20px 20px; }
  /* Stacked one-per-row there is nothing to align across, so the reserved
     heights only produce dead space inside each card. */
  .evidence-claim { min-height: 0; margin-bottom: 12px; }
  .evidence-source { min-height: 0; }
}

.pricing-cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 20px;
  align-items: stretch;
}
.pricing-card {
  --card-accent: #cfd4d8;
  /* The price tag's width, shared with the top hairline below so the two can
     never disagree about where the tag starts. Overridden on small screens. */
  --price-tag-w: 132px;
  position: relative;
  min-height: 472px;
  /* 27px on top, not 26: the top edge is drawn by ::before rather than by a
     real border, so this puts the content back where a 1px border left it. */
  padding: 27px 26px 26px;
  display: flex;
  flex-direction: column;
  /* NO TOP BORDER. An absolutely positioned child sits against the PADDING
     box, so a real top border always drew a line across the top of the price
     tag. On the Agent card that border composites over mist (198) while the
     tag is near-white (242) — a 44-point step, which is the line that showed
     above "Tailored". The tag cannot simply be moved up over it either: the
     card is overflow:hidden, and overflow clips children to the padding box,
     so nothing can paint over a border from the inside. */
  border: 1px solid rgba(24,27,32,.12);
  border-top: 0;
  border-radius: 8px;
  box-shadow: 0 14px 38px rgba(18,23,29,.08);
  overflow: hidden;
  opacity: 0;
  transform: translate3d(0, 96px, 0) scale(.965);
  transform-origin: center bottom;
  transition:
    opacity .78s var(--ease),
    transform 1.05s var(--ease),
    border-color .28s var(--ease),
    box-shadow .28s var(--ease);
}
.pricing-card:nth-child(2) { transition-delay: .12s; }
.pricing-card:nth-child(3) { transition-delay: .24s; }
.pricing-card.is-visible {
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1);
}
.pricing-card > *:not(.pricing-card-number):not(.pricing-price-tag) { position: relative; z-index: 1; }
.pricing-card-number {
  position: absolute;
  right: 14px;
  bottom: -18px;
  font-size: 104px;
  line-height: 1;
  letter-spacing: -.08em;
  font-weight: 600;
  color: currentColor;
  opacity: .035;
  pointer-events: none;
}
.pricing-card-featured .pricing-card-number { opacity: .055; }
/* THE TOP EDGE — AND IT STOPS AT THE PRICE TAG.
   This used to be a 4px accent stripe running the full width of the card, so
   it also ran directly above the price. On Essential it was #cfd4d8 on white,
   invisible, which is why that card read as a clean corner; on Property Film
   the same rule drew a bright gold line across the $149 and on Agent Batch a
   blue-grey one. Same markup, three different-looking price corners.
   Now it is a 1px hairline matching the card's other three borders — the
   card's edge, not a coloured stripe — and `right: var(--price-tag-w)` ends
   it exactly where the tag begins. Nothing sits above any price on any card.
   Tier colour still lives in the tag's bottom edge and the bullet dots. */
.pricing-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: var(--price-tag-w);
  height: 1px;
  background: rgba(24,27,32,.12);
}
.pricing-card:hover {
  border-color: rgba(24,27,32,.24);
  box-shadow: 0 22px 52px rgba(18,23,29,.13);
}
.pricing-card:hover::before { background: rgba(24,27,32,.24); }
.pricing-card-essential {
  --card-accent: #cfd4d8;
  background: #ffffff;
  color: #1c2025;
}
/* PROPERTY FILM — the premium tier, and it should look like it without
   shouting. The gold is no longer one hard stripe; it is spread thin across
   the whole card: a warm bloom behind the price corner, a hairline frame, a
   gold label, a gold rule under the title, a gold CTA. Each touch is quiet on
   its own — together they read as expensive. */
.pricing-card-featured {
  --card-accent: var(--metal);
  background:
    /* Warm light falling from the price corner. Placed at the top-right so it
       lifts the number, which is the thing the card is selling. */
    radial-gradient(128% 92% at 90% -4%, rgba(194,162,103,.19) 0%, rgba(194,162,103,.045) 44%, rgba(194,162,103,0) 68%),
    linear-gradient(176deg, #1e2127 0%, #17191f 58%, #131519 100%);
  color: #ffffff;
  /* A gold hairline instead of a black-on-black edge — the card gets a frame
     rather than just ending. */
  border-color: rgba(194,162,103,.36);
  box-shadow:
    0 24px 58px rgba(16,19,24,.30),
    inset 0 0 0 1px rgba(194,162,103,.06);
}
/* The gold frame closes across the top too — but as the same hairline as the
   other three sides, and still stopping at the price tag. */
.pricing-card-featured::before { background: rgba(194,162,103,.36); }
.pricing-card-featured:hover::before { background: rgba(194,162,103,.52); }
.pricing-card-agent {
  --card-accent: #8ea0ad;
  background: var(--mist);
  color: #1c2227;
}
.pricing-card-topline {
  /* No longer a two-column row. The price moved out to its own corner tag, so
     this is just the label and the title — and it reserves the space the tag
     occupies so a long title can never run underneath it. */
  display: flex;
  flex-direction: column;
  /* The title clears the tag by sitting BELOW it, not beside it. Reserving a
     right-hand gutter instead wrapped "Essential Film" onto two lines on one
     card and not the others — exactly the inconsistency this change was meant
     to remove. Below the tag, every title gets the full card width and every
     card's title starts on the same line. */
  padding-top: 74px;
  padding-bottom: 18px;
  border-bottom: 1px solid rgba(24,27,32,.13);
}
.pricing-card-featured .pricing-card-topline { border-color: rgba(194,162,103,.34); }
.pricing-card .plan-label { color: rgba(24,27,32,.52); }
/* Gold, and tracked out a little — a maker's mark above the product name. */
.pricing-card-featured .plan-label {
  color: var(--metal);
  letter-spacing: .05em;
}
.pricing-card h3 {
  margin: 8px 0 0;
  font-size: 25px;
  line-height: 1.04;
  letter-spacing: -.035em;
  font-weight: 600;
}

/* THE PRICE TAG.
   One fixed box, pinned to the same corner at the same size on all three cards,
   so the eye finds the number in the identical place every time — the corner of
   an index card. Everything about it is shared; only the colours differ, which
   is what keeps the three tiers telling themselves apart. */
.pricing-price-tag {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 2;
  /* Fixed width AND height, not min-height: a word and a number must produce
     an identical box or the three cards stop matching by a pixel or two. */
  width: var(--price-tag-w);
  height: 86px;
  padding: 13px 17px 11px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  gap: 3px;
  text-align: right;
  border-left: 1px solid rgba(24,27,32,.10);
  /* The card's accent colour lives in this edge — same shape on every card,
     different colour, which is what keeps the tiers distinguishable. */
  border-bottom: 2px solid var(--card-accent);
  border-bottom-left-radius: 10px;
  background: #f2f5f6;
}
.price-figure {
  margin: 0;
  font-size: 36px;
  line-height: .92;
  letter-spacing: -.055em;
  font-weight: 600;
  white-space: nowrap;
}
.price-figure-word {
  /* A word has to sit in the same box as a number without widening it.
     "Tailored" measures 89px at this size against 97px of usable width —
     about 8px of slack, which is the margin a fallback font needs. 27px
     fitted in Instrument Sans with half a pixel to spare and would have
     wrapped the moment the webfont failed to load. */
  font-size: 25px;
  line-height: 1.05;
  letter-spacing: -.03em;
}
.price-currency {
  display: inline-block;
  margin: .16em .05em 0 0;
  vertical-align: top;
  font-size: .38em;
  letter-spacing: 0;
}
.price-caption {
  margin: 0;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  opacity: .58;
  white-space: nowrap;
}
.pricing-card-featured .pricing-price-tag {
  /* Gold leaf rather than grey glass: the corner the number sits in is the
     most-looked-at 132px on the card, so it carries the most gold. */
  background: linear-gradient(158deg, rgba(194,162,103,.22) 0%, rgba(194,162,103,.07) 62%, rgba(194,162,103,.03) 100%);
  border-left-color: rgba(194,162,103,.30);
  border-bottom-color: rgba(194,162,103,.68);
}
/* Warm white, not pure white — pure white next to gold reads cold. */
.pricing-card-featured .price-figure { color: #f8f1e3; }
.pricing-card-featured .price-caption { color: var(--metal); opacity: .92; }
.pricing-card-featured .pricing-card-number { color: var(--metal); opacity: .10; }
.pricing-card-agent .pricing-price-tag {
  background: rgba(255,255,255,.58);
  border-left-color: rgba(24,27,32,.09);
}

.pricing-card-summary {
  min-height: 76px;
  margin: 18px 0 20px;
  color: rgba(24,27,32,.64);
  font-size: 14px;
  line-height: 1.55;
}
.pricing-card-featured .pricing-card-summary { color: rgba(255,255,255,.70); }
.compact-feature-list {
  list-style: none;
  margin: 0 0 24px;
  padding: 0;
  display: grid;
  gap: 9px;
}
.compact-feature-list li {
  position: relative;
  padding-left: 17px;
  font-size: 13px;
  line-height: 1.48;
}
.compact-feature-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .64em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--card-accent);
}
.pricing-card-action {
  margin-top: auto;
  display: grid;
  gap: 10px;
}
.pricing-card-action p {
  margin: 0;
  /* All three reserve the same room, or the buttons sit at three different
     heights purely because one caption runs to a third line. */
  min-height: 52px;
  color: rgba(24,27,32,.58);
  font-size: 12px;
  line-height: 1.45;
}
.pricing-card-featured .pricing-card-action p { color: rgba(255,255,255,.64); }
.pricing-button {
  min-height: 50px;
  /* 10px, down from 14: at the larger size "Start Property Film — pay $149
     today" measures 251px and the button's content box was exactly 251px —
     zero slack, so any font substitution would have wrapped it. This buys
     8px back. */
  padding-inline: 10px;
  font-size: 15px;
}
/* The 3-up grid is at its narrowest just above the 980px stacking point, and
   there the longest label does not fit on one line at 15px. Above ~1040px the
   shell reaches its max width and the cards stop shrinking, and below 981px
   they stack full-width with room to spare — so this band is the only place
   it happens, and it is the only place that steps back down. */
@media (min-width: 981px) and (max-width: 1060px) {
  .pricing-button { font-size: 13px; }
}
.pricing-button-dark { background: #20252b; color: #ffffff; }
.pricing-button-light { background: #ffffff; color: var(--graphite); }
.pricing-card-agent .pricing-button-dark { background: #27323a; }
/* The one button on the page that is gold. Dark text on it keeps the contrast
   ratio well clear of AA — a pale-gold button with white text would have
   looked premium and been unreadable. */
.pricing-card-featured .pricing-button-light {
  background: linear-gradient(180deg, #e3caa0 0%, #cbab72 54%, #bd9b60 100%);
  color: #17191d;
  box-shadow:
    0 1px 0 rgba(255,255,255,.36) inset,
    0 10px 26px rgba(150,116,54,.30);
}
.pricing-card-featured .pricing-button-light:hover {
  background: linear-gradient(180deg, #ebd5ae 0%, #d5b67e 54%, #c6a469 100%);
}

.trust-band { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: 0 0 24px; }
.trust-grid { display: grid; grid-template-columns: repeat(4, 1fr); }
.trust-item {
  padding: 34px 26px 40px 0;
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 36px;
}
.trust-item:nth-child(2),
.trust-item:nth-child(3),
.trust-item:nth-child(4) { padding-left: 28px; }
.trust-item:last-child { border-right: 0; }
.trust-item span { font-size: 11px; color: var(--muted); letter-spacing: .14em; }
.trust-item p { margin: 0; font-size: clamp(24px, 2.1vw, 34px); line-height: 1.08; letter-spacing: -.03em; font-weight: 500; }
.legal-note { margin-top: 22px; color: var(--muted); font-size: 13px; }

.final-cta { padding: clamp(110px, 14vw, 190px) 0; }
.final-cta-inner {
  background: rgba(255,255,255,.42);
  border: 1px solid var(--line);
  padding: clamp(32px, 5vw, 56px);
  text-align: center;
  box-shadow: var(--shadow);
}
.final-cta h2 { font-size: clamp(56px, 7vw, 98px); margin-inline: auto; }
.final-cta .eyebrow { display: block; }
.final-cta p:not(.eyebrow):not(.final-microcopy) {
  max-width: 680px;
  margin: 24px auto 34px;
  color: var(--muted);
}
.final-actions { display: flex; justify-content: center; gap: 18px; align-items: center; flex-wrap: wrap; }
.final-microcopy { margin: 18px 0 0; color: var(--muted); font-size: 14px; }

.site-footer {
  min-height: 126px;
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  gap: 24px;
  align-items: center;
  color: var(--muted);
  font-size: 13px;
  padding: 0 0 22px;
}
.footer-brand { color: var(--ink); }
.site-footer p { margin: 0; }
/* Footer credit. A fourth column above 900px so it shares a baseline with the
   copyright and reads as one line of small print; below that an earlier
   breakpoint collapses the footer to a single column and this must not fight
   it, hence the min-width scope. Size and colour are inherited from
   .site-footer, so the only additions are the underline — a credit that does
   not look like a link is not much of a credit — and the ink hover the rest of
   the page's links use. */
@media (min-width: 901px) {
  .site-footer { grid-template-columns: 1fr 1fr auto auto; }
}
.footer-credit a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: .18em;
  text-decoration-color: rgba(21,23,20,.28);
  transition: color .25s var(--ease), text-decoration-color .25s var(--ease);
}
.footer-credit a:hover { color: var(--ink); text-decoration-color: var(--ink); }

.modal {
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(1180px, calc(100% - 28px));
  max-height: calc(100vh - 28px);
  border: 0;
  padding: 0;
  background: var(--paper-3);
  color: var(--ink);
  box-shadow: 0 40px 120px rgba(0,0,0,.28);
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(21, 23, 20, 0.25) transparent;
}
/* NO backdrop-filter here. A blurred ::backdrop forces the compositor to
   re-blur the entire viewport on every frame the example plates move, which
   measured 16 fps on a 1440x900 2x display and a 67ms worst frame on Android.
   A slightly deeper flat scrim reads the same and costs nothing. */
.modal::backdrop { background: rgba(12, 13, 12, 0.78); }
.modal-shell { padding: 34px; }
/* Not sticky. This header used to pin to the top of the dialog and follow the
   scroll, which pulled attention away from the shots — the only thing on this
   screen that should be moving. It scrolls away with everything else now. */
.examples-head { padding-bottom: 22px; }
.modal-head h2 { font-size: clamp(42px, 5vw, 66px); }
.modal-lead,
.modal-note { margin: 16px 0 0; max-width: 800px; color: var(--muted); }
.modal-note { font-size: 15px; }
.dialog-close {
  position: absolute;
  top: 18px;
  right: 18px;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: rgba(255,255,255,.66);
  cursor: pointer;
  font-size: 28px;
  line-height: 1;
  z-index: 5;
}

.examples-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
  margin-top: 30px;
}
.example-card {
  background: #f6f2ea;
  border: 1px solid var(--line);
  overflow: hidden;
  /* Isolate each card so a moving plate can never invalidate its siblings. */
  contain: paint;
}
.motion-shot {
  position: relative;
  /* 16:9, matching the delivered film. It was 4:3 to suit a tall still plate;
     holding that now would crop the top and bottom off every shot, which is
     the one thing an example must not do. */
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: #1a1c1f;
}
/* REAL FOOTAGE NOW. Everything between here and the motion bar used to be a
   machine for faking the product: a wide still plate translated across a 4:3
   window on a shared 28s timeline, with a toggle offering two flavours of the
   fake. It was well built and it was the wrong thing to build — it looked like
   a crop panning over a photograph, which is exactly what Villa Sight does NOT
   do, so the examples argued against the product.
   These are four shots from one finished film (2081 Tahoe Mountain Rd). The
   camera genuinely moves through the room: near furniture and the mountains
   outside travel at different speeds, which no pan across a flat image can
   produce. That parallax is the proof, and it only needed a <video> tag. */
.motion-shot video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* Own compositor layer, same reasoning as the plates before it. */
  transform: translateZ(0);
  backface-visibility: hidden;
}

.example-meta { padding: 16px 16px 18px; }
.example-meta span { display: block; font-size: 16px; font-weight: 600; margin-bottom: 7px; }
.example-meta p { margin: 0; color: var(--muted); font-size: 14px; }

.property-film-demo {
  margin-top: 36px;
  border-top: 1px solid var(--line);
  padding-top: 30px;
}
.property-film-copy h3 { font-size: clamp(32px, 4vw, 50px); max-width: 950px; }
.property-film-copy p:last-child { margin: 16px 0 0; color: var(--muted); }
.film-demo-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(320px, .75fr);
  gap: 18px;
  margin-top: 24px;
}
.film-display-frame {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: #d9d2c7;
}
.film-display-frame video { width: 100%; height: 100%; object-fit: cover; display: block; }
.film-display-overlay {
  position: absolute;
  inset: auto 18px 18px 18px;
  background: rgba(11, 12, 11, 0.58);
  color: var(--white);
  padding: 14px 16px;
}
.film-display-overlay span { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
.film-display-overlay p { margin: 0; font-size: 14px; color: rgba(255,255,255,.82); }
.film-stages {
  display: grid;
  gap: 12px;
  align-content: start;
}
.film-stage {
  background: rgba(255,255,255,.7);
  border: 1px solid var(--line);
  display: grid;
  grid-template-columns: 54px 1fr;
  align-items: center;
  gap: 12px;
  text-align: left;
  padding: 16px;
  cursor: pointer;
  transition: transform .25s var(--ease), border-color .25s var(--ease), background .25s var(--ease);
}
.film-stage:hover { transform: translateY(-1px); }
.film-stage.is-active { background: var(--ink); color: var(--white); border-color: var(--ink); }
.film-stage strong { font-size: 12px; letter-spacing: .14em; }
.film-stage span { font-size: 16px; line-height: 1.3; }
.demo-action-row { margin-top: 24px; }

/* ── Upload dialog ────────────────────────────────────────────────────────
   Rebuilt. The old version had five different translucent whites over cream
   (.46 .56 .60 .72 .78), square corners on everything while the rest of the
   site uses 8-10px radii, and a raw native checkbox. The near-but-not-quite
   surfaces are what made it read as monotonous AND as though random boxes were
   lifted — the eye sees five elevations where the design has one.

   ONE surface token, ONE radius, ONE hairline, ONE shadow, used everywhere. */
.upload-shell {
  padding-top: 40px;
  --panel: #fffdf9;
  --panel-line: rgba(21, 23, 20, .11);
  --panel-radius: 12px;
  --panel-lift: 0 1px 2px rgba(20, 20, 18, .04), 0 10px 26px rgba(20, 20, 18, .05);
}

/* The order summary. Both columns start at the left edge of their own text —
   the right column used to be text-align:right, which left a ragged inner edge
   and made a two-column panel look accidental. */
.upload-package-bar {
  margin: 26px 0 30px;
  padding: 22px 24px;
  border: 1px solid var(--panel-line);
  border-left: 2px solid var(--metal);
  border-radius: var(--panel-radius);
  background: var(--panel);
  box-shadow: var(--panel-lift);
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 26px;
  align-items: center;
}
.upload-package-bar h3 {
  margin: 6px 0 0;
  font-size: 27px;
  line-height: 1.05;
  letter-spacing: -.03em;
}
.package-sidecopy {
  text-align: left;
  max-width: 300px;
  padding-left: 26px;
  border-left: 1px solid var(--panel-line);
}
.package-sidecopy span {
  display: block;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -.01em;
}
.package-sidecopy p { margin: 6px 0 0; color: var(--muted); font-size: 13.5px; line-height: 1.5; }

/* ── Steps ─────────────────────────────────────────────────────────────── */
.upload-form { display: grid; gap: 34px; }
.form-step { display: grid; gap: 16px; }
.form-step-label {
  display: flex;
  align-items: center;
  gap: 11px;
  margin: 0;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--panel-line);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink);
}
.form-step-label i {
  font-style: normal;
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--metal);
}

/* ── Fields ────────────────────────────────────────────────────────────── */
/* align-items:start, or a column whose help text makes it taller stretches the
   OTHER column's input to match — which is why "Property address" was rendering
   half again as tall as "Listing URL" and looked broken. */
.field-row.two-up {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  align-items: start;
}
.upload-form label { display: grid; gap: 8px; align-content: start; }
.upload-form label span {
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -.005em;
}
.upload-form label span em { font-style: normal; color: var(--muted-2); font-weight: 400; }
.upload-form input:not([type="file"]):not([type="checkbox"]),
.upload-form textarea {
  width: 100%;
  border: 1px solid var(--panel-line);
  border-radius: 9px;
  background: var(--panel);
  padding: 13px 15px;
  color: var(--ink);
  font-size: 15px;
  outline: none;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease), background .2s var(--ease);
}
.upload-form input::placeholder,
.upload-form textarea::placeholder { color: var(--muted-2); }
.upload-form input:not([type="file"]):not([type="checkbox"]):hover,
.upload-form textarea:hover { border-color: rgba(21,23,20,.2); }
/* There was no visible focus state at all — outline:none with nothing to
   replace it. Keyboard users had no idea where they were. */
.upload-form input:not([type="file"]):not([type="checkbox"]):focus,
.upload-form textarea:focus {
  border-color: var(--ink);
  box-shadow: 0 0 0 3px rgba(21, 23, 20, .08);
  background: #fff;
}
.upload-form textarea { resize: vertical; min-height: 118px; }
.field-help { color: var(--muted); font-size: 12.5px; line-height: 1.5; }

/* ── Photos ────────────────────────────────────────────────────────────── */
.uploader-block {
  border: 1px solid var(--panel-line);
  border-radius: var(--panel-radius);
  background: var(--panel);
  box-shadow: var(--panel-lift);
  padding: 22px;
  display: grid;
  gap: 16px;
}
.uploader-copy {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  align-items: flex-start;
}
.uploader-copy h3 { margin: 5px 0 0; font-size: 24px; letter-spacing: -.03em; line-height: 1.08; }
.uploader-copy p { margin: 0; color: var(--muted); max-width: 340px; font-size: 14px; line-height: 1.55; }
.uploader-copy .plan-label em {
  font-style: normal; color: var(--muted-2); font-weight: 400;
  text-transform: none; letter-spacing: normal;
}
.dropzone {
  border: 1.5px dashed rgba(21,23,20,.18);
  border-radius: 10px;
  background: linear-gradient(180deg, rgba(21,23,20,.012), rgba(21,23,20,.03));
  min-height: 188px;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 6px;
  text-align: center;
  padding: 26px;
  cursor: pointer;
  transition: border-color .22s var(--ease), background .22s var(--ease), box-shadow .22s var(--ease);
}
.dropzone:hover { border-color: rgba(21,23,20,.34); background: rgba(21,23,20,.035); }
.dropzone.dragover {
  border-color: var(--ink);
  border-style: solid;
  background: #fff;
  box-shadow: 0 0 0 4px rgba(21,23,20,.06);
}
.dropzone input { display: none; }
.dropzone strong { display: block; font-size: 16.5px; letter-spacing: -.01em; }
.dropzone span,
.dropzone small { display: block; color: var(--muted); }
.dropzone span { font-size: 14px; }
.dropzone small { font-size: 12.5px; color: var(--muted-2); }

/* Sits INSIDE the photos panel, so it must not carry its own border and
   shadow — that was one of the boxes that looked separately lifted. */
.photo-progress {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  padding: 0;
  border: 0;
  background: none;
}
.photo-progress strong { font-size: 13.5px; font-weight: 600; letter-spacing: -.01em; }
.photo-progress-track {
  flex: 1 1 160px;
  height: 3px;
  border-radius: 999px;
  background: rgba(21,23,20,.09);
  overflow: hidden;
}
.photo-progress-track i {
  display: block;
  height: 100%;
  width: 0%;
  border-radius: 999px;
  background: var(--ink);
  transition: width .45s var(--ease);
}
.photo-progress.is-happy strong { color: var(--ink); }
.photo-nudge { margin: 0; font-size: 13px; line-height: 1.55; color: var(--muted); }
.photo-nudge.is-visible { color: #7a5c2e; }
.upload-support-row { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }

.file-list { display: grid; gap: 10px; }
.file-item {
  display: grid;
  grid-template-columns: 72px 1fr auto;
  gap: 14px;
  align-items: center;
  padding: 10px;
  border: 1px solid var(--panel-line);
  border-radius: 10px;
  background: rgba(21,23,20,.022);
}
.file-thumb { width: 72px; height: 56px; border-radius: 6px; object-fit: cover; background: #ddd4c7; }
.file-meta { min-width: 0; }
.file-meta strong { display: block; font-size: 14.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-meta span { display: block; font-size: 12.5px; color: var(--muted); }
.remove-file {
  border: 1px solid var(--panel-line);
  border-radius: 999px;
  background: var(--panel);
  color: var(--ink);
  min-height: 38px;
  padding: 0 16px;
  font-size: 13.5px;
  cursor: pointer;
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
.remove-file:hover { border-color: var(--line-strong); background: #fff; }

/* ── Confirm ───────────────────────────────────────────────────────────── */
.consent {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 13px;
  padding: 16px 18px;
  border: 1px solid var(--panel-line);
  border-radius: var(--panel-radius);
  background: var(--panel);
  color: var(--muted);
  font-size: 14px;
  line-height: 1.55;
  cursor: pointer;
  transition: border-color .2s var(--ease);
}
.consent:hover { border-color: rgba(21,23,20,.2); }
/* The real input stays in the DOM and keeps `required`, keyboard focus and
   form validation — it is only visually replaced, never hidden from the
   accessibility tree with display:none. */
.consent input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  position: absolute;
  /* pointer-events:none is load-bearing. The input sits underneath the drawn
     box, so a click could land on the INPUT (toggle) and then be forwarded
     again by the wrapping <label> (toggle back) — a net no-op that made the
     tick look like it randomly refused to take. Taking the input out of hit
     testing leaves exactly one path: click label -> toggle once.
     Keyboard is unaffected — Tab still lands on it, Space still toggles. */
  pointer-events: none;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
}
.consent-box {
  position: relative;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  border: 1.5px solid rgba(21,23,20,.28);
  border-radius: 6px;
  background: #fff;
  transition: background .18s var(--ease), border-color .18s var(--ease);
}
.consent-box::after {
  content: "";
  position: absolute;
  left: 6px;
  top: 2px;
  width: 5px;
  height: 10px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) scale(.6);
  opacity: 0;
  transition: opacity .16s var(--ease), transform .16s var(--ease);
}
.consent input[type="checkbox"]:checked ~ .consent-box {
  background: var(--ink);
  border-color: var(--ink);
}
.consent input[type="checkbox"]:checked ~ .consent-box::after {
  opacity: 1;
  transform: rotate(45deg) scale(1);
}
.consent input[type="checkbox"]:focus-visible ~ .consent-box {
  border-color: var(--ink);
  box-shadow: 0 0 0 3px rgba(21,23,20,.14);
}

.form-actions { display: grid; gap: 12px; align-items: start; }
.form-status { margin: 0; color: var(--muted); font-size: 14px; }
.form-status code { font-size: 13px; }

.toast {
  position: fixed;
  z-index: 120;
  left: 50%;
  bottom: 24px;
  transform: translate(-50%, 20px);
  background: var(--ink);
  color: var(--white);
  padding: 14px 18px;
  border-radius: 999px;
  font-size: 14px;
  opacity: 0;
  pointer-events: none;
  transition: .4s var(--ease);
}
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }

.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .8s var(--ease), transform .8s var(--ease);
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }

@media (max-width: 1100px) {
  .pricing-editorial,
  .film-demo-grid { grid-template-columns: 1fr; }
  .featured-plan { grid-template-columns: 1fr; }
  .trust-grid { grid-template-columns: repeat(2, 1fr); }
  .trust-item:nth-child(2), .trust-item:nth-child(4) { border-right: 0; }
}

@media (max-width: 900px) {
  .pricing-row-body { grid-template-columns: 1fr; }
  body { font-size: 17px; }
  .site-header { height: 76px; }
  .header-actions a,
  .button-nav { display: none; }
  .hero-scroll { height: 270vh; }
  .hero-sticky { min-height: min(640px, 100svh); }
  .hero-visual picture { display: contents; }
.hero-image { min-width: 175%; height: calc(100% + 4px); right: -2px; left: auto; }
  .hero-ui { padding-top: 96px; }
  .hero-copy-wrap { width: min(100%, 700px); height: 52vh; }
  .hero-copy h1, .hero-copy h2 { font-size: clamp(46px, 10vw, 76px); }
  .hero-sub { font-size: 17px; }
  .scroll-meter { display: none; }
  .hero-bottom { position: relative; left: auto; right: auto; bottom: auto; flex-direction: column; align-items: flex-start; gap: 18px; margin-top: 18px; }
  .hero-proof-wrap { align-items: flex-start; }
  .hero-trust-line, .hero-proof { text-align: left; justify-content: flex-start; }
  .section-heading { grid-template-columns: 1fr; gap: 20px; }
  .examples-grid { grid-template-columns: 1fr 1fr; }
  .upload-package-bar, .field-row.two-up, .site-footer { grid-template-columns: 1fr; }
  /* .uploader-copy is a FLEX row, so grid-template-columns did nothing to it —
     it stayed side-by-side on phones and squeezed "One photo. One moving shot."
     down to four lines in a 120px column. */
  .uploader-copy { flex-direction: column; gap: 12px; }
  .uploader-copy p { max-width: none; }
  /* Stacked, the divider has to move from the side to the top or it hangs off
     the left of a full-width block. */
  .package-sidecopy {
    text-align: left;
    max-width: none;
    padding-left: 0;
    padding-top: 16px;
    border-left: 0;
    border-top: 1px solid var(--panel-line);
  }
}

@media (max-width: 640px) {
  .section-shell { width: min(100% - 30px, 1280px); }
  .site-header { padding-inline: 16px; }
  .brand { font-size: 11px; gap: 9px; }
  .brand-mark { width: 15px; height: 15px; }
  .brand-mark::after { left: 7px; height: 22px; }
  .button-small { min-height: 40px; padding-inline: 14px; }
  .hero-scroll { height: 245vh; }
  .hero-visual picture { display: contents; }
.hero-image { min-width: 210%; height: calc(100% + 4px); right: -2px; left: auto; }
  .hero-kicker { font-size: 9px; margin-bottom: 28px; }
  .hero-copy-wrap { height: 48vh; }
  .hero-copy h1, .hero-copy h2,
  .section-heading h2,
  .featured-plan h3,
  .property-film-copy h3,
  .final-cta h2,
  .modal-head h2 { font-size: clamp(38px, 12vw, 62px); }
  .hero-sub { font-size: 16px; }
  .hero-cta-row { width: 100%; flex-direction: column; }
  .hero-cta-row .button { width: 100%; }
  .hero-proof { font-size: 11px; }
  .pricing-section { padding-top: 96px; }
  .featured-plan,
  .support-plan,
  .final-cta-inner,
  .modal-shell { padding: 22px; }
  .trust-grid { grid-template-columns: 1fr; }
  .trust-item, .trust-item:nth-child(2), .trust-item:nth-child(3), .trust-item:nth-child(4) {
    padding: 26px 0;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }
  .trust-item:last-child { border-bottom: 0; }
  .examples-grid { grid-template-columns: 1fr; }
  .example-card, .support-plan, .featured-plan, .final-cta-inner { box-shadow: none; }
  .modal { width: min(100% - 16px, 1180px); max-height: calc(100vh - 16px); }
  .dialog-close { top: 12px; right: 12px; }
  .upload-support-row { flex-direction: column; }
  .file-item { grid-template-columns: 56px 1fr; }
  .remove-file { grid-column: 2; justify-self: start; }
  .final-actions { flex-direction: column; }
}

@media (prefers-reduced-motion: reduce) {
  .pricing-heading,
  .pricing-card {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  /* The hero pan is scroll-linked, not auto-playing — it only moves when the
     visitor moves, so it stays under reduced motion (the script drops the
     easing and tracks scroll 1:1). It must NOT be frozen with !important:
     iOS "Reduce Motion" is commonly enabled, and that rule beat the inline
     transform, killing the pan entirely on those phones. */
  .hero-visual picture { display: contents; }
.hero-image { will-change: auto; }
}

@media (max-width: 980px) {
  .pricing-shell { width: min(560px, 100%); }
  .pricing-cards { grid-template-columns: 1fr; gap: 26px; }
  .pricing-card { min-height: auto; }
  .pricing-card-summary { min-height: 0; }
}

@media (max-width: 560px) {
  .pricing-section { padding-inline: 15px; }
  /* Same +1px on top as the desktop rule, for the same reason. */
  .pricing-card { padding: 25px 21px 24px; }
  .pricing-card h3 { font-size: 23px; }
  /* The tag shrinks with the card, and the title's reserved gutter shrinks
     with it — by exactly the same amount, so the two never collide. The top
     hairline reads the same variable, so it keeps stopping at the tag. */
  .pricing-card { --price-tag-w: 118px; }
  .pricing-price-tag { width: var(--price-tag-w); height: 78px; padding: 11px 15px 10px; }
  .price-figure { font-size: 32px; }
  .price-figure-word { font-size: 22px; }
  .pricing-card-topline { padding-top: 64px; }
  .pricing-heading > p:last-child { font-size: 16px; }
}

/* ── Checkout ─────────────────────────────────────────────────────────────
   Payment lives inside the same dialog as the upload form. The customer never
   leaves the site: photos go up, the form hides, Stripe's embedded form takes
   its place. Two steps, one place. */
.checkout-panel {
  margin-top: 6px;
  padding-top: 26px;
  border-top: 1px solid var(--line);
}
.checkout-head { max-width: 720px; }
.checkout-head h3 {
  margin: 8px 0 0;
  font-size: clamp(24px, 3vw, 34px);
  line-height: 1.15;
  letter-spacing: -.015em;
}
.checkout-note { margin: 12px 0 0; color: var(--muted); font-size: 14px; }
#checkoutMount { margin-top: 26px; min-height: 320px; }
/* Hosted-link mode puts a single button here instead of Stripe's iframe, so
   the 320px reservation would leave it floating in dead space. */
#checkoutMount:has(.checkout-pay) { min-height: 0; }
.checkout-pay {
  display: inline-flex;
  font-size: 17px;
  padding: 18px 34px;
}
.checkout-fallback {
  margin: 22px 0 0;
  padding: 18px 20px;
  background: var(--paper-2);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: 15px;
}
.checkout-trust {
  margin: 18px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 18px;
  color: var(--muted);
  font-size: 13px;
}
.checkout-trust i {
  width: 4px; height: 4px; border-radius: 50%;
  background: var(--line-strong); align-self: center;
}
@media (max-width: 640px) {
  #checkoutMount { min-height: 420px; }
  #checkoutMount:has(.checkout-pay) { min-height: 0; }
  .checkout-pay { width: 100%; justify-content: center; }
  .checkout-panel { padding-top: 20px; }
}

/* ════════════════════════════════════════════════════════════════════════════
   PHONE HERO
   Three separate problems lived here. Each has a specific cause and a specific
   fix, and they are independent — the earlier attempts failed by treating them
   as one thing and tuning.

   1. THE BLACK BAR. svh is the viewport height with the browser toolbars
      SHOWING; lvh is with them HIDDEN. Both are constants. Size the sticky
      pane in svh and it is too short the instant the toolbar retracts, and the
      strip left underneath shows the section behind — that is the bar, and it
      appeared only after scrolling because that is when the toolbar goes.
      Size the pane in lvh instead and it can never be shorter than the
      viewport; the worst case is that it is slightly TALLER and the surplus is
      image below the fold, which nobody can see.
      Pane in lvh. Content in svh, so it still fits while the toolbar shows.

   2. THE CLIPPING. Everything in the hero was sized against viewport WIDTH,
      but height is the constraint — and Safari's usable height is ~100px less
      than the device because the toolbars sit inside it. The headline now
      takes the smaller of its width- and height-based size, and the gaps,
      sub-heading and button heights scale in svh.

   3. THE STUTTER. script.js pans by reading window.scrollY every frame, and
      iOS defers scroll events during momentum scrolling, so the plate lags.
      A CSS scroll timeline is the better mechanism in theory — but a browser
      that reports support and then does not engage leaves NO pan at all, which
      is far worse than one that judders. So the script drives it everywhere,
      and the cost is cut instead: no backdrop-filter over the moving layer, a
      third less texture to move, and the vignette's 34vh blur replaced by
      gradients.
   ════════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .hero-sticky {
    height: 100lvh;
    min-height: 100svh;
  }
  .hero-ui {
    height: 100svh;
    padding-top: clamp(72px, 11svh, 96px);
    padding-bottom: clamp(28px, 5svh, 60px);
    /* MUST be a flex column. .hero-copy-wrap below is flex:1 so it absorbs
       whatever height is left after the fixed parts — and flex does nothing
       without a flex parent. Without this the wrap collapses to zero (its
       stage copy is absolutely positioned, so it contributes no height) and
       the kicker, headline, buttons and trust lines all stack on top of each
       other. */
    display: flex;
    flex-direction: column;
  }
  .hero-copy-wrap { height: auto; flex: 1 1 auto; min-height: 0; }
  /* Pushed to the bottom of whatever room is left, rather than following the
     copy immediately. */
  .hero-bottom { margin-top: auto; }
  .hero-kicker { margin-bottom: clamp(10px, 2.2svh, 28px); }
  .hero-copy h1,
  .hero-copy h2 { font-size: clamp(32px, min(9.6vw, 7.4svh), 68px); }
  .hero-sub { font-size: clamp(15px, 2.1svh, 18px); margin-top: clamp(8px, 1.5svh, 18px); }
  .hero-bottom { gap: clamp(10px, 1.8svh, 18px); }
  .hero-cta-stack { gap: clamp(9px, 1.5svh, 16px); }
  .hero-cta-row { gap: clamp(8px, 1.4svh, 12px); }
  .hero-ui .button-large { min-height: clamp(46px, 6.4svh, 58px); }
  .hero-proof-wrap { gap: clamp(7px, 1.2svh, 14px); }
  .hero-trust-line { font-size: clamp(12px, 1.7svh, 14px); }

  /* Blur over a moving layer can never be cached, so iOS re-rasterised these
     on every frame of the pan. Flat translucent fills look near-identical over
     a dark photograph and cost nothing. */
  .site-header.is-light { backdrop-filter: none; background: rgba(245, 241, 233, 0.94); }
  .button-secondary { backdrop-filter: none; background: rgba(255,255,255,.16); }

  /* 150%, not 175/210%: a 3x phone was pushing roughly 1200x2600 device pixels
     every frame. This keeps real travel and cuts about a third of the area. */
  .hero-image { min-width: 150%; }

}


/* Superseded by the 600px rule at the end of this file. The proof row that
   made this block tall is now hidden on every phone, so a single line of
   summary text still fits comfortably at 640px. */

@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .hero-image { animation: none; }
}


/* ---------------------------------------------------------------------------
   Phone finishing pass. Each rule below is here because it was measured
   failing on a real phone viewport, not because it looked tidier in source.
   --------------------------------------------------------------------------- */
@media (max-width: 900px) {
  /* The blur was removed above (it re-rasterised over the moving pan every
     frame), but the fill was left translucent — and a 6% window onto dark body
     text over cream reads as a grey smear with the page's own words legible
     through the bar. Without a blur behind it there is nothing for translucency
     to buy, so the fill is simply opaque. */

  /* MEASURED, not eyeballed. With the previous scrim the worst backdrop pixel
     under the headline gave white text a contrast ratio of 1.6:1 — on a pale
     patio frame the copy effectively disappeared, and because the plate pans,
     the bright region slides under different lines as you scroll. The desktop
     wash is a left-to-right gradient built for copy that sits in the left
     third; on a phone the text spans the full width and nearly the full
     height, so it needs a vertical scrim instead. Alphas below are tuned to
     the darkest the photo needs to be for every band to clear its WCAG
     threshold at every scroll position, and no darker. */
  /* FLAT, and that is the whole point. A gradient scrim darkens the top and
     bottom edges of the photo more than its middle, and once viewport-fit=cover
     pushes those edges into the status-bar and home-indicator zones, the dark
     ends read as bars framing the site. Measured: the previous ramp left the
     bottom band at 46% of the middle's luminance — less than half as bright —
     while the photograph's own bottom (pale concrete floor) is actually 150%
     of its middle. The scrim was inventing a dark bar that the image does not
     have. One even value across the whole plate: no edge is darker than any
     other, so there is nothing for the eye to read as a border. .58 rather
     than something lighter because the flat value has to carry the contrast
     the old gradient's dark top was carrying — worst text band measures 4.98:1
     at this level, better than the 4.50:1 the gradient managed. */
  .hero-wash { background: rgba(7,10,8,.58); }
  .hero-vignette { background: none; }

  .site-header.is-light {
    background: #f5f1e9;
    box-shadow: 0 1px 0 rgba(21, 23, 20, 0.08);
  }

  /* The dotted proof row wraps to two lines on every phone, which strands a
     separator dot at the end of the first line — it reads as a sentence that
     got cut off, which is exactly what it was reported as. The three claims
     are already made verbatim in the trust band further down the page, so the
     row goes and the summary line above it stays. */
  .hero-proof { display: none; }

  /* Roughly 110px of empty cream sat between the closing card and the footer
     rule. On a phone that is a sixth of the screen spent on nothing. */
  .final-cta { padding-bottom: clamp(52px, 9vw, 88px); }

  /* padding was `0 0 22px`: the brand line touched the footer's own top border
     and the last line of text sat ~20px off the bottom of the screen, under
     the home indicator on any phone with one. */
  .site-footer {
    min-height: 0;
    gap: 16px;
    padding: 26px 0 calc(30px + env(safe-area-inset-bottom, 0px));
  }
}

/* The proof row is gone on phones, so the wrap is now a single line of summary
   text and there is room for it well below the old cut-off. */
@media (max-width: 900px) and (max-height: 600px) {
  .hero-proof-wrap { display: none; }
}


/* ---------------------------------------------------------------------------
   Dot-separated inline lists (.hero-proof, .pricing-friction-note).
   The separators were standalone <i> elements sitting BETWEEN the spans, so
   when the row wrapped — which it does on every phone and on narrow desktop
   windows — a dot could land as the last thing on a line. It reads as a
   sentence that was cut off mid-way, and that is exactly how it was reported.
   Fix: hide the standalone elements and hang the dot off the FRONT of each
   item after the first, so the separator is part of the item's own inline box
   and can never be stranded on the line above. Each item is inline-flex and
   nowrap so the dot and its label always travel together.
   --------------------------------------------------------------------------- */
.hero-proof i,
.pricing-friction-note i { display: none; }

.hero-proof span,
.pricing-friction-note span {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  white-space: nowrap;
}

.hero-proof span ~ span::before,
.pricing-friction-note span ~ span::before {
  content: "";
  flex: none;
  border-radius: 50%;
  background: currentColor;
}
.hero-proof span ~ span::before { width: 4px; height: 4px; }
.pricing-friction-note span ~ span::before { width: 3px; height: 3px; opacity: .42; }

/* The longest item cannot be nowrap on a narrow phone or it forces a
   horizontal scrollbar — let that one wrap and keep only its dot attached. */
@media (max-width: 620px) {
  .pricing-friction-note span { white-space: normal; }
  .pricing-friction-note span ~ span::before { align-self: center; }
}


/* ---------------------------------------------------------------------------
   Edge-to-edge on a phone (viewport-fit=cover).
   With cover set in the viewport meta, the layout viewport is the WHOLE screen
   — the hero photo now runs under the status bar and behind the home
   indicator, which is the point. The cost is that the safe areas are no longer
   kept clear for us, so every piece of UI that sits near an edge has to add
   the inset back itself or it ends up under the clock or the home bar.
   100svh/100lvh already resolve against the full screen under cover, so the
   hero pane needs no height change — only its padding does.
   --------------------------------------------------------------------------- */
@media (max-width: 900px) {
  /* The bar's fill extends up behind the status bar; its contents do not. */
  .site-header {
    height: calc(76px + var(--safe-top));
    padding-top: var(--safe-top);
    padding-left: calc(16px + var(--safe-left));
    padding-right: calc(16px + var(--safe-right));
  }

  .hero-ui {
    padding-top: calc(clamp(72px, 11svh, 96px) + var(--safe-top));
    padding-bottom: calc(clamp(28px, 5svh, 60px) + var(--safe-bottom));
  }

  /* Already carried its own inset; restated through the variable so the whole
     page uses one mechanism and one test can cover all of it. */
  .site-footer {
    padding-bottom: calc(30px + var(--safe-bottom));
  }

}

/* Landscape is where the side insets are non-zero, and where they would
   otherwise crop the first and last characters of every line. .section-shell
   is sized by width + auto margins, not padding, so the inset comes off the
   width — anything padding-based here would knock the shell off centre. */
@media (max-width: 900px) and (orientation: landscape) {
  .section-shell {
    width: min(1280px, calc(100% - 48px - var(--safe-left) - var(--safe-right)));
  }
}
