/* ══════════════════════════════════════════════════════════════════════
   ReadMyLand — landing v3 (TEST PAGE)

   Additions only. Every colour, face and rhythm comes from
   css/landing-v2.css, which this page loads first and which DESIGN.md
   makes canonical; nothing here redefines a token.

   Scoped under `.rml-v3` so loading this sheet can never reach the
   homepage, /explore or the sample pages if it is ever linked from them
   by accident.
   ══════════════════════════════════════════════════════════════════════ */

/* ── The states ──────────────────────────────────────────────────────
   Each state owns the viewport in turn. `hidden` is a UA display:none and
   nothing here sets `display` on .v3-state, so the attribute alone
   suffices — no state needs a class to be hidden, which is what keeps
   js/landing-v3.js free of a second source of truth about visibility. */
/* THE STAGE IS SIZED TO THE FOLD, not to the viewport. At `100svh` it
   started 87px down the page (54px sticky nav + 33px promo ribbon) and so
   overflowed the fold by exactly that much, which is why the reel needed a
   scroll to be seen whole and why handing over to the map moved the page.
   Subtracting the chrome makes every state end where the screen ends: the
   reel, the trace and the render are then the SAME BOX, and a cross-dissolve
   between them reflows nothing and scrolls nowhere.
   --v3-promo-h is declared rather than measured so the arithmetic cannot
   drift if the ribbon's copy wraps; the ribbon is pinned to it below. */
.rml-v3 {
  --v3-promo-h: 34px;
  /* vh FIRST, svh only where svh exists — and via @supports, not via a
     second declaration. A custom property is parsed as arbitrary tokens, so
     `--v3-fold: calc(100vh - ...)` followed by `--v3-fold: calc(100svh - ...)`
     always resolves to the SECOND: nothing about `100svh` is invalid until it
     is substituted into `height`, at which point the declaration that consumed
     it is dropped at computed-value time and falls back to its INITIAL value.
     Every .v3-stage would then take `height: auto` and be carried by the
     460px min-height alone — the fold arithmetic this block exists for, gone,
     on Chrome <108 and Safari <15.4. Same shape and same reasoning as
     explore.css's --tv-sheet. */
  --v3-fold: calc(100vh - var(--nav-h) - var(--v3-promo-h));
  /* Apple-ish: quick to commit, long tail. Never the browser default `ease`,
     which decelerates too early and reads as a stop rather than an arrival. */
  --v3-ease: cubic-bezier(.32,.08,.24,1);
}
@supports (height: 100svh) {
  .rml-v3 { --v3-fold: calc(100svh - var(--nav-h) - var(--v3-promo-h)); }
}
.rml-v3 .v3-stage {
  position: relative;
  height: var(--v3-fold);
  min-height: 460px;
  overflow: hidden;
  background: var(--bg-deep);
}
/* The result state keeps a full-height map: a long report follows it, so
   scrolling is expected there and the fold is not the boundary. */
.rml-v3 .v3-stage-tall { height: var(--v3-fold); }

/* The traced map is the other half of "same box". landing-v2.css sizes
   .draw-stage at 100svh for a page whose map IS the first element; here it
   sits under the same chrome as the reel and has to match it. */
.rml-v3 .draw-stage { height: var(--v3-fold); }

/* The reel's still frame — the fallback the live viewer draws over, and
   the whole content on the no-WebGL and reduced-motion paths. */
.rml-v3 .v3-reel-media,
.rml-v3 .v3-render-media {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
}
/* NOT the reel's poster. This is the six minutes a reader waits for THEIR
   parcel, and it used to play over a dimmed photograph of Zaytuna Farm —
   someone else's land, on the one page whose entire claim is that this is a
   real parcel being read rather than a picture of one.

   What goes here instead is the ring they just traced, drawn by
   js/landing-v3.js as an inline SVG at submit(). No imagery rights, no tile
   fetches, and it is literally the thing being read. The satellite plate was
   the obvious alternative and is the wrong one: it arrives in the COMPLETION
   payload, i.e. at the end of the wait it was meant to fill.

   The radial is a GROUND, not decoration. `.v3-prog` is `rgba(10,12,8,.95)`
   with a hairline border — a panel designed to float over something lit,
   which the poster used to be. Removing the poster and putting nothing back
   left a near-black card on near-black ground: measured in a browser, the
   card was still there at 560x204 with opacity 1 and simply could not be
   read. */
.rml-v3 .v3-render-media {
  display: flex; align-items: center; justify-content: center;
  background:
    radial-gradient(120% 90% at 50% 45%,
                    rgba(122, 148, 92, 0.11) 0%,
                    rgba(122, 148, 92, 0.04) 42%,
                    rgba(0, 0, 0, 0) 72%);
}
.rml-v3 .v3-render-ring {
  /* Bigger than the progress card, or the reader never sees the shape they
     traced — which is this element's whole argument. Measured before: a 420px
     ring centred inside a 560px card showed only its top and bottom caps, and
     on a 390x844 phone a 203px ring sat entirely inside a 343px card. Both are
     centred on the same point, so the ring has to read AROUND the card. */
  width: min(86vmin, 720px); height: min(86vmin, 720px);
  overflow: visible; opacity: .55;
}
.rml-v3 .v3-render-ring polygon {
  fill: rgba(122, 148, 92, 0.10);
  stroke: var(--accent, #7a945c);
  stroke-width: 2; stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
@media (prefers-reduced-motion: no-preference) {
  /* THE BLESSED IDIOM, not a new one. DESIGN.md §5 names exactly one animated
     decoration — the dashed boundary on the map polygon, `stroke-dashoffset`
     looped over 18s, "slow enough to read as 'this is live, not a
     screenshot.'" This is the same element doing the same job, so it uses the
     same property at the same tempo. The first cut pulsed opacity every 4s:
     ~90 pulses across a six-minute wait, which is the loading-spinner
     register DESIGN.md §1 rejects. */
  .rml-v3 .v3-render-ring polygon {
    stroke-dasharray: 6 7;
    animation: v3RingTrace 18s linear infinite;
  }
}
@keyframes v3RingTrace {
  to { stroke-dashoffset: -260; }
}

/* Two veils, two jobs. The side veil is a declared margin for the copy —
   it must not depend on what happens to be underneath, because a scrim
   that works over a warm pipeline plate fails over limestone or snow. The
   bottom veil only seats the chrome. */
.rml-v3 .v3-veil {
  position: absolute; inset: 0;
  background: linear-gradient(100deg,
    rgba(11,12,10,.94) 0%, rgba(11,12,10,.80) 30%,
    rgba(11,12,10,.18) 60%, rgba(11,12,10,0) 82%);
}
.rml-v3 .v3-veil-b {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(11,12,10,.55) 0%, rgba(11,12,10,0) 26%,
    rgba(11,12,10,0) 62%, rgba(11,12,10,.88) 100%);
}

/* ── Reel chrome ─────────────────────────────────────────────────────── */
.rml-v3 .v3-reeltag {
  position: absolute; left: 22px; top: 20px; z-index: 6;
  display: flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: 10px;
  letter-spacing: .15em; text-transform: uppercase; color: var(--accent);
  background: rgba(10,12,8,.82);
  border: 1px solid var(--accent-soft); padding: 7px 12px;
}
.rml-v3 .v3-rec {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--caution-rust);
}
/* No Skip chip (2026-09-26). It and "Draw my parcel" led to the same place,
   and a reader who pressed the corner chip mid-tour landed wherever the
   camera happened to be. One button, one road (js/landing-v3.js::goDraw). */

/* "Start tracing" below the zoom a corner can be placed at: pressable, but it
   looks like not yet, because what it does there is point at the search
   (js/landing-v3.js::armLocateGate). */
.rml-v3 #startTracing[aria-disabled="true"] { opacity: .55; }

/* ── The guided tour's caption ────────────────────────────────────────
   One card per lesson, lit by js/landing-v3.js as the storyboard paints
   that lesson's layer. Cards grid-stack in one cell so the swap can
   cross-fade — display:none cannot transition.

   Opaque on purpose: the card sits over the live MapLibre canvas, where a
   backdrop-filter is a compositor readback and blur every frame
   (explore.css records the ruling twice). Upper-left under the live tag —
   Henk's pick from a three-placement demo (2026-08-27); the hero text
   yields the frame to it below, and both exits — the CTA and the Skip
   chip — keep full opacity from the first frame to the last. */
/* The list is in the layout from page load — invisible, because every card
   is, but PRESENT, so the aria-live region is registered with assistive
   tech before it ever has something to announce; several screen readers
   only reliably track live regions that were rendered at load. What
   `is-touring` gates is the beat strip (two narrations must not compete)
   and the hero fade below — a card only ever lights via markTour. */
.rml-v3 .v3-tour {
  display: grid; position: absolute; z-index: 6;
  left: 22px; top: 64px; max-width: 460px;
  margin: 0; padding: 0; list-style: none;
}
.rml-v3 #reel.is-touring .v3-beats { display: none; }

/* Chrome lives on the CARD, not the list. The first two seconds of the
   tour are bare ground with no lesson lit; a list that carried the fill
   and border painted an empty framed panel over the map for that window.
   Hidden cards also leave the accessibility tree, which is what makes each
   swap a real change the aria-live region can announce — visibility rides
   out with the fade (delayed .45s) and snaps in with the incoming card. */
.rml-v3 .v3-tour-card {
  grid-area: 1 / 1; display: flex; flex-direction: column; gap: 8px;
  padding: 18px 20px; opacity: 0; visibility: hidden;
  background: rgba(10,12,8,.92);
  border: 1px solid var(--accent-soft);
  transition: opacity .45s var(--v3-ease), visibility 0s linear .45s;
}
.rml-v3 .v3-tour-card.is-now {
  opacity: 1; visibility: visible;
  transition: opacity .45s var(--v3-ease), visibility 0s;
}
.rml-v3 .v3-tour-k {
  font-family: var(--mono); font-size: 10px; letter-spacing: .15em;
  text-transform: uppercase; color: var(--accent); white-space: nowrap;
}
.rml-v3 .v3-tour-d {
  font-family: var(--serif); font-size: 19px;
  line-height: 1.45; color: var(--text);
}

/* The hero copy steps back while the tour narrates. The skipline is
   deliberately NOT in this list: it is exit chrome — the sentence that
   explains the CTA and that the reel ends on its own — and the two exits
   keep full strength from the first frame to the last. */
.rml-v3 #reel.is-touring .v3-h1,
.rml-v3 #reel.is-touring .v3-lede,
.rml-v3 #reel.is-touring .v3-proof {
  opacity: .16; transition: opacity .6s var(--v3-ease);
}

/* ── Hero copy ───────────────────────────────────────────────────────── */
.rml-v3 .v3-copy {
  position: absolute; left: 6vw; top: 50%; transform: translateY(-50%);
  max-width: 520px; z-index: 5;
}
.rml-v3 .v3-h1 {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(38px, 6vw, 60px); line-height: 1.02;
  letter-spacing: -.012em; margin: 0 0 18px;
}
.rml-v3 .v3-lede {
  font-size: clamp(16px, 2vw, 19px); line-height: 1.5;
  color: var(--text-dim); margin: 0 0 18px;
}
.rml-v3 .v3-proof {
  font-family: var(--mono); font-size: 11px; letter-spacing: .06em;
  color: var(--text-mute); margin: 0 0 22px;
}
.rml-v3 .v3-cta-row { display: flex; gap: 14px; flex-wrap: wrap; }
.rml-v3 .v3-skipline {
  font-family: var(--mono); font-size: 10.5px; line-height: 1.8;
  letter-spacing: .04em; color: var(--text-mute); margin: 14px 0 0;
}

/* ── Beat strip ──────────────────────────────────────────────────────
   What the sequence contains, in words — the static fallback for the
   paths where the guided tour never runs (reduced motion, no WebGL, a
   failed parcel). While the tour runs it is hidden (#reel.is-touring
   above) and the caption cards carry the narration; nothing lights these
   cells any more. */
.rml-v3 .v3-beats {
  position: absolute; left: 0; right: 0; bottom: 3px; z-index: 6;
  display: flex; margin: 0; padding: 0; list-style: none;
  border-top: 1px solid var(--border);
  background: linear-gradient(180deg, rgba(10,12,8,.72), rgba(10,12,8,.94));
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}
.rml-v3 .v3-beat {
  flex: 1; padding: 15px 20px;
  border-right: 1px solid var(--border);
}
.rml-v3 .v3-beat:last-child { border-right: 0; }
.rml-v3 .v3-beat-t {
  display: block; font-family: var(--mono); font-size: 9.5px;
  letter-spacing: .14em; color: var(--accent); margin-bottom: 7px;
}
.rml-v3 .v3-beat-d {
  display: block; font-family: var(--serif);
  font-size: 15px; line-height: 1.4; color: var(--text-dim);
}

/* ── Promo ribbon ────────────────────────────────────────────────────── */
/* `display:flex` outranks the `hidden` attribute, so the ribbon ships
   `hidden` in the markup and renders anyway. Latent rather than harmless:
   paintPromo() un-hides it on every load and nothing ever hides it again, so
   the first path that needs to would find that it cannot. */
.rml-v3 .v3-promo[hidden] { display: none; }
.rml-v3 .v3-promo {
  display: flex; align-items: center; gap: 12px;
  height: var(--v3-promo-h); box-sizing: border-box;
  padding: 0 6vw;
  background: var(--accent-bg);
  border-bottom: 1px solid var(--accent-soft);
  font-family: var(--mono); font-size: 11px;
  letter-spacing: .13em; text-transform: uppercase; color: var(--accent);
}
.rml-v3 .v3-promo-dot {
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent);
  flex: 0 0 auto;
}
.rml-v3 .v3-promo-tail {
  margin-left: auto; color: var(--text-mute);
  letter-spacing: .05em; text-transform: none;
}

/* ── State 3 · progress ──────────────────────────────────────────────── */
.rml-v3 .v3-tsmark {
  position: absolute; left: 0; top: 0; z-index: 6;
  background: #000; border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  font-family: var(--mono); font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--accent); padding: 8px 14px;
}
/* Placed by .v3-render-col below — never positioned on its own. */
.rml-v3 .v3-prog {
  width: min(560px, 88vw);
  background: rgba(10,12,8,.95); border: 1px solid var(--border-hi);
  padding: 26px 28px;
}
.rml-v3 .v3-prog-l {
  font-family: var(--serif); font-size: 22px; margin: 0 0 7px;
}
.rml-v3 .v3-prog-s {
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-mute); margin: 0 0 18px;
}
.rml-v3 .v3-prog-bar {
  height: 4px; background: rgba(255,255,255,.10); margin-bottom: 11px;
}
.rml-v3 .v3-prog-bar i {
  display: block; height: 100%; background: var(--accent);
  transition: width .5s ease;
}
.rml-v3 .v3-prog-f {
  display: flex; margin: 0;
  font-family: var(--mono); font-size: 10px; letter-spacing: .08em;
  color: var(--text-mute);
}
.rml-v3 .v3-prog-f b { margin-left: auto; color: var(--accent); font-weight: 400; }
/* The way back, revealed only by failRun().
   `hidden` alone does NOT hide it: that attribute is a UA-stylesheet
   `display:none`, and `.cta` sets an author-level `display:inline-flex` which
   wins the cascade outright. Measured on the shipped page — the button read
   `hidden === true` and `display: inline-flex` at the same time, so "Back to
   the map" sat inside the progress card of every healthy run, inviting the
   reader to abandon a job that was going fine.

   This is the mirror of the trap landing-v3.html records for the voucher row,
   where an author `display:none` beat `hidden = false`. Whenever a control
   carries a class with its own `display`, the hidden state needs a rule of
   its own — a JS spec cannot see this, because jsdom applies no author CSS.
   tests/test_v3_test_page.py carries the guard, derived from the markup. */
.rml-v3 .v3-prog-retry { margin-top: 16px; }
.rml-v3 .v3-prog-retry[hidden] { display: none; }

.rml-v3 .v3-render-note {
  text-align: center; font-family: var(--mono); font-size: 10px;
  letter-spacing: .07em; color: var(--text-faint); margin: 0;
}

/* The status card and the note are one column.
   CONTAINMENT, not measurement (DESIGN.md, 2026-08-21): the card used to be
   centred on its own and the note pinned 26px off the bottom, which was fine
   for one line of text. With the notify form in the note, a landscape phone's
   460px stage left 13px between them, and one wrapped stage label or one
   error line closed it. Two grid rows cannot overlap.
   Three rows, `1fr auto 1fr`, with the card in the middle and the note
   hanging from the top of the third: the CARD stays centred on the stage
   while the note grows or shrinks under it, so an error line appearing or the
   form giving way to its confirmation does not move the thing the reader is
   watching. (Centring the two as a group did: every change in the note's
   height shifted the card by half of it.) An empty first row shrinks to
   nothing, so a column taller than the stage starts at the top rather than
   losing it, and `overflow-y: auto` scrolls what is left instead of clipping
   the field. The 46px top padding clears the tsmark label. */
.rml-v3 .v3-render-col {
  position: absolute; inset: 0; z-index: 7;
  display: grid; grid-template-rows: 1fr auto 1fr; justify-items: center;
  row-gap: 22px; padding: 46px 0 22px; box-sizing: border-box;
  overflow-y: auto;
}
.rml-v3 .v3-render-col > .v3-prog { grid-row: 2; }
.rml-v3 .v3-render-col > .v3-render-note { grid-row: 3; align-self: start; }
/* The same measure as the card and the form, so the three read as one column
   instead of a wide line of type under two narrow blocks — and --text-mute,
   not the note's faint ink: "the link brings you back" is an instruction the
   reader acts on, and --text-faint is DESIGN.md's disabled/very faint
   metadata ink. */
.rml-v3 .v3-render-close {
  margin: 0 auto; max-width: min(560px, 88vw); color: var(--text-mute);
}
.rml-v3 .v3-render-close[hidden] { display: none; }

/* "Email me when it's ready." The same width as the status card above it, so
   the two read as one column; the field and its button follow the paid
   email row on the homepage (.paid-email-row in landing-v2.css) rather than
   inventing a third form style. The input keeps landing-v2's 16px so iOS does
   not zoom the page on focus — no font-size is set here on purpose.
   `display: flex` beats the `hidden` attribute (a UA-stylesheet
   `display: none`), so the hidden state has a rule of its own;
   tests/test_v3_test_page.py fails any control that ships without one. */
.rml-v3 .v3-notify {
  display: flex; flex-direction: column; gap: 8px;
  width: min(560px, 88vw); margin: 0 auto 14px; text-align: left;
}
.rml-v3 .v3-notify[hidden] { display: none; }
.rml-v3 .v3-notify-l {
  font-family: var(--mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--text-dim);
}
.rml-v3 .v3-notify-row { display: flex; gap: 8px; align-items: stretch; }
.rml-v3 .v3-notify-row input {
  flex: 1 1 auto; min-width: 0;
  padding: 12px 14px;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border-hi);
  font-family: var(--mono);
}
/* The house input focus (landing-v2.css's voucher field): a ring, never
   `outline: none`, which leaves a keyboard reader with nothing to follow. */
.rml-v3 .v3-notify-row input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.rml-v3 .v3-notify-row .cta { padding: 12px 20px; font-size: 13px; white-space: nowrap; }
/* On a phone the end form's longer button ("Email me the link") left the
   field about 140 px of text at 375 px wide, too narrow to show the address
   being typed: the row stacks there, field then button, each full width. The
   waiting card's short "Notify me" keeps its one row. */
@media (max-width: 480px) {
  .rml-v3 .v3-notify-end .v3-notify-row { flex-wrap: wrap; }
  .rml-v3 .v3-notify-end .v3-notify-row input,
  .rml-v3 .v3-notify-end .v3-notify-row .cta { flex: 1 1 100%; }
  /* Off the row it no longer stretches to the field's height: 42 px, under
     the 44 px touch floor, until held there. */
  .rml-v3 .v3-notify-end .v3-notify-row .cta { justify-content: center; min-height: 44px; }
}
.rml-v3 .v3-notify-row .cta[aria-disabled="true"] { opacity: .55; cursor: default; }
/* Busy refuses the press, so it must not answer the pointer either:
   landing-v2's .cta-secondary hover and press styles would turn it sage and
   nudge it while it declines. */
.rml-v3 .v3-notify-row .cta[aria-disabled="true"]:hover,
.rml-v3 .v3-notify-row .cta[aria-disabled="true"]:active {
  border-color: var(--border-hi); color: var(--text); transform: none;
}
/* The consent sentence, at --text-dim and 11px — a step above the close
   line's --text-mute 10px: a promise has to be legible, and it is the one
   sentence on the card the reader is agreeing to. */
.rml-v3 .v3-notify-fine {
  margin: 0; font-family: var(--mono); font-size: 11px;
  letter-spacing: .07em; color: var(--text-dim);
}
/* Left, under the field it answers — not centred with the close line. */
/* overflow-wrap: the confirmation repeats the typed address, and one long
   address in 11px mono overflows a phone's column without a break rule. */
.rml-v3 .v3-notify-msg {
  width: min(560px, 88vw); margin: 0 auto 10px; text-align: left;
  overflow-wrap: anywhere;
  font-family: var(--mono); font-size: 11px; letter-spacing: .06em;
  line-height: 1.6; color: var(--accent);
}
.rml-v3 .v3-notify-msg[hidden] { display: none; }
/* DESIGN.md's moderate refusal: a state the reader's next action clears
   (fix the address, try again later), so amber, never rust. */
.rml-v3 .v3-notify-msg.is-error { color: var(--caution-amber); }
/* Focused by script when the form it replaces goes (tabindex=-1). A ring
   only for a keyboard reader, the house focus-visible pattern; a pointer
   reader who clicked "Notify me" sees none. */
.rml-v3 .v3-notify-msg:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.rml-v3 .v3-notify-row input[aria-invalid="true"] { border-color: var(--caution-amber); }
/* The focus ring covers the 1px border, and the field keeps focus after
   Enter: without this the reader sees sage ("fine") on the field and amber
   ("wrong") in the line under it — DESIGN.md's two channels disagreeing. */
.rml-v3 .v3-notify-row input[aria-invalid="true"]:focus { outline-color: var(--caution-amber); }

/* "Also send me occasional updates": unticked, the box and its words the
   target, 44px tall (the touch floor the rest of this chrome keeps). The box
   is DRAWN,
   the way explore.css draws the layer checkboxes: landing-v2.css sets
   `appearance: none` on form controls, which measured the native box at
   0x0, and un-setting it brings back a pure-white rounded OS widget that
   DESIGN.md rules out (no pure-white ink, sharp corners). */
/* `align-self: flex-start`: the target is the box and its words, never the
   whole width of the form. Full width, the empty strip right of the words sat
   under the email field, where a phone reader taps to dismiss the keyboard,
   and ticked a consent they never read. `letter-spacing: normal`: on the
   waiting card the label otherwise inherits the mono note's tracking, and
   the same words would read differently in the two forms. */
.rml-v3 .v3-notify-opt {
  display: flex; align-items: center; gap: 10px; min-height: 44px;
  align-self: flex-start;
  font-family: var(--sans); font-size: 13px; letter-spacing: normal;
  color: var(--text-dim);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.rml-v3 .v3-notify-opt input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  position: relative; flex: 0 0 auto;
  width: 16px; height: 16px; margin: 0;
  /* --text-mute, not --border-hi: the unticked box's edge is the only sign
     a consent control is there, and --border-hi measured 1.65:1 on the card
     against WCAG 1.4.11's 3:1 for a control's boundary (5.15:1 now). */
  border: 1px solid var(--text-mute); background: transparent;
  cursor: pointer;
}
.rml-v3 .v3-notify-opt:hover input[type="checkbox"] { border-color: var(--accent); }
.rml-v3 .v3-notify-opt input[type="checkbox"]:checked {
  background: var(--accent); border-color: var(--accent);
}
.rml-v3 .v3-notify-opt input[type="checkbox"]:checked::after {
  content: ""; position: absolute; left: 4px; top: 1px;
  width: 4px; height: 8px;
  border: solid var(--bg); border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg);
}
.rml-v3 .v3-notify-opt input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* At the end of the reading the form sits in the report's own column, left
   and below the reading, not centred on a stage. */
.rml-v3 .v3-notify-end { width: min(560px, 100%); margin: 40px 0 0; }
.rml-v3 .v3-notify-end-msg { width: min(560px, 100%); margin: 12px 0 0; }
/* Once the form gives way to its sentence (sent, a failed run, a full
   parcel), the sentence keeps the form's distance from the reading, or it
   reads as the report's last line; the 16px below keeps its focus ring off
   the voucher hairline. */
.rml-v3 .v3-notify-end[hidden] + .v3-notify-end-msg { margin: 40px 0 16px; }
/* While the form is shown, its fine print (or its amber error line) keeps the
   same 16px off the voucher hairline, or the consent sentence reads as the
   voucher row's. */
.rml-v3 .v3-notify-end:not([hidden]) ~ .voucher-section { margin-top: 16px; }

/* ── Sections ────────────────────────────────────────────────────────── */
.rml-v3 .v3-sh {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(26px, 3.4vw, 34px); margin: 8px 0 12px;
}
.rml-v3 .v3-sp {
  font-size: 16px; line-height: 1.55; color: var(--text-dim);
  max-width: 660px; margin: 0;
}
.rml-v3 .v3-note {
  font-family: var(--mono); font-size: 11px; line-height: 1.7;
  color: var(--text-mute); margin: 18px 0 0; max-width: 720px;
}

.rml-v3 .v3-row { display: flex; gap: 20px; margin-top: 26px; flex-wrap: wrap; }
.rml-v3 .v3-card {
  flex: 1 1 220px; background: var(--bg-card); border: 1px solid var(--border);
  padding: 20px; text-decoration: none; display: block;
  transition: border-color .2s ease, transform .2s ease;
}
.rml-v3 .v3-card:hover { border-color: var(--accent-soft); transform: translateY(-3px); }
.rml-v3 .v3-ck {
  display: block; font-family: var(--mono); font-size: 9.5px;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-mute); margin-bottom: 9px;
}
.rml-v3 .v3-cv {
  display: block; font-family: var(--serif); font-size: 15px;
  line-height: 1.45; color: var(--text-dim);
}

.rml-v3 .v3-tiles {
  display: flex; margin-top: 26px; border: 1px solid var(--border); flex-wrap: wrap;
}
.rml-v3 .v3-tile {
  flex: 1 1 200px; padding: 16px 18px; border-right: 1px solid var(--border);
}
.rml-v3 .v3-tile:last-child { border-right: 0; }
.rml-v3 .v3-tn {
  display: block; font-family: var(--mono); font-size: 9.5px;
  letter-spacing: .16em; color: var(--accent); margin-bottom: 8px;
}
.rml-v3 .v3-tt {
  display: block; font-family: var(--serif); font-size: 17px; margin-bottom: 5px;
}
.rml-v3 .v3-td {
  display: block; font-family: var(--mono); font-size: 10.5px;
  line-height: 1.6; color: var(--text-mute);
}

/* ── Price ───────────────────────────────────────────────────────────
   Three columns, but not the tier grid this page replaced: the free
   column is the hero (accent border, longest list, stated first) and the
   two paid ones are quiet. The decision the reader makes here is whether
   to take work away, not which tier to compare. */
.rml-v3 .v3-freeline {
  font-family: var(--serif); font-size: clamp(20px, 2.6vw, 27px);
  line-height: 1.45; max-width: 820px; margin: 10px 0 0;
}
.rml-v3 .v3-freeline b { color: var(--accent); font-weight: 400; }
.rml-v3 .v3-freeline-sub { margin-top: 18px; }
.rml-v3 .v3-promo-line { margin-top: 28px; font-size: clamp(18px, 2.2vw, 23px); }

.rml-v3 .v3-plans { display: flex; gap: 20px; margin-top: 30px; flex-wrap: wrap; }
.rml-v3 .v3-plan {
  flex: 1 1 260px; background: var(--bg-card);
  border: 1px solid var(--border); padding: 22px 20px;
}
.rml-v3 .v3-plan-free { border-color: var(--accent-soft); }
.rml-v3 .v3-plan-k {
  font-family: var(--mono); font-size: 10px; letter-spacing: .17em;
  text-transform: uppercase; color: var(--text-mute); margin: 0 0 12px;
}
.rml-v3 .v3-plan-free .v3-plan-k { color: var(--accent); }
.rml-v3 .v3-plan-p {
  font-family: var(--serif); font-size: 38px; line-height: 1; margin: 0 0 6px;
}
/* The small mono label is every span in the price line EXCEPT the amounts
   painted from /api/pricing — the parcel's price and the free card's zero —
   which need an element to write into and keep the 38px serif. The zero
   was missing from this exception, so the free card printed its price as a
   10px label beside a 38px €39. */
.rml-v3 .v3-plan-p span:not([data-v4-price]):not([data-v4-free-price]) {
  font-family: var(--mono); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-mute); margin-left: 9px;
}
.rml-v3 .v3-plan-w {
  font-family: var(--mono); font-size: 10px; letter-spacing: .13em;
  text-transform: uppercase; color: var(--text-mute);
  margin: 0 0 16px; padding-bottom: 14px; border-bottom: 1px solid var(--border);
}
.rml-v3 .v3-plan-l { margin: 0; padding: 0; list-style: none; }
.rml-v3 .v3-plan-l li {
  font-family: var(--serif); font-size: 15px; line-height: 1.5;
  color: var(--text-dim); padding: 6px 0 6px 18px; position: relative;
}
.rml-v3 .v3-plan-l li::before {
  content: ""; position: absolute; left: 0; top: 15px;
  width: 8px; height: 1px; background: var(--text-faint);
}
.rml-v3 .v3-plan-free .v3-plan-l li::before { background: var(--accent); }
.rml-v3 .v3-tag {
  display: inline-block; margin: 14px 0 0;
  border: 1px dashed var(--accent-soft); color: var(--accent);
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .14em;
  text-transform: uppercase; padding: 5px 10px;
}
.rml-v3 .v3-tag-q { border-color: var(--border-hi); color: var(--text-mute); }

/* ── One pricing arm ─────────────────────────────────────────────────────
   /v3's subscription arm is retired (2026-09-26) and its markup deleted, so
   the `[data-v3-only]` / `[data-v4-only]` switch that lived here is gone with
   it: the template carries one offer, and a second copy of every offer
   sentence no longer sits in the HTML for view-source, reader mode and
   translation tools to read out. */

/* The parcel card's buy control: the page's primary CTA at the card's own
   width, above the tag. 44px floor like every other buy control. */
.rml-v3 .v3-plan-parcel { border-color: var(--accent-soft); }
.rml-v3 .v3-plan-parcel .v3-plan-k { color: var(--accent); }
.rml-v3 .v3-plan-parcel .v3-plan-l li::before { background: var(--accent); }
.rml-v3 .v3-plan-buy { display: block; width: 100%; min-height: 44px; margin-top: 18px; }
/* The guarantee directly under the card's buy control: its type is
   landing-v2.css's .guarantee-foot (the one owner; this page loads that
   sheet), so this rule is spacing and alignment only. */
.rml-v3 .v3-guarantee { margin: 12px 0 0; text-align: left; max-width: 46ch; }
/* The one road for several parcels: a mail to Henk, not a page. Quiet, under
   the cards, because it is not the offer. */
.rml-v3 .v3-several { margin: 20px 0 0; font-size: 14px; }
.rml-v3 .v3-several a { color: var(--text-dim); text-decoration: underline; text-underline-offset: 3px; }
.rml-v3 .v3-several a:hover { color: var(--accent); }
/* Its disabled look is explore.css's `[data-tv-buy]:disabled` — one rule for
   every buy control; this page loads that sheet. */
/* On /v4 the parcel card is the hero and the free column steps back: two
   accent-bordered cards read as two heroes, and the file's own rule above
   is one hero per grid. */
.rml-v4 .v3-plan-free { border-color: var(--border); }
.rml-v4 .v3-plan-free .v3-plan-k { color: var(--text-mute); }
.rml-v4 .v3-plan-free .v3-plan-l li::before { background: var(--text-faint); }

/* ── FAQ ─────────────────────────────────────────────────────────────── */
.rml-v3 .v3-faq { margin-top: 26px; border-top: 1px solid var(--border); }
.rml-v3 .v3-fq { border-bottom: 1px solid var(--border); padding: 20px 0; }
.rml-v3 .v3-fqq {
  font-family: var(--serif); font-weight: 400; font-size: 20px; margin: 0 0 9px;
}
.rml-v3 .v3-fqa {
  font-family: var(--serif); font-size: 16px; line-height: 1.6;
  color: var(--text-dim); max-width: 800px; margin: 0;
}

/* ── Close ───────────────────────────────────────────────────────────── */
.rml-v3 .v3-close { text-align: center; }
.rml-v3 .v3-close-h { max-width: 20ch; margin-left: auto; margin-right: auto; }
.rml-v3 .v3-close-p {
  font-family: var(--mono); font-size: 11px; letter-spacing: .07em;
  color: var(--text-mute); margin: 0 0 22px;
}

/* ── Phone ───────────────────────────────────────────────────────────
   The copy leaves the side veil and sits on the bottom third, because a
   left-anchored column on a 390px screen leaves the land as a sliver.
   The beat strip stacks: four columns at that width are four words each. */
@media (max-width: 720px) {
  .rml-v3 .v3-copy {
    left: 20px; right: 20px; max-width: none;
    top: auto; bottom: 78px; transform: none;
  }
  .rml-v3 .v3-veil {
    background: linear-gradient(180deg,
      rgba(11,12,10,.55) 0%, rgba(11,12,10,.10) 30%,
      rgba(11,12,10,.82) 62%, rgba(11,12,10,.96) 100%);
  }
  .rml-v3 .v3-beats { flex-wrap: wrap; }
  .rml-v3 .v3-beat { flex: 1 1 50%; border-bottom: 1px solid var(--border); }
  .rml-v3 .v3-promo { flex-wrap: wrap; gap: 6px 12px; }
  .rml-v3 .v3-promo-tail { margin-left: 0; flex: 1 1 100%; }
  .rml-v3 .v3-tile { border-right: 0; border-bottom: 1px solid var(--border); }
  .rml-v3 .v3-tile:last-child { border-bottom: 0; }
}

/* Reduced motion: the reel does not auto-advance (js/landing-v3.js checks
   the same query), so nothing here should animate a progress hairline that
   is not moving. */
@media (prefers-reduced-motion: reduce) {
  .rml-v3 .v3-prog-bar i,
  .rml-v3 .v3-tour-card,
  .rml-v3 .v3-card { transition: none; }
}

/* ══ THE HANDOFF ══════════════════════════════════════════════════════
   The reel does not disappear and the map does not appear. They cross over
   each other, in place, in the same box.

   WHY A CROSS-DISSOLVE AND NOT A SLIDE OR A SCROLL. Both states are the
   same ground seen two ways — a recording of terrain, then live terrain you
   can trace. A slide says "here is a different thing"; a dissolve says
   "the same thing, closer". That is also why the outgoing layer scales UP
   as it leaves rather than shrinking away: the reader moves forward into
   the map, they are not shown a stack of cards.

   THE STAGGER IS THE WHOLE EFFECT. Copy leaves first and fast (~200ms),
   the media follows slowly (~700ms). Simultaneous fades read as a
   brightness change; staggered ones read as one thing giving way to
   another. Nothing here animates `top`/`left`/`height` — only opacity and
   transform, so the whole handoff stays on the compositor. */

.rml-v3 .v3-deck { position: relative; }

/* Only while two states are on screen at once. The deck's height is locked
   in JS for exactly this window, so the page cannot reflow underneath the
   dissolve, and both states are taken out of flow so they overlap. */
.rml-v3 .v3-deck.is-crossfading { overflow: hidden; }
.rml-v3 .v3-deck.is-crossfading > .v3-state {
  position: absolute; inset: 0; width: 100%;
}

/* ONLY THE INCOMING LAYER FADES. The outgoing one keeps full opacity and
   simply gets covered.

   This is not a stylistic preference, it is the fix for a measurable defect.
   Fading both is the textbook cross-dissolve and it DIPS: two layers at 50%
   composite to 0.5 + 0.5x0.5 = 75% coverage, so a quarter of the page
   background shows through the middle of every handoff. On a near-black page
   that reads as a flicker — the exact "feels like a bug" this was rewritten
   to remove. Keeping the departing layer opaque underneath means the
   composite is never less than fully covered at any instant.

   The departing layer still departs: it scales past the frame and its still
   softens (see the blur rule below). Motion carries the exit, not opacity. */
.rml-v3 .v3-deck.is-crossfading > .v3-state.v3-leave,
.rml-v3 .v3-deck.is-crossfading > .v3-state.v3-leave-active { z-index: 1; }
.rml-v3 .v3-deck.is-crossfading > .v3-state.v3-enter,
.rml-v3 .v3-deck.is-crossfading > .v3-state.v3-enter-active { z-index: 2; }

.rml-v3 .v3-state.v3-enter  { opacity: 0; transform: scale(1.022); }
.rml-v3 .v3-state.v3-leave  { opacity: 1; transform: scale(1); }

.rml-v3 .v3-state.v3-enter-active {
  opacity: 1; transform: scale(1);
  transition: opacity .68s var(--v3-ease), transform .9s var(--v3-ease);
}
/* No opacity here on purpose — see above. */
.rml-v3 .v3-state.v3-leave-active {
  transform: scale(1.045);
  transition: transform .9s var(--v3-ease);
}

/* Copy goes first, and further. The eye follows text, so releasing it early
   is what makes the media dissolve feel like a consequence rather than a
   simultaneous fade. */
.rml-v3 .v3-state.v3-leave-active .v3-copy,
.rml-v3 .v3-state.v3-leave-active .v3-beats,
.rml-v3 .v3-state.v3-leave-active .v3-tour,
.rml-v3 .v3-state.v3-leave-active .v3-reeltag {
  opacity: 0; transform: translateY(-12px);
  transition: opacity .2s ease-out, transform .34s var(--v3-ease);
}

/* The still frame softens as it goes — the one place a blur is affordable,
   because it is a background image and not a live map. Blurring the
   INCOMING side would force the browser to rasterise every Leaflet tile for
   the length of the transition, which is the stutter this is meant to
   avoid. */
.rml-v3 .v3-state.v3-leave-active .v3-reel-media { filter: blur(7px); transition: filter .6s var(--v3-ease); }

/* The map arrives last and settles. A short, late rise under the incoming
   chrome — the search bar and the dock — so the tracing surface reads as
   having come to meet the reader. */
.rml-v3 .v3-state.v3-enter-active .search-bar,
.rml-v3 .v3-state.v3-enter-active .draw-dock,
.rml-v3 .v3-state.v3-enter-active .v3-prog {
  animation: v3-settle .62s var(--v3-ease) .16s both;
}
@keyframes v3-settle {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
/* The dock and the search bar are already translated by landing-v2.css
   (the bar is centred with translateX(-50%)), so a keyframe that sets
   `transform: none` would yank them sideways. Give those two their own
   pair that only moves opacity. */
.rml-v3 .v3-state.v3-enter-active .search-bar,
.rml-v3 .v3-state.v3-enter-active .v3-prog {
  animation-name: v3-settle-fade;
}
@keyframes v3-settle-fade { from { opacity: 0; } to { opacity: 1; } }

/* Reduced motion: the handoff still happens, it just happens at once. No
   scale, no blur, no stagger — an instant swap is the honest translation of
   "do not animate", and the page never scrolled for this anyway. */
@media (prefers-reduced-motion: reduce) {
  .rml-v3 .v3-state.v3-enter-active,
  .rml-v3 .v3-state.v3-leave-active { transition: opacity .12s linear; transform: none; }
  .rml-v3 .v3-state.v3-leave-active .v3-reel-media { filter: none; transition: none; }
  .rml-v3 .v3-state.v3-enter-active .search-bar,
  .rml-v3 .v3-state.v3-enter-active .draw-dock,
  .rml-v3 .v3-state.v3-enter-active .v3-prog { animation: none; }
}

/* Phones: the beat strip would cover the bottom third of the land, which is
   where the copy already sits. The copy says what the reel contains. The
   tour card goes full width at the top, under the live tag, so the CTA and
   the copy keep the bottom — this is the first time the phone reel is
   explained at all. The #reel in the selector outranks the base placement
   above. */
@media (max-width: 720px) {
  .rml-v3 .v3-beats { display: none; }
  .rml-v3 #reel .v3-tour {
    left: 10px; right: 10px; top: 64px; max-width: none;
  }
  .rml-v3 .v3-tour-card { gap: 6px; padding: 12px 14px; }
  .rml-v3 .v3-tour-d { font-size: 16px; }
}

/* THE SCROLL CUE NEEDS THE BAR HEIGHT, AND IT IS NOT A DESCENDANT OF THE HOST.

   explore.css declares --tv-bar-h/--tv-bar on `.tv-shell, .tv-host` inside
   this same query, and its comment there records why both selectors are
   needed: everything the viewer BUILDS is a descendant of the host, while
   `.tv-scroll-cue` is a SIBLING of it. `.tv-shell` is markup only
   site/explore.html has — this page's wrapper is `.v3-stage` — so on /v3 the
   cue reads an undefined var, `bottom` is invalid at computed-value time and
   falls back to `auto`, and the pill abandons its anchor to land over the
   masthead on every phone. That is the same incident the explore.css comment
   describes, re-entering through the one element outside the host.

   The 44px is a second literal, which that comment rightly warns against. It
   is here rather than in explore.css because adding this selector there would
   move a token three pages share; if a third page ever needs these, promote
   them to one declaration instead of adding a fourth copy. */
@media (max-width: 720px) and (min-height: 561px) {
  .rml-v3 .v3-stage {
    --tv-bar-h: 44px;
    /* The phone offer bar's height, published by the viewer to the host AND
       its parent — this element — precisely so the cue can read it. Without
       it here the cue on /v3 sat on the Orbit button while the controls,
       inside the host, had moved up; measured at 375x812, cue [109,621]
       over Orbit [80,617]. tests/test_keyhole_controls_are_wired.py pins
       this derivation to explore.css's. */
    --tv-bar-extra: 0px;
    --tv-bar: calc(var(--tv-bar-h) + var(--tv-bar-extra, 0px));
    --tv-bar: calc(var(--tv-bar-h) + var(--tv-bar-extra, 0px) + env(safe-area-max-inset-bottom,
                                         env(safe-area-inset-bottom, 0px)));
  }
}

/* ══ THE SEQUENCE BORROWS THE EXPLORER, NOT ITS CHROME ═══════════════════
   `js/v3-approach.js` mounts the real RMLTerrainViewer here, and the viewer
   builds its own furniture: a layer panel, a 3D/Orbit pair, MapLibre's zoom
   controls. On /explore all of that is the product — it is how a buyer reads
   their parcel. On this stage it is wrong twice over.

   It is wrong for the reader: these controls invite a click, and the beat
   they belong to lasts ten seconds before the camera leaves. Offering
   somebody a panel and then flying away from it is worse than not offering
   it. The sequence toggles the layers; the reader traces a boundary.

   And it is wrong for the frame: the panel lands top-right, exactly where
   the hero copy needs the eye to be quiet.

   The corner badge (`.tv-mark`) STAYS. It is one chip in the bottom-left
   and it carries the measured resolution, which is the claim this stage is
   making. The TILED mark is the one that comes off — see below. */
.rml-v3 .v3-reel-media .tv-controls,
.rml-v3 .v3-reel-media .tv-head,
.rml-v3 .v3-reel-media .tv-scroll-cue,
.rml-v3 .v3-reel-media .maplibregl-ctrl-top-right,
.rml-v3 .v3-reel-media .maplibregl-ctrl-bottom-right > .maplibregl-ctrl-group {
  display: none !important;
}

/* THE TILED WATERMARK COMES OFF THIS STAGE, AND ONLY THIS ONE.

   `.tv-canvas::after` repeats "ReadMyLand.com" across the whole canvas at
   z-index 5. On /explore that is right and stays: the reader is looking at
   THEIR parcel, the mark sits on land, and explore.css's own note explains
   why a watermark absent from the view people actually open is not a
   watermark. On this stage it lands on the headline, the lede and the
   caption card rather than on ground — about thirty repetitions over a hero
   — and it is worst exactly where it protects least: through the flight the
   parcel is gone and the tile is the only thing drawn on the frame.

   What it guards is a picture of somebody's land. The reel is a demo of OUR
   own showcase farm, so a crop of it costs nothing, while the parcel the
   reader draws keeps the mark in full — the result stage below is untouched
   by this rule, as is every /explore view. The corner badge keeps the stage
   attributed — including on phones, where explore.css hides the badge's
   wordmark on the strength of this very tile; the phone block further down
   restores it here, and the two rules stand or fall together.

   Henk's call, 2026-08-28. This narrows #258's "every Explorer view" by one
   surface; it does not reopen the free/paid question the chip once implied.
   No !important: three classes outrank explore.css's bare `.tv-canvas::after`
   on specificity alone, and an !important here would win against explore.css
   modes this rule has no business beating. */
.rml-v3 .v3-reel-media .tv-canvas::after {
  content: none;
}

/* The map fills the stage and takes no input. `pointer-events: none` is the
   whole non-interactivity contract: a drag that spins the parcel would fight
   the camera the sequence is driving, and a reader who grabs it has no way to
   know the flight is still coming. The skip and the CTA sit above it in
   z-order and keep their events. */
.rml-v3 .v3-reel-media .tv-map,
.rml-v3 .v3-reel-media .maplibregl-map,
.rml-v3 .v3-reel-media canvas {
  position: absolute; inset: 0; width: 100% !important; height: 100% !important;
  pointer-events: none;
}

/* ...EXCEPT for the lane it is given when the controls are present. #266
   moves the badge BESIDE the 3D/Orbit row on a portrait phone, indenting it by
   `--tv-ctrl-w` and bounding it against MapLibre's zoom column. Those rules are
   keyed off `.tv-host`, so this stage gets them too — but the rule above hides
   the controls here, so the badge was indented 152px past nothing with empty
   map to its left. It takes the corner back, which is the same move
   `.tv-host.tv-sheet-open .tv-mark` already makes for the same reason: the
   lane exists only while the controls are in it.

   The wordmark comes BACK on this stage. explore.css hides it on phones with
   "the name is the tile's job here" as the stated reason — and the rule above
   just took the tile off this stage, so on a phone the reel would carry no
   ReadMyLand name anywhere on the frame, on the most screenshotted surface
   the product has. The three restore rules mirror explore.css's phone trio
   (:913-:916) exactly: the name back to inline, the separator back to the
   base " · " (explore.css:1588's own string), and ::first-letter back off
   capitalize — that last one exists because "elevation read at 50 cm" is a
   mid-sentence fragment after a separator, not a sentence opening. The name
   selector ties explore.css's on specificity, so this file loading second
   (landing-v3.html:20-21) is load-bearing. */
@media (max-width: 720px) and (min-height: 561px) {
  .rml-v3 .v3-reel-media .tv-mark {
    left: max(12px, env(safe-area-inset-left, 0px));
    right: auto;
    min-height: 0;
  }
  .rml-v3 .v3-reel-media .tv-mark .tv-mark-name { display: inline; }
  .rml-v3 .v3-reel-media .tv-mark .tv-mark-res::before { content: " \00b7  "; }
  .rml-v3 .v3-reel-media .tv-mark-res::first-letter { text-transform: none; }
}

/* HISTORY — DO NOT BRING THE TILE BACK "THINNED".

   Before the lift above removed the tile from this stage, an override here
   scaled it to 620x360, written to "thin it out" behind the headline. It did
   the opposite: `background-size` on a tiled SVG scales the GLYPHS too, so
   the mark came out two and a half times larger and dominated the frame. If
   the tile ever returns to the reel, production's own 250x145 at 15px and
   30% opacity is already the subtle version — the right override is none. */

/* ══ THE ASK ═════════════════════════════════════════════════════════════
   Sits over the tracing map, top-left, clear of the search bar (which is
   centred) and of the dock (which owns the bottom). It is the only copy in
   the draw state, so it carries the whole argument for tracing in three
   lines and then gets out of the way. */
.rml-v3 .v3-ask {
  /* z-index 501, not the 6 the other overlays on this page use.
     Everything else here floats over a still image or a MapLibre canvas,
     where single digits are plenty. This one floats over LEAFLET, whose panes
     sit at 400 and whose controls sit at 800 — so at 6 the card was rendered,
     reported visible, measured at the right size and position, and was
     completely hidden behind the map. It sits just above `.search-bar`
     (landing-v2.css:526, z-index 500), which is the same problem already
     solved once on this surface. */
  position: absolute; left: 24px; top: 92px; z-index: 501;
  max-width: 340px; padding: 18px 20px;
  background: rgba(10,12,8,.90);
  border: 1px solid var(--accent-soft);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  transition: opacity .45s var(--v3-ease), transform .45s var(--v3-ease);
}
.rml-v3 .v3-ask-h {
  font-family: var(--serif); font-size: 21px; line-height: 1.2;
  margin: 0 0 9px; color: var(--text);
}
.rml-v3 .v3-ask-p {
  font-family: var(--serif); font-size: 14.5px; line-height: 1.5;
  margin: 0; color: var(--text-dim);
}
/* Answered. Fades rather than vanishing, because it sits in the reader's
   peripheral vision while they aim at their first corner. */
.rml-v3 .v3-ask.is-answered {
  opacity: 0; transform: translateY(-8px); pointer-events: none;
}

/* On a phone the map is the whole screen and a 340px card over it covers the
   land being traced. It moves under the search bar and goes full width, where
   it reads as a lead-in rather than an overlay.

   UNDER, measured. `top: 78px` put it ON the bar: at 375x812 the search bar
   (landing-v2.css, `top: calc(var(--nav-h) + 12px)`, 52px tall) spans
   66-118px and the card spanned 78-220px at z-index 501 over the bar's 500,
   so the one field a phone reader needs first sat under a paragraph. The top
   is derived from the bar's own offset plus its height plus a 10px gap, so a
   taller nav (64px under 360px wide) moves both together. The copy shrinks to
   the heading and one line: on a phone the argument is the map. The short
   line is hidden by default HERE, before the query, so the query's own rule
   wins on a phone (same specificity; the later rule wins). */
.rml-v3 .v3-ask-short { display: none; }
@media (max-width: 720px) {
  .rml-v3 .v3-ask {
    left: 14px; right: 14px; top: calc(var(--nav-h) + 12px + 52px + 10px);
    max-width: none; padding: 12px 16px;
  }
  .rml-v3 .v3-ask-h { font-size: 18px; margin-bottom: 4px; }
  .rml-v3 .v3-ask-p { font-size: 13.5px; }
  .rml-v3 .v3-ask-long { display: none; }
  .rml-v3 .v3-ask-short { display: block; }
}


/* ══ THE LAYER PANEL WATCHES, IT DOES NOT WAIT FOR A CLICK ═══════════════
   It was hidden here at first, on the argument that a control which invites
   a click ten seconds before the camera flies away is worse than no control.
   Half right: the INVITATION is the problem, not the panel. The panel is the
   legend — it is what tells a reader that "wetness" is the blue one and that
   six layers exist rather than three — and hiding it left the beats
   unexplained.

   So it stays and stops being a control. `pointer-events: none` on the whole
   panel, and the checkboxes are disabled-looking rather than merely
   unclickable: a tick that cannot be untricked should not render as
   something you could. The sequence drives them; the reader reads them. */
.rml-v3 .v3-reel-media .tv-panel {
  pointer-events: none;
  opacity: .82;
}
/* And no chevron on the reel's bar. explore.css draws one on `.tv-host` to
   say the layer panel opens — true of the reader's own parcel below, false
   here, where the whole panel is `pointer-events: none` on purpose. An
   affordance on a control that cannot be operated is worse than none: it
   invites a tap that does nothing. This page mounts the viewer TWICE, which
   is what makes the partner rule necessary and is the half TODOS.md's entry
   on the chevron did not account for. */
.rml-v3 .v3-reel-media .tv-panel-head::after { content: none; }
.rml-v3 .v3-reel-media .tv-panel input,
.rml-v3 .v3-reel-media .tv-panel button,
.rml-v3 .v3-reel-media .tv-panel [role="button"] {
  pointer-events: none; cursor: default;
}
/* The per-layer "?" buttons open explanation cards nobody can dismiss while
   the camera is moving, and they are the one thing in the panel that reads
   as clickable at a glance. (This rule targeted `.tv-why`/`.tv-help` for a
   while — classes that do not exist; the real class is `.tv-row-info`,
   terrain-viewer.js buildPanel — so the buttons were never actually
   hidden.) */
.rml-v3 .v3-reel-media .tv-panel .tv-row-info { visibility: hidden; }

/* The legend row of whatever the tour has lit gets a hairline ring. Rides
   the `.tv-row-on` class the viewer's own syncBox toggles on every
   setLayer, so no script has to track it. */
.rml-v3 .v3-reel-media .tv-panel .tv-row.tv-row-on {
  outline: 1px solid var(--accent-soft); outline-offset: -1px;
}

/* ── The import control at `tracing` ───────────────────────────────────
   /v3 ONLY, and only for a mouse with nothing traced yet.

   THE DEFECT THIS FIXES. #importBoundary sits in data-slot="locate ready",
   which is right on landing-v2: that page's map starts below MIN_DRAW_ZOOM
   (16), so the reader rests at `locate`, sees both ways in, and only reaches
   `tracing` by choosing it. /v3 has no such moment. The reel hands the camera
   over at the flight's final zoom — measured at 18 — so autoArmDrawing() fires
   the instant the reel ends and `tracing` becomes the RESTING state
   (map-widget.js:1090). Measured on the live site after #278: the dock sat at
   tracing from handover onward and the import button was display:none for the
   entire session. The feature was reachable only on touch.

   WHY NOT THE OBVIOUS FIXES. Handing the camera over below zoom 16 would fix
   it and break the thing handOverCamera exists for — the two frames being "the
   same place at the same size", so the handover reads as a dissolve rather
   than a jump. Adding the token alone does not work either: landing-v2.css:761
   hides every .dock-slot[data-slot~="tracing"] on a mouse at data-corners="0",
   deliberately, to drop the three dead Undo/Close/Cancel controls. That rule is
   right and stays; this pair carves one exception out of it.

   TOUCH IS DELIBERATELY UNCHANGED. There, autoArmDrawing returns early, the
   dock rests at locate/ready, and the button already shows — so a rule that
   also fired on touch would only add ~50px of dock on the smallest screens,
   which the markup's own comment warns about. Hence the .rml-mouse scope.

   AND IT LEAVES ONCE THE READER COMMITS. At the first corner (data-corners
   stops being "0") the offer withdraws: they have chosen to trace, and a file
   would replace the work they just started.

   COMPOUND, NOT DESCENDANT. `.rml-v3` and `.landing-v2` are both on <body>
   (class="landing-v2 rml-v3"), so `.rml-v3 .landing-v2` asks for a descendant
   that does not exist and the rule never matches. Written as a descendant
   first, and both rules were silently dead — measured against the live DOM,
   which reported the exact inverse of the intent: hidden at 0 corners, shown
   at 1. A selector that cannot match is indistinguishable from one that loses.

   AND IT HID THE WRONG SLOT FOR TWO WEEKS (2026-09-09 to 2026-09-26). The
   first rule read `.dock-slot[data-slot~="tracing"]`, which matches EVERY
   slot that carries the tracing token — the import slot, and also the rail
   holding Undo, Close shape and Cancel (data-slot="tracing"). So on /v3 and
   /v4 those three were display:none at the very step they exist for, on
   every device, while the homepage kept them. It is scoped to the import
   slot now: `[data-slot~="locate"][data-slot~="tracing"]`, the one slot
   carrying both tokens. tests/test_dock_tracing_rail.py pins it. */
.rml-v3.landing-v2 .draw-dock[data-step="tracing"]
  .dock-slot[data-slot~="locate"][data-slot~="tracing"] {
  display: none;
}
.rml-mouse .rml-v3.landing-v2 .draw-dock[data-step="tracing"][data-corners="0"]
  .dock-slot[data-slot~="locate"][data-slot~="tracing"] {
  display: flex;
}
