/* ============================================================
   rawcloser.com — Tester page (Phase 7.A.18)
   Page-specific rules ONLY for /tester (tester/index.html).

   SECTION 1 REUSES THE /waitlist SECTION-1 LAYOUT SYSTEM WHOLESALE,
   which is itself the landing hero's, generalized in landing.css (one
   definition, three consumers). Taken from there, unmodified:
     · the fold-filling, vertically-centred section box
       (.hero / .wl-intro / .ts-intro) and its --space-9 top /
       --space-section-y bottom rhythm;
     · the asymmetric two-column grid itself (.hero-grid / .wl-grid /
       .ts-grid) — its --space-6 column gap, its centring, and its own
       page gutters in place of a .container;
     · the 60rem breakpoint itself, and the single-column mobile stack below
       it (form first, full width) — shared with the hero and the waitlist.
       What happens to the MEDIA column below that breakpoint is NOT shared
       (see the "differs" list below, and §4's Phase 7.A.32 rule): the hero
       and waitlist shots show, capped to 60% width; this page's motif is
       hidden outright;
     · the --text-6xl H1 step and the subhead treatment (size, secondary
       tone, prose measure, --space-5 gap);
     · the uppercase kicker (.privacy-kicker / .ts-kicker);
     · the on-LOAD `hero-rise` stagger — same keyframe, same 900ms, same
       easing, and the same reduced-motion kill switch.

   This file holds ONLY what genuinely DIFFERS from /waitlist section 1:
     · the MIRRORED, WIDER desktop layout (§5) — visual LEFT / copy RIGHT
       instead of copy left, a 41rem copy track instead of 28rem, and a
       --space-9 column gap instead of --space-6. The block cap is REPLACED by
       a pinned 117px side margin (clamped so it degrades on narrow
       desktops) — the one place this page stops tracking the hero's own
       side spacing. Scoped to >=60rem, so the hero and the waitlist
       (matched by the shared selectors only) are untouched;
     · the TWO-field request form (email + job) with a per-field helper
       line, in place of the waitlist's single-input form;
     · the "spots filling" MOTIF that occupies the media slot, in place of
       the waitlist's MacBook <picture> pair;
     · the media column's mobile visibility (§4, Phase 7.A.32): HIDDEN below
       60rem, unlike the hero/waitlist shots (shown, capped to 60% width) —
       the motif's height tracks the COPY's height (--ts-rows x --ts-pitch),
       not its own width, so a width cap cannot constrain it the way it
       constrains an image;
     · a DIFFERENT heading-parity device (§5). waitlist.css §5 reserves a
       square on its OWN media track; this page's track is too narrow for
       that to give the right height, so the media column instead takes
       the landing hero's row height directly, recomputed from the hero's
       numbers. Same goal — the H1 landing where the hero's does — reached
       from the other side;
     · this section's own on-load stagger DELAYS (its element set differs
       — there is a kicker here, and no fine-print/tester-link pair).

   SECTION 2 (§7, added 7.A.19) shares nothing with /waitlist section 2 —
   it DIVERGES from that page's text-only, no-cards beat by decision, so
   its two-column layout and type live here in full. What it does NOT
   restate is the CARD: the shell is components.css's shared .card,
   applied in the markup, and no shell declaration appears in this file.

   Consumes tokens.css only — never hardcode a color, spacing, radius,
   type, shadow, or motion value. The raw numbers below are structural
   motif/layout dims and motif loop durations, commented at each use site
   — the same latitude the landing hero takes for its 28rem copy cap and
   the landing feature motifs take for their loop timings.
   ============================================================ */

/* ---------- 1. Kicker → heading gap ---------- */
/* The kicker's type, tone and caps come from the shared rule in landing.css
   (.privacy-kicker/.ts-kicker). Only the gap down to the H1 is set here — the
   same --space-3 step the landing privacy section puts between its kicker and
   title, so the eyebrow+heading pair reads identically on both surfaces. */
.ts-title{
  margin-top:var(--space-3);
  text-wrap:balance;
}

/* ---------- 2. Request form — stacked, full-width ---------- */
/* Fields above a full-width button — the same stacked shape as the /waitlist
   form, and the same shared BEHAVIOUR module (assets/js/waitlist-form.js, bound
   via the data-waitlist-form hook), extended rather than forked to cover this
   page's second required field. What differs is that this form has TWO field
   GROUPS rather than one bare input, so the flex column's children are the
   groups, and the column gap is the group-to-group rhythm (--space-5, opened up
   from the waitlist's --space-3, which only ever separated one input from its
   button). margin-top clears the subhead on the same rhythm scale the hero and
   the waitlist use. No max-width: the grid's copy column (41rem here, §5) already
   caps it on desktop, exactly as the 28rem one caps the waitlist form. */
.ts-form{
  display:flex;
  flex-direction:column;
  gap:var(--space-5);
  margin-top:var(--space-7);
}

/* One field group = label (visible or visually-hidden) + control + optional
   helper line. Its own tight internal gap, so a group holds together against the
   wider gap between groups. */
.ts-field{
  display:flex;
  flex-direction:column;
  gap:var(--space-2);
}

/* Visible field label — used by the job field, whose placeholder is an EXAMPLE
   answer and cannot double as the label the way "you@email.com" can. Small,
   primary tone, medium weight: a clear question, not a heading. (The email
   field's label is .visually-hidden instead — see the markup.) */
.ts-label{
  font-size:var(--text-sm);
  font-weight:var(--weight-medium);
  color:var(--color-text);
}

/* Text inputs. No input primitive exists in the site yet, so they are styled from
   tokens here — deliberately the SAME token values as the waitlist input and the
   landing final-CTA input (raised surface, strong hairline border, pill radius,
   --space-2 block padding matching .btn height), so every form control on the
   site is the same control, only laid out differently. Full-width: the flex
   column stretches them. Keyboard focus uses the global :focus-visible ring
   (base.css) — no custom focus rule. */
.ts-input{
  padding-block:var(--space-2);
  padding-inline:var(--space-5);
  background:var(--color-surface-raised);
  border:1px solid var(--color-border-strong);
  border-radius:var(--radius-pill);
  color:var(--color-text);
  font-size:var(--text-base);
  transition:border-color var(--dur-fast) var(--ease-standard);
}

.ts-input::placeholder{
  color:var(--color-text-tertiary);
}

.ts-input:hover{
  border-color:var(--color-text-tertiary);
}

/* Per-field helper line, directly beneath the job input (wired to it with
   aria-describedby, so it is announced WITH the field instead of orphaned after
   it). Fine-print treatment — the same --text-sm / tertiary tone as the hero's
   requirements line and the waitlist's offer fine print — and inset by the
   input's own horizontal padding so it hangs off the placeholder's text edge
   rather than the pill's outer edge. --leading-normal keeps a wrapped helper from
   setting as tightly as the display type. */
.ts-help{
  padding-inline:var(--space-5);
  font-size:var(--text-sm);
  line-height:var(--leading-normal);
  color:var(--color-text-tertiary);
}

/* Full-width submit beneath the fields. .btn is inline-flex, so width:100% is
   what makes it span the stacked column. It ships `disabled` (see the markup
   seam); the shared .btn:disabled state (components.css) already reads it as
   inert, and the native attribute conveys that to assistive tech on its own. */
.ts-submit{
  width:100%;
}

/* ---------- 3. Offer line beneath the form ---------- */
/* The "accepted testers get early access free" line. Muted fine print — the same
   --text-sm / --color-text-tertiary treatment as the hero's requirement line and
   the waitlist's .wl-fine — so it reads as reassurance under the form without
   competing with the button above it. --space-6 off the form, wider than the
   form's own internal group gap, so it clearly sits OUTSIDE the form rather than
   reading as a fourth field. --leading-normal for the same reason as the helper:
   it wraps at this measure. */
.ts-fine{
  margin-top:var(--space-6);
  font-size:var(--text-sm);
  line-height:var(--leading-normal);
  color:var(--color-text-tertiary);
}

/* ---------- 4. "Spots filling" motif — the media column ---------- */
/* Replaces the waitlist's MacBook <picture> pair. A custom, ABSTRACT CSS/SVG
   motif in the same language as the landing page's motifs (how-it-works
   waveform/meters/note-lines, the features geometric set): monochrome, greyscale,
   token-driven, no silver/gold gradient, and NOT a literal UI element or a
   screenshot. A FIELD of nodes — the first four rows solid (spots taken), one
   ringed and pulsing (the next to fill), everything after it an open outline
   (spots available). It reads as selection out of a finite set: request a spot.
   Drawn at a 30px pitch with r=8 nodes, so the marks sit at just over half the
   pitch — plainly separate chips, not a texture — and the field fills its media
   track edge to edge, top to bottom.

   THE NODE IS A FIXED SIZE; THE COUNT IS WHAT FILLS THE BOX. Three earlier passes
   each got one part of this right and another wrong, so all three mechanisms are
   recorded to stop a fourth attempt repeating any of them:
     · the FIRST pass sized the field from its track's WIDTH (width:100%, height
       following from a column:row aspect-ratio). The dot size was then a function of
       the viewport — 30px pitch on a ~1440-1520px window, 46px at 1710px — and the
       height was too: the field matched the copy block at one width only, running
       ~90px past it at 1710px while falling ~80px short at 1512px;
     · the SECOND pass fixed the field's box in rem to cure the height, but computed
       the pitch from the 1710px case (2.875rem/46px). That held the height at every
       width, at the cost of scaling every dot UP by half on the ~1440-1520px windows
       this page is actually built on — a coarse field of big chips instead of the
       fine one the motif wants;
     · the THIRD pass made the node the constant and the count the variable, which is
       right, but wrote the count out by hand: 28 columns x 23 rows of literal
       <circle> elements, with the box capped at the drawn width. Both numbers were
       then wrong wherever the page was not the window they were measured on. The
       cap stopped the field widening past a ~1920px viewport, so on a wider monitor
       it sat at 28 columns with a growing empty band between it and the copy; and
       23 rows x 30px = 690px is the copy's height at ONE size only, so the bottom
       row finished short of the copy's last line everywhere else.
   THE FIX IS TO STOP STATING THE COUNT AT ALL. The node stays the constant — 1.875rem
   (30px) pitch, r=8 nodes at 16px across — and both counts are now derived from the
   space the field is given:
     · COLUMNS: pure CSS, no number anywhere. The nodes are an SVG <pattern> painted
       into rects sized 100% of the box (tester/index.html), so a row runs to the
       box's edges at the fixed pitch and a wider track simply means more columns.
       The box itself is the media track — no max-width — so it widens with the
       track at every viewport, and the surplus is a single PARTIAL column rather
       than a hard stop.
     · ROWS: --ts-rows, set by assets/js/tester-motif.js from the MEASURED height of
       the copy block beside it (floor(copyHeight / pitch), so the field never ends in
       a cut row). Scripted because CSS cannot read a sibling grid item's height and
       the grid ROW here is deliberately taller than the copy — it carries the landing
       hero's row height so the two H1s line up (§5) — so the box cannot just stretch
       to the row. The number below is a FALLBACK, not the design: with no JS the
       field renders 23 rows, exactly what this page shipped with, so the motif is
       complete before the script runs and merely fits itself better afterwards.
   The knobs, if the motif's proportions ever need work: the PITCH and the RADIUS,
   which are the only authored numbers left. Never a row or column count — there
   isn't one to edit any more.

   The focal (pulsing) node is deliberately placed 6 columns in from the RIGHT edge,
   not at a fixed offset from the left: the left is the clipped, faded side, so
   measuring from the right is what guarantees the one mark the motif is built around
   is always on screen and always at full opacity (it sits at 62-89% across the box
   across the whole desktop range, well clear of the fade's 40% ramp).

   Tonal scheme is lifted from the progress-line motif's own convention (§ the
   .progress block above in landing.css): the settled/done marks carry the
   RESTRAINED --color-text-secondary, and only the single CURRENT mark carries
   full --color-text. Open nodes drop to --color-border, the same hairline tone
   that motif's unfilled track uses. Every value is a token, so the whole motif
   flips correctly under [data-theme] in both light and dark with no per-theme
   rule and no hardcoded hex.

   Decorative: the SVG is aria-hidden in the markup and carries no title/desc, so
   it is absent from the accessibility tree and needs no text alternative. */
/* THE FIELD BOX — what is actually SEEN of the motif, and the thing the page
   margins are measured against. It is the media track itself: a block in the
   column, at the track's full width, so its LEFT edge sits exactly on the grid's
   left content edge — the same 117px margin + --container-pad-x gutter (165px at a
   1710px viewport) that the copy column's RIGHT edge sits on — and its RIGHT edge
   stops where the --space-9 column gap starts, well clear of the text. Equal
   gutters left and right; the field never reaches or crosses the viewport edge.
   (An earlier pass pulled it out past the page edge with a negative margin. That is
   reverted: the fade happens INSIDE the page margin, in the field's own first
   columns, not off-screen.)

   NO MAX-WIDTH. The previous pass capped the box at the drawn width of its 28
   hand-written columns, which is precisely what stopped the field widening on a
   monitor bigger than the one it was authored on. The pattern has no width to run
   out of, so the cap is gone and the box tracks the column at every size.

   HEIGHT IS THE ROW COUNT x THE PITCH, and that is a CONTRACT, not just an
   expression: tester-motif.js reads this element's height and divides by the
   --ts-rows it can see to recover the pitch in px, precisely so the pitch is stated
   HERE and nowhere else. Keep the two multiplied; do not hardcode a px height.

   OVERFLOW:HIDDEN is the clip. The tiling is anchored to the field's RIGHT edge
   (the mirror on .ts-dotfield below), so a track that is not an exact multiple of the
   pitch leaves its one partial column on the LEFT, where it is cut here at the page
   margin. Two reasons the surplus goes left: a cut column on the left is invisible
   (the fade is at ~0 alpha there), whereas cutting on the right would put half-dots
   at full opacity against the gap; and clipping on the element itself means nothing
   ever escapes into the section, so no column and no row can widen the page or
   produce a horizontal scrollbar. Below the 60rem breakpoint the whole media column
   is display:none (this file, Phase 7.A.32 rule below), so there is nothing to
   clip there either.

   THE HORIZONTAL FADE — unchanged from the previous pass, and deliberately so: a
   left-to-right alpha ramp on the box (0 alpha at the box's left edge, full by 40%
   across). It starts at 0 rather than an earlier 30% for one reason: the left column
   is CUT mid-node by the clip above, and only a zero-alpha start makes that cut
   invisible. Everything to the right of the ramp is at full weight — 40% of the box
   in, long before the copy column, at every width the box now takes. A mask is used
   rather than per-dot opacity because it ramps CONTINUOUSLY across the field rather
   than in per-column steps (and, before the pattern rewrite, because it was one
   declaration instead of 644). It sits on THIS element (the visible box) and not on
   .ts-media, so the ramp is measured across exactly the part of the field that is on
   screen.

   The rgba/`black` stops here are MASK ALPHA VALUES, not colours — a mask reads
   only the alpha channel, so they say how opaque, nothing else. It is not
   themeable and not a design-token category; the dots' actual colours are still
   the tokens set below and still flip with [data-theme]. The -webkit- prefix is
   required for Safari, which is the site's primary target. Where masks are
   unsupported the declaration is ignored and every dot simply renders at full
   opacity — a graceful, still-correct fallback.

   The rem pitch is a structural motif dim, the same latitude the rest of this file
   takes — not a design token. */
.ts-motif{
  --ts-pitch:1.875rem;   /* 30px node pitch — the ONE authored size in the motif  */
  --ts-rows:23;          /* no-JS fallback; tester-motif.js derives the real count */

  height:calc(var(--ts-rows) * var(--ts-pitch));
  overflow:hidden;

  -webkit-mask-image:linear-gradient(to right, rgba(0,0,0,0) 0%, black 40%);
          mask-image:linear-gradient(to right, rgba(0,0,0,0) 0%, black 40%);
}

/* Hidden on mobile, Phase 7.A.32. REVERSES 7.A.27's max-width:60% cap on
   .ts-motif, which capped the wrong axis: a max-width constrains WIDTH, and
   the problem here is HEIGHT. .ts-motif's height is --ts-rows x --ts-pitch
   (above), and --ts-rows is derived from .ts-copy's own rendered height
   (tester-motif.js) — not from the field's width, and not capped by anything
   this page sets. On a phone the copy stack (kicker, H1, subhead, two-field
   form, offer line) runs tall, so the field grows to match it: not a compact
   accent shrunk to 60% width like the hero/waitlist shots, but a field of
   dots as tall as the whole form, reading as a large, unexplained block under
   it. A width cap cannot fix that, because the height was never driven by
   width.
   Hidden on .ts-media, not .ts-motif, so nothing is left in the track for
   the grid to size around. Desktop (>=60rem) is untouched: its own
   `display:flex` (§5 below) lives in a min-width:60rem query, so the two
   declarations are mutually exclusive and never cascade against each other. */
@media (max-width: 59.999rem){
  .ts-media{
    display:none;
  }
}

/* The field itself: the box, exactly, at 1:1. The <svg> carries no viewBox, so its
   user units ARE CSS pixels — 30px pitch, r=8 (16px) nodes, exactly as authored,
   never stretched to fit a track. Filling the box in both axes is also what makes
   the pattern's node count follow the box: the rects inside are 100% of this.

   MIRRORED, and that is the whole right-anchoring mechanism. An SVG <pattern> tiles
   from the user-space origin — the LEFT edge — which would leave the one partial
   column hard-cut against the copy at full opacity. scaleX(-1) lays the identical
   grid out from the RIGHT edge instead, so the partial column falls on the left,
   inside the fade, and the focal node's 6-columns-from-the-right placement needs no
   width arithmetic. The nodes are circles centred in their tiles, so the flip is
   invisible in the marks; it only moves where the grid starts. transform-origin is
   the element's centre by default here, so the flip maps x -> boxWidth - x with no
   further offset. The mask on the parent is applied after this transform, so the
   fade still ramps from the box's visual left edge. */
.ts-dotfield{
  display:block;
  width:100%;
  height:100%;
  transform:scaleX(-1);
}

/* Taken spots — solid, restrained tone. */
.ts-node-taken{
  fill:var(--color-text-secondary);
}

/* Open spots — outlines, the faintest tone in the motif. 2 is a structural stroke
   width, and deliberately the SAME weight the landing feature motifs use, so the
   outline reads at the same optical thickness across the site's motif family;
   stroke widths are not a token-governed category. */
.ts-node-open{
  fill:none;
  stroke:var(--color-border);
  stroke-width:2;
}

/* The next spot to fill — the one focal mark, so it takes the full primary tone
   exactly as the progress line's single current node does. */
.ts-node-next{
  fill:none;
  stroke:var(--color-text);
  stroke-width:2;
}

/* The pulse itself: a dot inside the focal ring. THIS IS THE RESTING (STATIC)
   STATE — a partly-formed dot sitting in the ring, which is what shows with no
   JS, below the breakpoint, and under prefers-reduced-motion. The loop is added
   ONLY inside the no-preference block below, exactly the pattern the landing
   feature motifs use, so a reduced-motion visitor never gets an animation to
   suppress in the first place — the motif is simply static and fully formed.
   transform-box/origin so the scale in that loop happens about the dot's OWN
   centre rather than the SVG user-space origin. */
.ts-node-pulse{
  fill:var(--color-text);
  opacity:.55;
  transform:scale(.8);
  transform-box:fill-box;
  transform-origin:center;
}

/* The one loop. Gentle by construction: a slow 4200ms cycle that eases OUT to
   the filled state, holds there for roughly a third of the cycle, then eases
   back — no travel, no spin, nothing frantic. transform + opacity ONLY, so it
   never triggers layout or paint. 4200ms is a raw motif loop duration, the same
   latitude the landing feature motifs take (fm-gauge 5200ms, fm-ping 3400ms);
   there is no duration token anywhere near this length. */
@media (prefers-reduced-motion: no-preference){
  .ts-node-pulse{
    animation:ts-pulse 4200ms var(--ease-standard) infinite;
  }
}

/* 0% and 100% match, so the loop boundary is seamless and never flashes. */
@keyframes ts-pulse{
  0%,100%{ opacity:.35; transform:scale(.55); }
  40%,70%{ opacity:.9;  transform:scale(1); }
}

/* ---------- 5. Desktop column order, copy width, media box ---------- */
/* EVERYTHING in this block is scoped to the shared 60rem breakpoint, so it can
   never reach the hero or the waitlist (both of which are matched by the shared
   selectors only, not by .ts-*) and never applies below the breakpoint, where the
   media column is dropped and the copy is a single full-width stack anyway. */
@media (min-width: 60rem){

  /* HARDCODED 117px SIDE MARGIN — an explicit design decision for this section,
     and the ONE place this page stops tracking the landing hero automatically.
     The shared rule gives every first section `max-width:92.25rem` +
     `margin-inline:auto`, which happens to yield exactly 117px at a 1710px
     viewport and a different figure at every other width. Here 117px is pinned
     instead, so the cap has to go: a cap PLUS a fixed margin would left-align the
     block and let the right margin grow on wide screens — visibly off-centre.

     WHY IT IS CLAMPED AND NOT A BARE `117px`. A fixed inset costs 234px of width
     at EVERY viewport, which is affordable at 1710px and ruinous at 1000px, where
     it would leave the copy around 150px wide — an unusable form. So the margin
     ramps: 0 below a 78rem viewport, then growing with it, reaching the full 117px
     at ~1512px and holding there. The section sits at the requested 117px across
     the whole desktop range, and degrades instead of collapsing below it. The raw
     px and the 78rem knee are structural layout dims, the same category as the
     hero's 92.25rem cap — not design tokens.

     THE OTHER TWO CHANGES HERE:
       · the copy track is 41rem (656px) — opened from 38rem so the H1, subhead and
         form breathe a little more horizontally;
       · the column gap is --space-9 (96px), tightened from a composed
         --space-9 + --space-6 (128px). At 128px the two halves read as
         disconnected rather than as one section; 96px is the top of the spacing
         scale and pulls them together while still plainly separating the columns,
         and it is a single token again rather than a composed value.
     The two moves are deliberately paired and deliberately small: together they
     shift 48px from the media track to the copy (at a 1710px viewport, 644px/608px
     of media/copy becomes 628px/656px), so the section still reads as two weighted
     halves — now with the text marginally the wider of the two — rather than as a
     text column with a decoration beside it. The dots themselves are unaffected in
     size by either change: node size and pitch are fixed (§4), so a narrower media
     track shows FEWER columns of the same small dots rather than smaller ones.

     The media track keeps its 22rem FLOOR, with a changed job. It used to stop the
     dot field being crushed to a smear just above the breakpoint; the dots' size no
     longer depends on the track at all, so what the floor now governs is the FEWEST
     columns the field can be reduced to down there (22rem = 11 whole columns plus a
     partial one, which the fade eats). At the other end there is no ceiling any
     more: the field is the track, so a wider monitor is simply more columns — the
     28-column cap that used to strand it in the middle of a wide track is gone
     (§4). It also decides who
     yields first: the floor makes the COPY give way (it drops to ~435px at a 960px
     viewport and recovers to its full 41rem by ~1200px), which remains the right
     trade — the copy reflows gracefully, and the motif keeps its geometry. */
  .ts-grid{
    max-width:none;
    margin-inline:clamp(0px, calc((100vw - 78rem) / 2), 117px);
    grid-template-columns:minmax(22rem, 1fr) minmax(0, 41rem);
    gap:var(--space-9);
  }

  /* The order flip, done with explicit placement rather than by reordering the
     markup: the DOM keeps the form first (see the section comment — it is what a
     screen reader and the tab sequence follow, and what the mobile stack shows),
     while the desktop grid paints the visual on the left and the copy on the
     right. grid-row is pinned on both so the two can never be auto-placed onto
     separate rows. */
  .ts-copy{
    grid-column:2;
    grid-row:1;
  }

  /* HEADING-HEIGHT PARITY WITH THE LANDING HERO.
     What actually fixes an H1's height here is the GRID ROW's height, because
     the shared grid centres its items (align-items:center, landing.css): the copy
     block floats down by (rowHeight - copyHeight) / 2. On the landing hero the row
     is set by the MacBook, a 1:1 square as tall as the media track is wide — 864px
     at a 1440px viewport — so the hero copy sits 92px down inside it. Left to
     itself this column's row would be only as tall as the dot field (which is now
     the copy's own height, §4), the copy would start at the row top, and the H1
     would land ~90px HIGHER than the hero's.

     So the row is given the hero's row height directly. The two copy blocks are
     within ~13px of each other in height (the hero's H1 + subhead + button +
     two fine lines vs this page's kicker + H1 + subhead + two-field form + offer
     line), and equal rows with near-equal copy means near-equal H1 tops — the
     residual is (heroCopy - thisCopy) / 2, about 6px at desktop. That is the
     mechanism the landing hero uses, reproduced, rather than a magic offset.

     The value is the hero's media-track WIDTH, recomputed from the hero's own
     numbers rather than hardcoded, so it tracks at every viewport exactly as the
     hero's square does: the hero block is min(viewport, its 92.25rem cap), less
     its two --container-pad-x gutters, less its 28rem copy track, less its
     --space-6 gap. (Checks out against the hero at every width: 440px at a 1000px
     viewport, 864px at 1440px, and settling at the hero's 900px once the cap is
     reached.) The 92.25rem / 28rem here are the HERO's structural layout dims,
     quoted from landing.css — not this page's, and not design tokens.

     One honest limit: this is a min-height, and below roughly a 1250px viewport the
     hero's row (624px there and falling) drops under the copy's own height, so from
     there down the COPY (and with it the field, which is now the same height) sets
     the row and the H1 sits a little lower than the hero's. That band is the same
     one where the copy is already yielding to the media floor above; the parity
     holds across the widths this section is actually designed at.

     Note this is NOT the old `aspect-ratio:1/1` from waitlist.css §5. A square
     built on THIS page's track would be 628px — narrower track, wrong height. The
     height has to come from the hero's track, not from this one.

     WHY THE TWO COLUMNS STILL LINE UP TOP AND BOTTOM. The field is height-matched to
     the copy block (§4: rows = floor(copyHeight / pitch), so it is the copy's height
     rounded DOWN to a whole row — at most one pitch, 30px, short of it) and the two
     are centred in the same row, so the residual splits evenly: the top row of dots
     lands within ~15px of the kicker's top and the bottom row within ~15px of the
     last muted line's bottom, at every width. That is the whole point of measuring
     rather than fixing the row count — the field tracks the copy instead of matching
     it at one viewport. The row being taller than both is what holds the H1 at the
     hero's height; it does not pull the two columns apart.

     This is also why min-height (not height) and justify-content:center matter here:
     the field is free to grow with the copy, and the box centres whatever height it
     ends up with. */
  .ts-media{
    grid-column:1;
    grid-row:1;
    min-height:calc(
      min(100vw, 92.25rem)
      - 2 * var(--container-pad-x)
      - 28rem
      - var(--space-6)
    );
    display:flex;
    flex-direction:column;
    justify-content:center;
  }
}

/* ---------- 6. Section-1 on-load stagger — this page's own beats ---------- */
/* The `hero-rise` animation itself (keyframe + the raw 900ms + easing) is applied
   to these elements by the SHARED rule in landing.css, so the motion family is
   identical to the hero and the waitlist and is defined only once. What is
   page-specific is the ORDER of the beats — this section's element set is
   kicker → heading → supporting line → FORM → offer line → motif (the waitlist's
   is heading → line → form → fine print → tester link → visual). So only the
   per-element DELAYS live here, spaced by the same --stagger token: the kicker
   leads (no delay), the motif lands last.

   No reduced-motion guard is needed here — landing.css's shared guard already
   lists these elements and kills the animation outright under reduced motion, so
   a reduced-motion visitor sees section 1 instantly and completely. And because
   the whole thing is a pure-CSS load animation with animation-fill-mode:both
   (never a JS-applied hidden state), a visitor with no JS at all still sees it
   settle normally — visibility is never JS-gated. */
.ts-title{   animation-delay:var(--stagger); }
.ts-subhead{ animation-delay:calc(var(--stagger) * 2); }
.ts-form{    animation-delay:calc(var(--stagger) * 3); }
/* The 7.C privacy notice takes the FORM's beat rather than a beat of its own.
   Two reasons, and both matter: it belongs to the form it describes, so it
   should arrive with it and not a step behind; and giving it its own beat would
   push .ts-fine and .ts-media each one step later, retiming a settled sequence
   to seat a line of fine print. */
.ts-notice{  animation-delay:calc(var(--stagger) * 3); }
.ts-fine{    animation-delay:calc(var(--stagger) * 4); }
.ts-media{   animation-delay:calc(var(--stagger) * 5); }

/* ---------- 7. Section 2 — the fused testing + privacy beat (7.A.19) ---------- */
/* The page's SECOND AND FINAL section: what were planned as two separate beats
   (what testing involves; the privacy reassurance) FUSED into ONE two-column
   section — three stacked cards on the left, the privacy block on the right.

   IT DIVERGES FROM /waitlist SECTION 2 ON PURPOSE. That page's equivalent beat is
   text-only and centred, with NO cards (waitlist.css §6), and the two pages are
   otherwise built as siblings — so this is the one place the tester page
   deliberately does not mirror it. Nothing here is shared with, moved out of, or
   moved into waitlist.css: that file and the /waitlist page are untouched by this
   sub-phase. The divergence is a decision, not drift.

   THE CARDS ARE THE SITE'S EXISTING CARD COMPONENT, NOT A NEW ONE. The markup puts
   `class="card"` (components.css) on each <article> and this file adds NO shell
   rules — no background, border, radius, shadow, or padding is restated here. That
   component was authored in 7.A.1 and, until now, had no direct consumer: the
   landing feature/pricing-preview cards and the /pricing plan cards each restate
   its four declarations locally because they also need a flex column for their
   pinned footers. These cards need nothing beyond the shell, so they take the
   shell itself — the same raised surface, hairline border, --radius-lg and
   --shadow-md those clusters render, from the one definition rather than a fifth
   copy of it.

   Sits on the site DEFAULT background (--color-bg) — it sets no fill, so it is NOT
   the landing privacy section's recessed --color-surface band, even though the
   right column talks about privacy. Raised cards on the default page background is
   exactly the landing pricing-preview footing.

   Rides the shared .section rhythm and the shared .container gutter, so its
   vertical breathing and its left/right edges match every other section on the
   site. Every value below is a token except the one structural rem measure (the
   privacy block's 40rem cap) and the 1.3fr column ratio, both commented at their
   use site — the same latitude the rest of this file takes. */

/* Tighten the gap to section 1 — the site convention after a fold-filling first
   section (the landing hero -> problem, /waitlist section 1 -> 2). Section 1 is a
   min-height box whose padding-block-end is mostly absorbed by the fold, so the
   visible gap here is driven by THIS section's top padding; left at the shared
   .section default it runs the full fluid section height and reads as a void after
   the fold. This padding-top longhand overrides only the top half of .section's
   padding-block shorthand — the BOTTOM keeps the full --space-section-y, which is
   correct: this is the last section, and that is the gap down to the footer. */
.ts-testing{
  padding-top:var(--space-9);
}

/* ----- The two columns ----- */
/* Mobile-first single column: the three cards, THEN the privacy block, which is
   also the source order (no order flipping anywhere in this section — the DOM
   order and both painted orders agree). The two columns come in at the shared
   60rem breakpoint every multi-column block on this site uses.

   ALIGN-ITEMS:CENTER is what stops the right column reading as empty beside the
   stack: the privacy block is shorter than three cards, so it is centred against
   the stack's full height rather than sitting at the top with a long void beneath
   it.

   THE PRIVACY COLUMN IS THE WIDER OF THE TWO (1.3fr against the cards' 1fr). An
   earlier pass had this the other way round, on the argument that three stacked
   blocks of body copy need more room than a heading plus one line. That was wrong
   about which side is actually constrained: a card body is one short sentence and
   sets happily at any width down to the breakpoint, whereas the privacy block
   carries the beat's one piece of large display type (--text-5xl, up to 64px) and
   is the only thing here with a real measure to satisfy. So the surplus goes right,
   and it was opened again (1.15 -> 1.3) once the heading took the full section
   step: at 64px the heading needs roughly 730px to set on ONE line, which no
   column inside the .container can give it, so the width it does get is what
   decides how much of the first line it fills. At 1.3fr that is ~570px at a 1440px
   viewport against the cards' ~438px — the cards stay comfortable (their body is a
   single sentence) and the heading spans plainly. The ratio is a structural layout
   dim, not a token.

   THE COLUMN GAP IS --space-9 (96px), opened up from --space-8. An earlier note
   here argued 96px would read as two separate blocks rather than one beat, since
   this section sits inside the .container and its columns are narrower than section
   1's. In practice the opposite was true: the cards are a bordered, filled, shadowed
   cluster, and a panelled column needs MORE clearance from loose type beside it than
   two blocks of plain text would — at 64px the privacy heading crowded the card
   edges. It is set here in the media query, not on the base rule, so the gap it
   overrides stays --space-8 in the STACKED case, where it is a vertical gap between
   the cards and the privacy block and 96px would be a hole. */
.ts-testing-grid{
  display:grid;
  grid-template-columns:1fr;
  gap:var(--space-8);
}

@media (min-width: 60rem){
  .ts-testing-grid{
    grid-template-columns:1fr 1.3fr;
    gap:var(--space-9);
    align-items:center;
  }
}

/* ----- LEFT: three vertical cards ----- */
/* Equal width (one column, so they share it by construction) and equal spacing.
   A tight --space-4 stack — the same gap the landing privacy cards take — so the
   three read as ONE stacked unit rather than three loose panels; the section's
   real separation is the --space-9 column gap to the right, not the gaps inside
   the stack. The card SHELL itself comes from components.css .card in the markup;
   this rule only stacks them. */
.ts-cards{
  display:grid;
  gap:var(--space-4);
}

/* Card title — a bold lead, not a display heading: --text-md, with the semibold
   weight and tight leading inherited from base.css's shared heading rule. Matches
   the landing privacy cards' title step exactly, so this card cluster reads at the
   same optical weight as the others on the site. */
.ts-card-title{
  font-size:var(--text-md);
}

/* One sentence under it — base size, secondary tone, --space-2 off the title. Same
   pairing as every other title+line card on the site. No measure cap: the card is
   already the measure. */
.ts-card-body{
  font-size:var(--text-base);
  color:var(--color-text-secondary);
  margin-top:var(--space-2);
}

/* ----- RIGHT: the privacy block ----- */
/* Deliberately NOT a card. The three cards on the left are the section's panelled
   half; giving this one a fourth card would flatten the two columns into one
   four-card grid and lose the beat's shape. It is plain type on the section
   background, centred against the stack by the grid (align-items above).

   THE MEASURE CAP NO LONGER BINDS ON DESKTOP, deliberately. At 30rem (480px) it
   was landing within a few px of the column's own width, so it was silently acting
   as the block's real width and holding the text narrower than the column it sits
   in — which is half of why the heading wrapped tight (the other half was
   text-wrap:balance, dropped below). At 40rem (640px) it clears the column with
   room to spare across the whole desktop range — the column is ~445px at the 60rem
   breakpoint and tops out at ~570px, since .container caps the content at 1104px
   however wide the monitor gets — so the GRID sets the text width there and this
   cap does only the job it is meant to do: keeping the block readable in the
   STACKED case below the breakpoint, where it would otherwise run the container's
   full width. Structural rem cap, not a design token — the same category as the
   hero's 28rem copy cap. */
.ts-privacy{
  max-width:40rem;         /* structural rem cap, not a token */
}

/* The heading, at the site's FULL section-heading step (--text-5xl) — the same
   one the landing problem/privacy/pricing headings and /waitlist section 2 take.
   An earlier pass set it a step lower (--text-3xl, 40px flat) on the argument that
   a column heading beside a card stack should be quieter than a page-spanning
   section title; that made it too small to hold its half of the section against
   three cards, so it now takes the full step. It is the only large display type in
   the beat, and the cards are deliberately modest (--text-md titles), so nothing
   competes with it.

   Fluid, not flat, because that step is a clamp: ~50px at the 60rem breakpoint
   rising to 64px at 1440px and above — comfortably larger than the old 40px
   everywhere the two-column layout exists. Below the breakpoint it keeps shrinking
   (to 32px at a 375px viewport), which is the point of the token: a flat 40-64px
   heading in a full-width single column on a phone reads as broken, and every
   other heading on the site scales the same way.

   NO text-wrap:balance, and that is the deliberate exception to the site's usual
   heading treatment. `balance` optimises for EVEN line lengths, which on a
   four-word heading means it splits into two SHORT centred-looking lines well
   inside the measure — the heading then reads narrow no matter how much width the
   column is given, because the wrap algorithm is choosing not to use it. Dropping
   it restores normal greedy wrapping: the first line fills the measure and only
   the remainder drops, so the heading visibly spans its column. The line beneath
   keeps text-wrap:pretty, which only trims orphans and does not pull lines in. */
.ts-privacy-title{
  font-size:var(--text-5xl);
}

/* The reassurance line — one step above body size, secondary tone: the same lead
   treatment as the landing privacy body and the hero subhead, so it reads as a
   real statement rather than fine print. text-wrap:pretty trims orphans across a
   multi-line paragraph without forcing an even ragged edge (this line is longer
   than a balance target should be).

   GOVERNANCE, REWRITTEN 2026-08-29 (Phase 7.A.47): this paragraph duplicates the
   reasoning in tester/index.html's privacy-block comment and, canonically,
   index.html's privacy section comment (section 5) — read those for the full
   argument, not this copy. Short version: the objection-sharing opt-in this
   comment used to carve an exception around has been removed from the product
   entirely. The line above was already scoped to CALL AUDIO/CONTENT specifically
   and needs no rewording — it is now an absolute with no carve-out at all. Keep
   it scoped to calls regardless: the account email address still reaches
   api.rawcloser.com (sign-in code, plan confirmation), so a claim covering
   everything would still be false. */
.ts-privacy-line{
  font-size:var(--text-lg);
  color:var(--color-text-secondary);
  margin-top:var(--space-5);
  text-wrap:pretty;
}

/* The link line. Its COLOUR/weight/hover treatment is NOT here — it is the site's
   shared quiet arrow-link, grouped into landing.css's .final-cta-tester rule (one
   definition, two consumers, exactly like .privacy-kicker / .ts-kicker). Only the
   spacing and size are this page's, so the line + link group as one closing note
   under the heading. */
.ts-privacy-link{
  margin-top:var(--space-5);
  font-size:var(--text-base);
}
