/* ------------------------------------------------------------------------
   Astro funnel — eleven steps, mobile first.

   Centred composition: a logo mark and wordmark at the top, the promise as a
   heavy white serif, and answers as cards carrying a circular badge. The
   accent is a periwinkle iris, spent only on the badges, the progress bar and
   the primary button — everything else is navy and white.
   ------------------------------------------------------------------------ */

:root {
  --night:      #131A2B;
  --night-2:    #232C42;
  --night-3:    #2B3550;
  --night-edge: #0E1422;

  --star:       #FFFFFF;
  --star-dim:   #A4ACC3;
  --star-faint: #6E7893;

  --iris:       #5D6CE8;
  --iris-soft:  #808CEE;
  --iris-deep:  #4351D4;
  --iris-glow:  rgba(93, 108, 232, 0.22);

  --gilt:       #D6A64C;      /* reserved for locked / premium marks */
  --good:       #4CAF7D;

  --line:       rgba(255, 255, 255, 0.09);
  --line-soft:  rgba(255, 255, 255, 0.05);

  --f-display: Georgia, "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
  --f-body: "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, "Helvetica Neue", sans-serif;
  --f-mono: Consolas, "Cascadia Mono", ui-monospace, Menlo, monospace;

  /* Zodiac and planet characters. The display serif does not carry them, and
     the default fallback is a colour emoji font — which ignores any colour we
     set. These three cover Windows, Apple and Android with monochrome glyphs. */
  --f-glyph: "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2", "DejaVu Sans", serif;

  --shell: 31.25rem;     /* 500px */
  --pad: 1.5625rem;
  --radius: 1rem;
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; min-height: 100%; }

/* The page does not zoom -- the CSS third of it. See assets/nozoom.js for the
   whole argument and for what none of this can reach.

   pan-x pan-y, not manipulation: "manipulation" turns off the double-tap zoom
   and leaves the pinch, which is the wrong half. Naming the two pans keeps
   scrolling in both directions -- the funnel scrolls down, the energy cards
   scroll sideways -- and refuses everything else, which on a touch screen is
   the pinch and the double tap.

   text-size-adjust is a separate thing wearing the same coat: it is the phone
   browser silently inflating type inside a block it decides is too narrow, and
   it moves text without anybody having zoomed. */
html {
  touch-action: pan-x pan-y;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  background: var(--night);
  color: var(--star);
  font-family: var(--f-body);
  font-size: 1rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

.shell {
  position: relative;
  max-width: var(--shell);
  margin: 0 auto;
  min-height: 100dvh;
  /* The wheel's glow is inset -22% and is meant to spill past the artwork; on a
     narrow screen that spill reaches the page edge and buys a horizontal
     scrollbar. Clipped, not hidden — clip stops it without turning this into a
     scroll container, which would break the sticky footer below. */
  overflow-x: clip;
  display: flex;
  flex-direction: column;
  padding: 0 var(--pad);
  text-align: center;
}

/* ---- masthead: logo, wordmark, menu ------------------------------------- */

.masthead {
  display: grid;
  grid-template-columns: 2rem 1fr 2rem;
  align-items: center;
  padding: 1rem 0 0.5rem;
}
/* `display: grid` above beats the browser's own [hidden] { display: none },
   so renderChrome() setting .hidden did nothing and the badge, the wordmark
   and the menu sat on top of the ReadPalm row on every step. The same trap
   .topbar already guards against two rules down. Any element toggled with
   `hidden` that also has a display rule needs this line. */
.masthead[hidden] { display: none; }

.mark {
  grid-column: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  color: var(--star);
  text-decoration: none;
}

.mark .badge {
  width: 1.875rem;
  height: 1.875rem;
  border-radius: 50%;
  background: linear-gradient(150deg, var(--iris-soft), var(--iris-deep));
  display: grid;
  place-items: center;
  font-family: var(--f-display);
  font-size: 0.9375rem;
  color: #fff;
  box-shadow: 0 0 0 1px rgba(255,255,255,0.14);
  flex: none;
}

.mark .wordmark {
  font-weight: 700;
  font-size: 1.0625rem;
  letter-spacing: 0.005em;
}

.menu-btn {
  grid-column: 3;
  appearance: none;
  background: none;
  border: 0;
  padding: 0.375rem;
  cursor: pointer;
  display: grid;
  gap: 0.25rem;
  justify-self: end;
}
.menu-btn span {
  display: block;
  width: 1.125rem;
  height: 2px;
  border-radius: 1px;
  background: var(--star);
  transition: opacity 0.2s var(--ease);
}
.menu-btn:hover span { opacity: 0.7; }

.menu {
  position: absolute;
  top: 3.25rem;
  right: var(--pad);
  z-index: 20;
  background: var(--night-2);
  border: 1px solid var(--line);
  border-radius: 0.75rem;
  padding: 0.5rem;
  text-align: left;
  min-width: 11rem;
  box-shadow: 0 1rem 2.5rem rgba(0,0,0,0.45);
}
.menu[hidden] { display: none; }
.menu a {
  display: block;
  padding: 0.5rem 0.625rem;
  border-radius: 0.5rem;
  color: var(--star-dim);
  text-decoration: none;
  font-size: 0.875rem;
}
.menu a:hover { background: var(--night-3); color: var(--star); }

/* ---- progress ----------------------------------------------------------- */

/* The button and the track share a row of their own, so the circle centres on
   the line itself rather than on the line plus the caption underneath it. */
.topbar {
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
  padding: 0.75rem 0 0.75rem;
}
.topbar[hidden] { display: none; }

/* ONE ROW: back, the name, the count. Three columns with the outer two equal,
   so the name is centred on the screen and not on whatever is left after the
   count -- "3/20" and "19/20" are different widths and the name must not
   drift between steps. */
.topbar-row {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.75rem;
}

/* A plain arrow. It was a circled chevron in its own bordered disc, which is
   a control asking to be noticed; the row's job is the name, and the way back
   only has to be findable by a thumb that is looking for it. */
.back {
  appearance: none;
  background: none;
  border: 0;
  color: var(--star-dim);
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  margin-left: -0.25rem;
  cursor: pointer;
  flex: none;
  justify-self: start;
  display: grid;
  place-items: center;
  border-radius: 0.5rem;
  transition: color 0.18s var(--ease);
}
.back:hover { color: var(--star); }
.back[hidden] { visibility: hidden; display: grid; }   /* keeps the name centred */
.back svg { width: 1.625rem; height: 1.625rem; display: block; }

.topbar-brand {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 1.4375rem;
  letter-spacing: -0.005em;
  color: var(--star);
  text-align: center;
  line-height: 1;
}

/* Full width, and thick enough to read as a bar rather than a rule. */
.progress-track {
  width: 100%;
  height: 0.5rem;
  background: var(--line);
  border-radius: 999px;
  overflow: hidden;
}
.progress-bar {
  height: 100%;
  width: 0;
  background: var(--iris);
  border-radius: 999px;
  transition: width 0.45s var(--ease);
}

/* The real count, not a flattering one. A progress bar that lies is the
   cheapest conversion trick there is and the most expensive at renewal. */
.progress-label {
  font-family: var(--f-body);
  font-size: 1.125rem;
  font-weight: 500;
  color: var(--star-dim);
  font-variant-numeric: tabular-nums;
  text-align: right;
  justify-self: end;
  margin: 0;
  line-height: 1;
}

/* ---- stage -------------------------------------------------------------- */

.stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 1.5rem 0 1rem;
}

.step { animation: rise 0.4s var(--ease) both; }
@keyframes rise {
  from { opacity: 0; transform: translateY(0.75rem); }
  to   { opacity: 1; transform: none; }
}

h1.question {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 1.75rem;
  line-height: 1.22;
  letter-spacing: -0.005em;
  text-wrap: balance;
  color: var(--star);
  margin: 0.75rem auto 1rem;
  max-width: 22ch;
}

.lede {
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--star-dim);
  margin: 0 auto 2rem;
  max-width: 34ch;
}

/* The secondary instruction on the opening screen — smaller than the promise
   above it, because the promise is what the ad sold. */
.prompt {
  font-size: 1.0625rem;
  color: var(--star-dim);
  margin: 0 auto 1.375rem;
  max-width: 30ch;
}

.hint {
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--star-dim);
  margin: 0 auto 1.5rem;
  max-width: 34ch;
}

/* ---- options ------------------------------------------------------------ */

.options { display: grid; gap: 0.75rem; margin: 0 0 1rem; }
.options[data-cols="1"] { grid-template-columns: 1fr; }
.options[data-cols="2"] { grid-template-columns: 1fr 1fr; }
.options[data-cols="3"] { grid-template-columns: repeat(3, 1fr); }

.option {
  appearance: none;
  font: inherit;
  color: var(--star);
  background: var(--night-2);
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background 0.18s var(--ease), border-color 0.18s var(--ease), transform 0.12s var(--ease);
}
.option:hover { background: var(--night-3); }
.option:active { transform: scale(0.985); }
.option[aria-pressed="true"], .option.is-selected {
  border-color: var(--iris);
  background: var(--iris-glow);
}

/* Three across: badge stacked over the label, centred. */
.options[data-cols="3"] .option,
.options[data-cols="2"] .option {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.875rem;
  padding: 1.75rem 0.5rem 1.5rem;
  min-height: 8.25rem;
  text-align: center;
}

/* One per row: badge beside the label, reading left to right. */
.options[data-cols="1"] .option {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  padding: 0.875rem 1rem;
  text-align: left;
}

/* The box on a many-answers row. Empty until the row is chosen, then filled
   with the accent and a tick -- the same two states the row itself already
   has, said again in the one place a reader looks to count what they picked. */
.options[data-cols="1"] .option.has-box { gap: 0.75rem; }
.option-box {
  flex: none;
  display: grid;
  place-items: center;
  width: 1.25rem;
  height: 1.25rem;
  border: 1.5px solid var(--line-strong, var(--line));
  border-radius: 0.375rem;
  color: transparent;
  background: transparent;
  transition: background 0.15s var(--ease), border-color 0.15s var(--ease),
              color 0.15s var(--ease);
}
.option-box svg { width: 0.875rem; height: 0.875rem; display: block; }
.option.is-selected .option-box {
  background: var(--iris);
  border-color: var(--iris);
  color: #fff;
}

/* The circular badge is the accent's main job on this screen. */
.option .icon {
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  background: linear-gradient(150deg, var(--iris-soft), var(--iris-deep));
  display: grid;
  place-items: center;
  font-size: 1.5rem;
  line-height: 1;
  color: #fff;
  flex: none;
}
/* In the list layout the icons are emoji, which carry their own colour. Sitting
   them on the iris disc muddies both, so the disc is only for the monochrome
   glyphs used by the card layout. */
.options[data-cols="1"] .option .icon {
  width: 1.75rem;
  height: 1.75rem;
  background: none;
  font-size: 1.25rem;
}

/* A drawn icon replaces the emoji rather than sitting on top of one. No disc
   behind it: these are made for this palette and bring their own weight, and
   the iris gradient under a blue line drawing flattens both. */
.option .icon.has-art { background: none; }
/* A drawing earns more room than an emoji does in the list layout, and more
   than first guessed. An emoji at 28px is a glyph drawn to work at 28px; these
   are illustrations with a pen, four fingers and a cuff in them, and below
   about 44px those stop being separate things and become one smudge. Checked
   at 36, 48 and 72 before settling here. */
.options[data-cols="1"] .option .icon.has-art { width: 2.75rem; height: 2.75rem; }
.option .icon.has-art img,
.rc-mark.has-art img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.rc-mark.has-art { background: none; border-color: transparent; padding: 0.1rem; }

.option .label { font-size: 0.9375rem; font-weight: 600; }
.options[data-cols="3"] .option .label { font-size: 0.9375rem; }

.swatch {
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  flex: none;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.2);
}

/* ---- pickers ------------------------------------------------------------ */

.picker-row { display: flex; gap: 0.5rem; margin: 0 0 1.25rem; text-align: left; }
.picker-row .field { flex: 1; }

.field label {
  display: block;
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--star-faint);
  margin-bottom: 0.375rem;
}

input[type="text"] {
  width: 100%;
  font: inherit;
  font-size: 1rem;
  color: var(--star);
  background: var(--night-2);
  border: 1px solid var(--line);
  border-radius: 0.6875rem;
  padding: 0.8125rem 0.75rem;
  transition: border-color 0.18s var(--ease);
}
input[type="text"]:focus { outline: none; border-color: var(--iris); }

/* ---- zodiac strip ------------------------------------------------------- */

/* Previous, current and next sign. It moves as the wheels move, so the date
   picker always shows what the date actually means before it is confirmed. */
/* The signs slide rather than swap.
 *
   A wheel of dates has one honest answer to "what changed" — the sign moved
   along one — and a strip that redraws instantly tells you the answer without
   showing the move. So the three positions are slots rather than a grid, the
   items are laid over one another, and the only thing that changes on a new
   date is which slot each item is in.

   Two items sit off each edge at zero opacity. They are what the arriving sign
   fades up out of, and they are also where the leaving one goes to be reused —
   an item crossing from one edge to the other is invisible the whole way.
*/
.zodiac-strip {
  position: relative;
  height: 6.5rem;
  margin: 0.5rem 0 1.25rem;
  overflow: hidden;
}

/* The slot values are written out, not passed through custom properties.
 *
   `transform: translateX(var(--zx))` reads better and does not animate: an
   unregistered custom property has no type, so it changes discretely, and the
   transform that depends on it lands on the new value without travelling. The
   first version of this did exactly that — --zx updated, the computed transform
   stayed where it was, and the strip moved as abruptly as when it was being
   redrawn. Registering the properties with @property would also fix it; five
   literal rules need no browser support at all.
*/
/* A third of the strip each, so one step across is translateX(100%) and the
   three visible signs land on the same centres as the three wheels below —
   month, day, year. A fixed rem offset put them all near the middle and left
   the edges of a wide screen empty, which is the squeeze this replaced: the
   strip has to widen with the thing it labels, not sit at one size inside it. */
.zodiac-item {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 33.333%;
  margin-left: -16.6665%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  transform: translateX(0);
  opacity: 0;
  transition: transform 0.45s var(--ease), opacity 0.45s var(--ease);
  pointer-events: none;
}

/* The two neighbours sit a little higher than the sign in the middle. It reads
   as depth — the chosen one nearest, the others set back — and it is the one
   asymmetry that stops three evenly spaced glyphs looking like a toolbar. */
.zodiac-item[data-slot="-2"] { transform: translateX(-200%) translateY(-0.7rem); opacity: 0; }
.zodiac-item[data-slot="-1"] { transform: translateX(-100%) translateY(-0.7rem); opacity: 0.45; }
.zodiac-item[data-slot="0"]  { transform: translateX(0)     translateY(0);       opacity: 1; }
.zodiac-item[data-slot="1"]  { transform: translateX(100%)  translateY(-0.7rem); opacity: 0.45; }
.zodiac-item[data-slot="2"]  { transform: translateX(200%)  translateY(-0.7rem); opacity: 0; }

/* One glyph size, scaled. Transitioning font-size would relayout the strip on
   every frame; a transform is the same picture and costs the compositor
   nothing. */
.zodiac-item .zglyph {
  font-family: var(--f-glyph);
  font-variant-emoji: text;
  font-size: 3rem;
  line-height: 1;
  color: var(--star-faint);
  transform: scale(0.67);
  transition: transform 0.45s var(--ease), color 0.45s var(--ease),
              text-shadow 0.45s var(--ease);
}
.zodiac-item[data-slot="0"] .zglyph {
  transform: scale(1);
  color: var(--iris-soft);
  text-shadow: 0 0 1.75rem var(--iris-glow);
}

/* The name keeps its size through the move. Scaling it too made the two
   neighbours' names smaller than they were worth reading. */
.zodiac-item .zname {
  font-size: 0.8125rem;
  color: var(--star-faint);
  white-space: nowrap;
  transition: color 0.45s var(--ease);
}
.zodiac-item[data-slot="0"] .zname {
  font-weight: 600;
  color: var(--star);
}

/* ---- wheels ------------------------------------------------------------- */

.wheels {
  position: relative;
  display: flex;
  gap: 0.25rem;
  margin: 0 0 1.25rem;
  --item-h: 2.75rem;
  --rows: 5;
  height: calc(var(--item-h) * var(--rows));
}

/* The birth-time picker. Three rows instead of five: it opens on every
   column's first value, and with five rows the two above the highlight are
   empty padding, masked to nothing -- a blank band under the question that
   made the whole picker look dropped. See quiz.js time(). */
.wheels-short { --rows: 3; }

/* The selected row sits behind the columns and never intercepts a drag. */
.wheel-highlight {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: var(--item-h);
  transform: translateY(-50%);
  border-radius: 0.75rem;
  background: var(--night-2);
  border: 1px solid var(--line);
  pointer-events: none;
}

/* All columns share the width equally. Sizing month wide and day/year narrow
   spaces the boxes evenly but the centred labels unevenly, which is what the
   eye actually reads. */
.wheel {
  flex: 1 1 0;
  min-width: 0;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  /* Fade the rows that are not selected, top and bottom alike. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 26%, #000 74%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, #000 26%, #000 74%, transparent 100%);
}
.wheel::-webkit-scrollbar { display: none; }

/* Half a column of empty space at each end, so the first and last values can
   still reach the middle. */
.wheel-list {
  padding-block: calc((var(--item-h) * (var(--rows) - 1)) / 2);
  display: flex;
  flex-direction: column;
}

.wheel-item {
  height: var(--item-h);
  display: grid;
  place-items: center;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  font-size: 1.0625rem;
  color: var(--star-faint);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  user-select: none;
  transition: color 0.15s var(--ease), font-weight 0.15s var(--ease);
}
.wheel-item.is-active {
  color: var(--star);
  font-weight: 600;
}

.wheel-caption {
  display: flex;
  gap: 0.25rem;
  margin: -0.75rem 0 1rem;
}
.wheel-caption span {
  flex: 1;
  text-align: center;
  font-family: var(--f-mono);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--star-faint);
}

.ghost-btn {
  appearance: none;
  background: none;
  border: 1px dashed var(--line);
  color: var(--star-dim);
  font: inherit;
  font-size: 0.875rem;
  border-radius: 0.6875rem;
  padding: 0.75rem 1rem;
  width: 100%;
  cursor: pointer;
  transition: border-color 0.18s var(--ease), color 0.18s var(--ease);
}
.ghost-btn:hover { border-color: var(--iris); color: var(--iris-soft); }

/* place autocomplete */
.place-box { position: relative; text-align: left; margin: 0 0 1rem; }
.suggestions {
  position: absolute;
  left: 0; right: 0; top: calc(100% + 0.375rem);
  z-index: 15;
  background: var(--night-2);
  border: 1px solid var(--line);
  border-radius: 0.75rem;
  overflow: hidden;
  box-shadow: 0 1rem 2rem rgba(0,0,0,0.4);
}
.suggestions:empty { display: none; }
.suggestion {
  appearance: none;
  width: 100%;
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--line-soft);
  color: var(--star);
  font: inherit;
  text-align: left;
  padding: 0.6875rem 0.875rem;
  cursor: pointer;
}
.suggestion:last-child { border-bottom: 0; }
.suggestion:hover, .suggestion:focus-visible { background: var(--night-3); }
.sg-name { font-size: 0.9375rem; }
.sg-country { font-size: 0.75rem; color: var(--star-faint); }

.place-fallback {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--star-faint);
  margin: 0.875rem auto 0;
  max-width: 34ch;
}

.place-fallback[hidden] { display: none; }

/* bodies the engine placed */
.wheel-chart .moon { fill: var(--star-dim); }

/* Report mode plots every body inside the ring. The glyphs are the chart's
   content, so they get the readable weight and everything else stays quiet. */
.wheel-chart .body {
  font-family: var(--f-glyph);
  font-variant-emoji: text;
  font-size: 9px;
  fill: var(--star);
  text-anchor: middle;
  dominant-baseline: central;
}
.wheel-chart .body.lit { fill: var(--gilt); }
.wheel-chart .retro { font-size: 6px; fill: var(--star-faint); }
.wheel-chart .spoke { stroke: rgba(255, 255, 255, 0.35); stroke-width: 1; }
.wheel-chart .asc { stroke: var(--iris-soft); stroke-width: 2; }

/* A photo that could not be decoded at all: not the reading's verdict on the
   hand, just this file failing to open, so it sits with the control that would
   fix it rather than with the disclaimers. */
.palm-trouble {
  margin: 0.75rem auto 0;
  max-width: 22rem;
  font-size: 0.875rem;
  color: var(--gilt);
  text-align: center;
}

/* ---- chart step --------------------------------------------------------- */

/* A light card on a dark page: the one bright surface in the funnel, so the
   sentence it holds is the thing the eye lands on first. */
.bubble {
  position: relative;
  background: #FBFAF7;
  color: #161B28;
  border-radius: 1rem;
  padding: 1rem 1.25rem;
  margin: 0 auto 1.25rem;
  max-width: 24rem;
  font-size: 1.0625rem;
  line-height: 1.45;
  text-wrap: balance;
}
.bubble em { font-style: normal; color: var(--iris-deep); font-weight: 650; }
.bubble::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -0.5rem;
  width: 1.25rem;
  height: 0.6875rem;
  transform: translateX(-50%);
  background: #FBFAF7;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
}

.bubble-mark {
  width: 2.5rem;
  height: 2.5rem;
  margin: 0 auto 1.5rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-family: var(--f-glyph);
  font-variant-emoji: text;
  font-size: 1.25rem;
  color: #fff;
  background: linear-gradient(150deg, var(--iris-soft), var(--iris-deep));
  box-shadow: 0 0 0 3px rgba(93, 108, 232, 0.18);
}

/* The glow sits behind the wheel rather than inside the SVG, so it can bleed
   past the artwork without enlarging the drawing itself. */
.wheel-chart {
  position: relative;
  width: min(17rem, 78%);
  margin: 0 auto 1.75rem;
}
.wheel-chart::before {
  content: "";
  position: absolute;
  inset: -22%;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(93, 108, 232, 0.32) 0%,
    rgba(214, 166, 76, 0.12) 45%,
    transparent 70%);
  filter: blur(6px);
  pointer-events: none;
}
.wheel-chart svg {
  position: relative;
  display: block;
  width: 100%;
  height: auto;
}
.wheel-chart .rings circle,
.wheel-chart .ticks line {
  fill: none;
  stroke: rgba(255, 255, 255, 0.16);
  stroke-width: 1;
}
.wheel-chart .sector { fill: rgba(93, 108, 232, 0.22); }
.wheel-chart .pending {
  fill: none;
  stroke: rgba(255, 255, 255, 0.1);
  stroke-width: 1;
  stroke-dasharray: 3 5;
}
.wheel-chart .ray { stroke: var(--iris-soft); stroke-width: 1.5; opacity: 0.5; }
.wheel-chart .sun { fill: var(--gilt); }
.wheel-chart .glyphs text {
  font-family: var(--f-glyph);
  font-variant-emoji: text;
  font-size: 11px;
  fill: var(--star-dim);
  text-anchor: middle;
  dominant-baseline: central;
}
.wheel-chart .glyphs text.lit { fill: var(--gilt); font-size: 13px; }

.placements {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
  margin: 0 0 1rem;
}
.placement-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1875rem;
  padding: 0.75rem 0.25rem;
  border-radius: 0.75rem;
  background: var(--night-2);
  border: 1px solid var(--line-soft);
}
.placement-tile .pt-glyph {
  font-family: var(--f-glyph);
  font-variant-emoji: text;
  font-size: 1.25rem;
  line-height: 1;
  color: var(--iris-soft);
  margin-bottom: 0.125rem;
}
.placement-tile .pt-value { font-size: 0.9375rem; font-weight: 600; }
.placement-tile .pt-label {
  font-family: var(--f-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--star-faint);
}
.placement-tile.locked { border-style: dashed; }
.placement-tile.locked .pt-glyph { font-size: 1rem; }
.placement-tile.locked .pt-value { color: var(--gilt); font-weight: 500; }

.chart-note {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--star-faint);
  margin: 0 auto;
  max-width: 32ch;
}

/* ---- the chart arriving -------------------------------------------------- */

/* Step five is the first thing the visitor is shown that came out of their own
   answers rather than out of their own typing, so it is worth watching happen.
   The wheel is built the way one would be drawn: the rim, the twelve divisions,
   the twelve signs, then the three placements, each landing together with the
   card that names it.

   All of it is animation-delay. No timers, so there is nothing still running
   after Continue is pressed and nothing to tear down; and the reduced-motion
   rule near the end of this file switches off duration and delay together, so
   someone who asked not to be animated gets the finished screen at once without
   any of this needing to know.

   Geometry note for anything edited below: the wheel is drawn in a 0-200 user
   space centred on 100,100, with the outer ring at r=88 and the inner at r=68.
   The dash lengths are those circumferences rounded up — 553 and 428.
*/

/* ---- fitting the reveal on one phone screen ------------------------------
 *
 * Measured at 375x812 before this: heading 171-200, bubble 228-310, glyph
 * 328-348, wheel 382-646, tiles 674-765, note 781-817, sticky footer from 701.
 * 190px too tall, and the note -- the one line that says the chart is real,
 * "computed from your birth moment" -- was the part hidden under the button.
 *
 * Removing the heading gives back ~57px. The rest comes from the wheel and
 * the gaps, and ONLY here: .wheel-chart and .bubble are also used on the
 * report, where nothing needs to fit a viewport. After: bubble ~171, wheel
 * ~305-520, tiles to ~620, note to ~665, footer at 701. Measured again.
 */
.chart-reveal .bubble       { margin-bottom: 0.875rem; }
.chart-reveal .bubble-mark  { margin-bottom: 1rem; }
.chart-reveal .wheel-chart  { width: min(13.5rem, 62%); margin-bottom: 1rem; }
.chart-reveal .placements   { margin-bottom: 0.625rem; }

.chart-reveal .bubble      { animation: rise 0.62s var(--ease) 0.06s both; }
.chart-reveal .bubble-mark { animation: pop  0.62s var(--ease) 0.25s both; }
.chart-reveal .chart-note  { animation: fade-in 0.75s var(--ease) 2.19s both; }

/* The wheel turns into place rather than appearing square on. Small — twelve
   degrees — because this is a chart and a chart that spins is a toy. */
.chart-reveal .wheel-chart svg {
  animation: wheel-in 1.12s var(--ease) 0.38s both;
}
@keyframes wheel-in {
  from { opacity: 0; transform: rotate(-12deg) scale(0.9); }
  to   { opacity: 1; transform: rotate(0) scale(1); }
}

/* The glow swells rather than switching on. */
.chart-reveal .wheel-chart::before {
  animation: fade-in 1.25s var(--ease) 0.56s both;
}

/* A light passing over the wheel while it is being assembled, and gone once it
   is. Something that keeps sweeping after the chart is finished says the page
   is still working when it is not. */
.chart-reveal .wheel-chart::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    transparent 0deg, rgba(255, 255, 255, 0.16) 30deg, transparent 80deg);
  mask: radial-gradient(circle, transparent 62%, #000 63%, #000 99%, transparent 100%);
  -webkit-mask: radial-gradient(circle, transparent 62%, #000 63%, #000 99%, transparent 100%);
  pointer-events: none;
  animation: sweep-round 1.88s linear 0.44s 1 both, fade-out 0.5s var(--ease) 2.12s both;
}
@keyframes sweep-round { from { transform: rotate(0); } to { transform: rotate(360deg); } }
@keyframes fade-out { to { opacity: 0; } }

/* ---- the rim draws itself ------------------------------------------------ */

/* Rotated a quarter turn so the stroke starts at the top rather than at three
   o'clock, which is where a circle's path begins. */
.chart-reveal .wheel-chart .rings circle {
  transform-box: fill-box;
  transform-origin: center;
  transform: rotate(-90deg);
}
.chart-reveal .wheel-chart .rings circle:nth-child(1) {
  stroke-dasharray: 553;
  animation: draw-553 1.06s var(--ease) 0.44s both;
}
.chart-reveal .wheel-chart .rings circle:nth-child(2) {
  stroke-dasharray: 428;
  animation: draw-428 0.94s var(--ease) 0.69s both;
}
@keyframes draw-553 { from { stroke-dashoffset: 553; } to { stroke-dashoffset: 0; } }
@keyframes draw-428 { from { stroke-dashoffset: 428; } to { stroke-dashoffset: 0; } }

/* ---- the twelve divisions, one at a time --------------------------------- */

/* view-box rather than fill-box: these have to grow out of the middle of the
   wheel, not out of the middle of themselves. */
.chart-reveal .wheel-chart .ticks line,
.chart-reveal .wheel-chart .sector,
.chart-reveal .wheel-chart .glyphs text {
  transform-box: view-box;
  transform-origin: 100px 100px;
}

.chart-reveal .wheel-chart .ticks line { animation: spoke-in 0.5s var(--ease) both; }
@keyframes spoke-in {
  from { opacity: 0; transform: scale(0.55); }
  to   { opacity: 1; transform: scale(1); }
}
.chart-reveal .wheel-chart .ticks line:nth-child(1)  { animation-delay: 0.78s; }
.chart-reveal .wheel-chart .ticks line:nth-child(2)  { animation-delay: 0.81s; }
.chart-reveal .wheel-chart .ticks line:nth-child(3)  { animation-delay: 0.85s; }
.chart-reveal .wheel-chart .ticks line:nth-child(4)  { animation-delay: 0.89s; }
.chart-reveal .wheel-chart .ticks line:nth-child(5)  { animation-delay: 0.93s; }
.chart-reveal .wheel-chart .ticks line:nth-child(6)  { animation-delay: 0.96s; }
.chart-reveal .wheel-chart .ticks line:nth-child(7)  { animation-delay: 1s; }
.chart-reveal .wheel-chart .ticks line:nth-child(8)  { animation-delay: 1.04s; }
.chart-reveal .wheel-chart .ticks line:nth-child(9)  { animation-delay: 1.07s; }
.chart-reveal .wheel-chart .ticks line:nth-child(10) { animation-delay: 1.11s; }
.chart-reveal .wheel-chart .ticks line:nth-child(11) { animation-delay: 1.15s; }
.chart-reveal .wheel-chart .ticks line:nth-child(12) { animation-delay: 1.19s; }

/* The signs follow their own division round, a beat behind it. */
.chart-reveal .wheel-chart .glyphs text { animation: sign-in 0.56s var(--ease) both; }
@keyframes sign-in {
  from { opacity: 0; transform: scale(0.4); }
  60%  { transform: scale(1.12); }
  to   { opacity: 1; transform: scale(1); }
}
.chart-reveal .wheel-chart .glyphs text:nth-child(1)  { animation-delay: 0.98s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(2)  { animation-delay: 1.01s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(3)  { animation-delay: 1.05s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(4)  { animation-delay: 1.09s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(5)  { animation-delay: 1.12s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(6)  { animation-delay: 1.16s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(7)  { animation-delay: 1.2s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(8)  { animation-delay: 1.24s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(9)  { animation-delay: 1.27s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(10) { animation-delay: 1.31s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(11) { animation-delay: 1.35s; }
.chart-reveal .wheel-chart .glyphs text:nth-child(12) { animation-delay: 1.39s; }

/* The dotted inner circle is the part of the chart that is not known yet, so it
   arrives turning — once, slowly, and then it stops like everything else. */
.chart-reveal .wheel-chart .pending {
  transform-box: view-box;
  transform-origin: 100px 100px;
  animation: fade-in 0.75s var(--ease) 0.88s both,
             turn-once 7.5s linear 0.88s 1 both;
}
@keyframes turn-once { from { transform: rotate(0); } to { transform: rotate(120deg); } }

/* ---- what the chart actually says ---------------------------------------- */

/* The sun's sign, opening out of the middle. */
.chart-reveal .wheel-chart .sector { animation: wedge-in 0.62s var(--ease) 1.5s both; }
@keyframes wedge-in {
  from { opacity: 0; transform: scale(0.2); }
  to   { opacity: 1; transform: scale(1); }
}

/* The two lines draw from the centre outward. A dash longer than any line in
   the box covers both without measuring either. */
.chart-reveal .wheel-chart .ray,
.chart-reveal .wheel-chart .asc {
  stroke-dasharray: 200;
  animation: draw 0.69s var(--ease) both;
}
.chart-reveal .wheel-chart .ray { animation-delay: 1.62s; }
.chart-reveal .wheel-chart .asc { animation-delay: 1.88s; }
@keyframes draw {
  from { stroke-dashoffset: 200; }
  to   { stroke-dashoffset: 0; }
}

/* The two lights land on the rim with a little overshoot, the sun carrying a
   flare that settles. fill-box here: a dot grows out of itself. */
.chart-reveal .wheel-chart .sun,
.chart-reveal .wheel-chart .moon {
  transform-box: fill-box;
  transform-origin: center;
}
.chart-reveal .wheel-chart .sun  { animation: land 0.69s var(--ease) 1.69s both, flare 1.12s var(--ease) 1.94s both; }
.chart-reveal .wheel-chart .moon { animation: land 0.69s var(--ease) 1.88s both; }
@keyframes land {
  from { opacity: 0; transform: scale(0); }
  65%  { transform: scale(1.5); }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes flare {
  from { filter: drop-shadow(0 0 0 rgba(214, 166, 76, 0)); }
  35%  { filter: drop-shadow(0 0 6px rgba(214, 166, 76, 0.95)); }
  to   { filter: drop-shadow(0 0 2px rgba(214, 166, 76, 0.35)); }
}

/* Each card lands on the beat its own point appears on the wheel above. */
.chart-reveal .placement-tile { animation: rise 0.62s var(--ease) both; }
.chart-reveal .placement-tile:nth-child(1) { animation-delay: 1.69s; }
.chart-reveal .placement-tile:nth-child(2) { animation-delay: 1.88s; }
.chart-reveal .placement-tile:nth-child(3) { animation-delay: 2.03s; }

/* ---- photo capture ------------------------------------------------------ */

.palm-frame {
  position: relative;
  aspect-ratio: 3 / 4;
  /* 40vh put the frame at 325px on a 812px phone, which pushed the disclaimer,
     the privacy line and the proof line all below the fold with no cue that
     anything was down there. The frame is a target to aim a camera at, not the
     picture itself -- it loses nothing at 32vh, and the three lines under it
     gain the only thing that matters to them, which is being read. */
  max-height: 30vh;
  margin: 0 auto 1rem;
  border-radius: var(--radius);
  background: var(--night-2);
  display: grid;
  place-items: center;
  overflow: hidden;
}
.palm-frame img { width: 100%; height: 100%; object-fit: cover; }

/* ---- the viewfinder ------------------------------------------------------ */

/* Four corner brackets instead of the dashed box that was here. A dashed
   rectangle says "a field"; corners say "point a camera", which is the actual
   instruction and needs no words.

   Drawn as eight gradients on one element rather than as eight nodes. Each is a
   short bar with its own size and corner position, and background-size is what
   makes a gradient a bar instead of a fill — the same thing the star field
   needed, learned the same way. */
.palm-scan {
  position: absolute;
  inset: 0.625rem;
  pointer-events: none;
  --arm: 1.25rem;
  --thick: 2px;
  --edge: var(--iris-soft);
  background-image:
    linear-gradient(var(--edge), var(--edge)), linear-gradient(var(--edge), var(--edge)),
    linear-gradient(var(--edge), var(--edge)), linear-gradient(var(--edge), var(--edge)),
    linear-gradient(var(--edge), var(--edge)), linear-gradient(var(--edge), var(--edge)),
    linear-gradient(var(--edge), var(--edge)), linear-gradient(var(--edge), var(--edge));
  background-size:
    var(--arm) var(--thick), var(--thick) var(--arm),
    var(--arm) var(--thick), var(--thick) var(--arm),
    var(--arm) var(--thick), var(--thick) var(--arm),
    var(--arm) var(--thick), var(--thick) var(--arm);
  background-position:
    left top, left top,
    right top, right top,
    left bottom, left bottom,
    right bottom, right bottom;
  background-repeat: no-repeat;
  opacity: 0.55;
}

/* The sweep. Bright in the middle and gone at both ends, so it reads as light
   crossing the hand rather than as a bar sliding down a page.
 *
   Down, then back up. The first version travelled one way and jumped home
   invisibly to start again, which is cheaper but wrong: the return is the half
   that makes it a scanner rather than something falling. It pauses briefly at
   each end — a scan that turns round instantly reads as a bouncing ball. */
.palm-scan::after {
  content: "";
  position: absolute;
  left: -0.625rem;
  right: -0.625rem;
  height: 3px;
  border-radius: 3px;
  background: linear-gradient(90deg,
    transparent, rgba(128, 140, 238, 0.95) 22%, #EAEEFF 50%,
    rgba(128, 140, 238, 0.95) 78%, transparent);
  box-shadow: 0 0 1.25rem 0.25rem rgba(93, 108, 232, 0.75),
              0 0 0.375rem rgba(200, 210, 255, 0.6);
  animation: palm-sweep 5s ease-in-out infinite;
}

/* One cycle is the whole round trip, so the loop closes where it opened and
   there is no invisible jump home. */
@keyframes palm-sweep {
  0%       { top: 6%;  opacity: 0; }
  6%       { opacity: 1; }
  44%      { opacity: 1; }
  50%      { top: 94%; opacity: 0.85; }
  56%      { opacity: 1; }
  94%      { opacity: 1; }
  100%     { top: 6%;  opacity: 0; }
}

/* A photograph in the frame ends the sweep. Reading happens on the server,
   after payment; a line still travelling over a picture that has already been
   taken would be miming work that is not being done.

   The child selector is load-bearing. The guide drawing is an <img> too, nested
   inside .palm-guide, so a plain :has(img) matched every time and the sweep
   never ran at all. Only the photograph is a direct child of the frame. */
.palm-frame:has(> img) .palm-scan::after { animation: none; opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .palm-scan::after { display: none; }
}
/* ---- the scan ------------------------------------------------------------

   The screen after the photograph: the picture back at full size, its palm
   found, and the four major lines drawn onto it one at a time.

   The overlay is an SVG in the photograph's own pixel coordinates sitting over
   an uncropped picture, so nothing here positions anything — the box carries
   the photograph's aspect ratio and the two are the same rectangle. That is
   also why the capture frame's object-fit: cover is deliberately not reused:
   cover crops, and a crop means the coordinates in the file and the coordinates
   on the screen stop agreeing. */

.scan { display: flex; flex-direction: column; align-items: center; }

/* Sized off the photograph's own ratio, handed in as --ar, so the box is the
   picture and not a container the picture is letterboxed inside. Height is the
   real constraint on a phone — a portrait photograph given the full width would
   push the percentage and the button off the screen — so the width is capped by
   what 46vh of height allows at this ratio.

   46 and not 42: at 42 a portrait photograph came out 179px wide on a 320px
   screen, a bit over half the width, on the one screen whose whole subject is
   that photograph. The extra costs some scrolling on the shortest phones, which
   the sticky footer already makes harmless — the button does not move. */
.scan-shot {
  position: relative;
  width: min(100%, calc(46vh * var(--ar, 0.75)));
  aspect-ratio: var(--ar, 0.75);
  margin: 0 auto;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--night-2);
  animation: rise 0.5s var(--ease) both;
}
.scan-shot img { display: block; width: 100%; height: 100%; object-fit: contain; }

/* A vignette, not a veil. The lines have to read over whatever the room did to
   the exposure, and darkening the corners buys most of that contrast without
   dulling the palm itself — which is the part being looked at. */
.scan-shot::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(120% 90% at 50% 45%,
    transparent 42%, rgba(19, 26, 43, 0.42) 100%);
}

.scan-trace {
  position: absolute;
  inset: 0;
  color: var(--iris-soft);
}
.scan-trace svg { display: block; width: 100%; height: 100%; }

/* One shadow for the whole overlay rather than a stroke per line. Palms
   photograph anywhere from very pale to very dark, and a coloured line has no
   fixed relationship to any of them; a dark halo under all of them means every
   line reads on every skin tone without tinting the colours that name them. */
.pl-trace { filter: drop-shadow(0 0.5px 2px rgba(10, 14, 24, 0.75)); }

/* Four colours because the lines are four different things and the captions
   name them one at a time — a single accent would make the naming pointless.
   Lifted off the funnel's own palette rather than the saturated primaries these
   diagrams usually reach for: gilt is the token the product already uses, and
   the violet is the iris the whole product is built on. */
.pl-heart { stroke: #F0648C; }
.pl-head  { stroke: #E8B65A; }
.pl-life  { stroke: #3FC2AE; }
.pl-fate  { stroke: #9B8BF4; }

/* pathLength="1" in the markup is what makes one rule draw all four: every
   curve is a different real length, so a shared dash array would otherwise be
   a shared wrong length. */
.pl-line {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  opacity: 0;
}
.pl-line.is-drawn {
  opacity: 1;
  animation: pl-draw 0.72s var(--ease) forwards;
}
@keyframes pl-draw { to { stroke-dashoffset: 0; } }

.pl-mounts { opacity: 0; transition: opacity 0.6s var(--ease); }

/* The hand's box lives exactly as long as the "finding your hand" beat and
   then leaves completely. It used to linger faintly, and the palm used to get
   corner brackets of its own — but rectangles over a hand are a machine's crop
   marks, and the finished screen is meant to be a palmistry diagram, whose own
   marks say "palm" better than any box. The mounts arriving on the next beat
   are what now stands for "palm located". */
.pl-hand {
  stroke: var(--star);
  opacity: 0;
  transition: opacity 0.5s var(--ease);
}
.scan-trace[data-at="hand"] .pl-hand { opacity: 0.5; }

/* From the "hand" beat on: the dots are on the fingertips now, and fingertips
   are the "found your hand" claim itself, not a palm detail. */
.scan-trace[data-at="hand"] .pl-mounts,
.scan-trace[data-at="palm"] .pl-mounts,
.scan-trace[data-at="done"] .pl-mounts { opacity: 0.7; }

/* The sweep, borrowed from the capture frame and stopped at the end. It runs
   while the hand is being found and the palm located, which is the stretch
   where something is genuinely being worked out; once the lines are on it there
   is nothing left to sweep for and a line still travelling would be miming. */
.scan-sweep {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}
.scan-sweep::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 3px;
  border-radius: 3px;
  background: linear-gradient(90deg,
    transparent, rgba(128, 140, 238, 0.95) 22%, #EAEEFF 50%,
    rgba(128, 140, 238, 0.95) 78%, transparent);
  box-shadow: 0 0 1.25rem 0.25rem rgba(93, 108, 232, 0.75),
              0 0 0.375rem rgba(200, 210, 255, 0.6);
  animation: palm-sweep 2.6s ease-in-out infinite;
}
.scan-shot:has(.scan-trace[data-at="done"]) .scan-sweep::after {
  animation: none;
  opacity: 0;
}

.scan-pct {
  margin-top: 1.25rem;
  font-family: var(--f-display);
  font-size: 1.75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;   /* so the width does not jitter per digit */
  line-height: 1;
}

.scan-bar {
  width: min(17rem, 78%);
  height: 6px;
  margin-top: 0.75rem;
  border-radius: 6px;
  background: var(--line);
  overflow: hidden;
}
.scan-bar i {
  display: block;
  width: 6%;
  height: 100%;
  border-radius: 6px;
  background: linear-gradient(90deg, var(--iris-deep), var(--iris-soft));
  transition: width 0.46s var(--ease);
}

.scan-say {
  margin: 0.75rem 0 0;
  min-height: 2.8em;          /* the caption changes length; the page must not */
  max-width: 20rem;
  font-size: 0.9375rem;
  line-height: 1.4;
  color: var(--star-dim);
  text-align: center;
}

.scan-legend {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.375rem 0.75rem;
  margin: 0.25rem 0 0;
  padding: 0;
  list-style: none;
}
.scan-chip {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  font-size: 0.75rem;
  color: var(--star-faint);
  opacity: 0.45;
  transition: opacity 0.4s var(--ease), color 0.4s var(--ease);
}
.scan-chip.is-on { opacity: 1; color: var(--star); }
.chip-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
}
.chip-heart .chip-dot { background: #F0648C; }
.chip-head  .chip-dot { background: #E8B65A; }
.chip-life  .chip-dot { background: #3FC2AE; }
.chip-fate  .chip-dot { background: #9B8BF4; }

/* ---- the email step ------------------------------------------------------

   A locked card in the shape of the reading, and the address field under it.
   The whole card sits behind one sheet of glass; the only sharp thing is the
   sentence under it. This paragraph used to say the photograph was NOT
   softened, which stopped being true the moment the veil landed forty lines
   below it -- a comment that contradicts its own file is worse than none. */

/* The lines sit finished on a preview -- no draw-on animation, no beats. The
   drawing already happened, one screen ago; here it is a photograph of it.

   NOT scoped to the email card, and it must not be: report.js and palm2.js both
   hand the same `scan-trace is-settled` markup to their own figures, so these
   three lines are shared by every finished drawing in the product. They were
   deleted once, by a rewrite of the email card that took the rules underneath
   it with them, and the visible symptom was every stroke on every settled hand
   sitting at opacity 0 -- the funnel, the report and the palm page at once. */
.is-settled .pl-line { opacity: 1; stroke-dasharray: none; stroke-dashoffset: 0; animation: none; }
.is-settled .pl-mounts { opacity: 0.7; }
.is-settled .pl-hand { opacity: 0; }


/* Built to the same rules as `.report-preview` further down this file, because
   it is the same object one screen earlier: same ground, same hairline, same
   radius, same mono eyebrow. What it does not take is that card's bottom fade,
   which promises content below the cut -- there is none below this one. */
.email-card {
  max-width: 21rem;
  margin: 0 auto 1rem;
  /* The side padding is named once and spent twice: it insets the rows, and
     the photograph cancels exactly it to reach the card's edges. Change one
     and change the other. */
  padding: 0.75rem 0.75rem 0.4375rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--night-2);
  overflow: hidden;
}

/* FULL BLEED. The photograph is as wide as the card and touches both sides.
 *
   It used to be capped at 25vh, which on a 375px screen drew it 203px wide
   inside a 312px card and left a band of empty card either side of it. The cap
   is gone and the negative margins cancel the card's own padding, so the frame
   is the card's full width and its top corners are the card's top corners.

   WHAT THIS COSTS, because it is not free. The window on the photograph is
   square -- a portrait phone photograph cropped to the hand -- so width and
   height rise together: the photograph goes from 203px to 336px and the card
   from 372px to about 505px. On a 375x812 phone that puts the address field
   below the fold, reachable by a short scroll with the Continue bar still
   pinned. That is a deliberate trade, made at the owner's direction, and it is
   written down here so it is not rediscovered as a bug. */
.email-peek {
  position: relative;
  width: auto;
  margin: -0.75rem -0.75rem 0.5rem;
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
  overflow: hidden;
  pointer-events: none;
  user-select: none;
  animation: none;
}

/* The frame inside the frame: sized larger than its parent and shifted, so the
   window lands on the hand. `fill` and not `cover` -- the placement is already
   done here and letting the image choose its own fit would undo it. */
.email-peek-in { position: absolute; inset: 0; }
.email-peek-in img,
.email-peek-in .scan-trace { position: absolute; inset: 0; width: 100%; height: 100%; }
.email-peek-in img { object-fit: fill; }

/* The hand box and the fingertip dots were the SCANNING screen's proof that a
   hand had been found. On a card meant to look like the finished thing they
   read as a machine's crop marks. */
.email-peek .pl-hand,
.email-peek .pl-mounts { display: none; }

/* Weight in screen pixels, not in the photograph's coordinate space, so the
   stroke is the same whatever camera took it. */
.email-peek .pl-line { vector-effect: non-scaling-stroke; stroke-width: 2.5px; }

.email-peek-say {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  margin: 0;
  padding: 1.5rem 0.75rem 0.5rem;
  text-align: left;
  font-family: var(--f-display);
  font-size: 0.9375rem;
  color: var(--star);
  text-shadow: 0 2px 14px rgba(11, 16, 32, 0.9);
  background: linear-gradient(to top, rgba(11, 16, 32, 0.75), transparent);
}

.email-lines { display: grid; gap: 0.05rem; }

/* Four columns, same order as the report: dot, name, what the line reads, the
   meter. The name column is fixed so the four phrases start on one edge --
   a ragged left there turns four rows into four separate things. */
.email-line {
  display: grid;
  grid-template-columns: 0.4rem 2.9rem minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.5rem;
  min-height: 1.4375rem;
}
.email-line-dot {
  width: 0.4rem; height: 0.4rem;
  border-radius: 50%;
  background: var(--row);
}
/* One declaration for both the rows and the paragraphs under them, so a line
   can never be one colour in the meter and another in the prose. */
.email-line.is-heart, .email-fog-p.is-heart { --row: #F0648C; }
.email-line.is-head,  .email-fog-p.is-head  { --row: #E8B65A; }
.email-line.is-life,  .email-fog-p.is-life  { --row: #3FC2AE; }
.email-line.is-fate,  .email-fog-p.is-fate  { --row: #9B8BF4; }

/* text-align is set here because the funnel centres everything by default,
   and four phrases centred in their own column stop reading as a column. */
.email-line-name, .email-line-say { text-align: left; }
.email-line-name { font-size: 0.75rem; color: var(--star-dim); }
.email-line-say {
  font-size: 0.75rem;
  color: var(--star);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Stops, not a fill. A bar filled to some fraction states a magnitude and
   there is no magnitude here; four unlit stops state only that four slots
   exist. The report lights exactly one per row out of the vocabulary that
   row's word came from, so this is that same picture before anything has
   been decided.

   NOT ONE OF THEM IS LIT, and none may be. A lit stop beside "Heart -- how you
   attach" is read as a result about the person looking at it, and there is no
   result: the four curves are canonical Beziers warped onto the measured palm
   (palm-find.js:86-95), identical for every hand alive, and the crease reader
   is not written. Lighting one at random would be the `.proof` line's mistake
   moved onto the customer's own hand -- see quiz.js on why that line is flagged
   and on the 2022 EU Omnibus directive.

   ALL FOUR EQUAL, and tinted rather than grey. They were rgba(255,255,255,.13),
   which read as a widget that had failed to load rather than as a reading that
   has not happened. The line's own colour at low opacity ties each track to its
   stroke on the photograph above and makes the row look drawn on purpose. Equal
   is the load-bearing part: four identical stops state that four slots exist
   and nothing about which one it will be. Any falloff across them -- a gradient
   left to right, a shimmer that sweeps -- would put weight on one end, and
   weight on one end is a claim.

   No shimmer for the same reason `.rp-skel-bar` has none (offer.js): a pulsing
   placeholder says work is in progress, and nothing is being computed here --
   the reading starts when the address is committed, a screen later. */
/* THE GAP HAS TO BE WIDER THAN THE BLUR, and this is the whole reason the
   numbers below are what they are.
 *
   At a 2.4px gap under a 3.5px blur the four stops fused into one continuous
   coloured bar -- checked at 2x, not predicted. That bar is a filled meter,
   which is the one thing this card is not allowed to show: it states a
   magnitude for a line nobody has read. The rule against lighting a stop is
   worth nothing if the blur draws the same picture by accident.

   So the gap is 0.4375rem (7px), wider than the 2.5px the veil now uses, and
   the stops read as four separated marks through the glass. The opacity comes
   back down from 0.6 too: 0.6 was raised to survive a blur that has since been
   reduced, and on separated stops it made each one read as lit. */
.email-line-track { display: flex; gap: 0.4375rem; }
.email-tick {
  width: 0.5rem;
  height: 0.25rem;
  border-radius: 0.125rem;
  background: var(--row);
  opacity: 0.42;
}

/* THE GLASS.
 *
   On the wrapper, never on .email-card: a filter on the card blurs its own 1px
   border and its rounded corners into a smear, and the card stops reading as a
   card. Every other blur in this file sits one level in from its frame for the
   same reason -- see .rp-wheel and .teaser .fog.

   2.5px AND NOT 3.5. offer.js sets the two tiers and says which is which:
   3.5 "turns a paragraph into texture, which is the point there; here it would
   turn a chart into a smudge, and the reader is meant to recognise what they
   are looking at." This card is mostly a drawing of a hand, so it takes the
   drawing tier, the same 2.5 .rp-wheel takes.

   The paragraphs inside drop their own blur. Stacking blur on blur made one
   strip of the card muddier than the rest, and the point of a veil is that it
   is one sheet of glass over the whole thing. */
.email-veil {
  filter: blur(2.5px);
  opacity: 0.9;
  pointer-events: none;
  user-select: none;
}
.email-veil .email-fog-p { filter: none; }

/* EDGE BLEED, and why the photograph is scaled rather than left alone.
 *
   A blur samples what is outside the element as transparent, so a filtered box
   feathers INWARD at its own edge. The card clips at that same edge, so nothing
   can be sampled from beyond it. On the top and the two sides -- where the
   photograph runs full bleed to the card's border -- a lit palm against
   #232C42 produced a pale halo along the frame. handWindow() returns w: 1
   (palm.js), so there is never any horizontal overhang to sample from; this is
   not an occasional case.

   Four per cent of scale gives the blur material on all four sides. It moves
   the crop by two per cent of the frame, which is below what anyone can see on
   a photograph of their own hand, and it costs nothing because .email-peek
   already clips. */
.email-veil .email-peek-in { transform: scale(1.04); }

/* One filter, not two nested ones. .pl-trace carries a drop-shadow of its own
   (see the rule further up this file); inside the veil that is a second
   offscreen pass over the same region for a shadow nobody can see through
   2.5px of blur, and iOS is known to mis-rasterize the inner one. */
.email-veil .pl-trace { filter: none; }

/* Thicker than the blur, or it is not a line any more. The strokes are 2.5px
   and the veil's sigma is 2.5px, which takes a curve down to about a third of
   its peak alpha -- the four coloured lines ARE the picture, so they get the
   weight back. */
.email-veil .pl-line { stroke-width: 4px; }

/* THE LOCKED HALF.
 *
   Capped and faded rather than shown whole: four paragraphs run to about 250px
   and the card is already the tallest thing on this screen. The cap is what
   makes the fade honest -- there IS more under it, which is the condition
   app.css states for the offer card's own fade further down this file.

   The blur sits on the paragraphs and the fade on their container, never both
   on one element: a filter on the parent would blur its own pseudo-element and
   turn the gradient into a smear. Same reason .rp-wheel keeps its blur one
   level in. 3.5px is the funnel's prose blur, taken from .teaser .fog so the
   two locked blocks a screen apart are out of focus by the same amount. */
/* THE FADE IS A MASK NOW, NOT A PAINTED GRADIENT.
 *
   It was an ::after painting var(--night-2) over the text. That works only
   while nothing above it is filtered -- and the veil filters this element's
   ancestor, so the gradient was being blurred along with everything else and
   came out as a smear that no longer matched the card behind it. The rule is
   stated four lines up and the veil broke it from the outside.

   A mask is not painted, so a parent filter cannot smudge it: it removes the
   pixels instead of covering them, the card's own background shows through
   whatever colour that is, and it survives being blurred because there is
   nothing there to blur. */
.email-fog {
  max-height: 2.25rem;
  overflow: hidden;
  margin-top: 0.5rem;
  text-align: left;
  user-select: none;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 45%, transparent 96%);
  mask-image: linear-gradient(to bottom, #000 45%, transparent 96%);
}
.email-fog-p {
  margin: 0 0 0.5rem;
  font-size: 0.8125rem;
  line-height: 1.6;
  color: var(--star-dim);
  filter: blur(3.5px);
  opacity: 0.75;
}
.email-fog-p b { font-weight: 650; color: var(--row); }

.email-card-wait {
  margin: 0.375rem 0 0;
  text-align: center;
  font-size: 0.6875rem;
  line-height: 1.45;
  color: var(--star-faint);
}

/* THE SHORT-PHONE RULE, and the reason this card can be the tall one.
 *
   On a 360x667 screen the chrome, the sticky footer and the heading leave
   about 266px between the question and the point where the address field would
   disappear under the Continue bar. A field nobody can see is a field nobody
   fills, so at that height everything on the card gives a little: the
   photograph most, since it is the only part that can be smaller and still be
   the same picture, and the rows and the margins the rest. */
/* THE SHORT-PHONE RULE, and the one place the photograph is not full bleed.
 *
   A square photograph at the full width of the card is about 320px tall, and a
   360x667 screen has roughly 409px between the question and the point where the
   address field disappears under the Continue bar. Full bleed there does not
   put the field below the fold, it puts it a screen and a half below the fold.
   So under 700px of viewport the photograph steps back inside the card's
   padding and takes its height from the viewport again -- the same card, drawn
   smaller, rather than a different one. */
@media (max-height: 700px) {
  .email-card { margin-bottom: 0.625rem; padding: 0.5rem 0.625rem 0.4375rem; }
  .email-peek {
    width: min(100%, calc(15vh * var(--ar, 0.75)));
    /* The caption sits INSIDE the photograph, so the photograph may not shrink
       below the width of the caption: at a smaller cap this reached 100px and
       "Your left palm" hung off both edges of it. */
    min-width: 7.5rem;
    margin: 0 auto 0.375rem;
    border-radius: 0.625rem;
  }
  .email-peek-say { padding: 1.125rem 0.4375rem 0.3125rem; font-size: 0.75rem; }
  .email-line { min-height: 1.375rem; }
  /* THE LOCKED PROSE GOES, AND THE PHOTOGRAPH STAYS.
   *
     Something had to, and this is the right one. Squeezing the block instead
     took the photograph down to 120x69 -- a coloured sliver that no longer
     reads as a hand -- to keep two blurred lines nobody can read anyway. The
     prose is there for weight; the photograph is the subject, and it is the
     one thing on this card that is the reader's own.

     (This block also used to make the fog BIGGER here than in the base rule --
     3.25rem against 2.25 -- because the base came down later and this was not
     re-checked. A max-height inside a max-height media query has to be read
     against its base every time the base moves.) */
  .email-fog { display: none; }
  .email-card-wait { margin-top: 0.375rem; }
}

input.email-field {
  width: 100%;
  font: inherit;
  font-size: 1rem;
  color: var(--star);
  background: var(--night-2);
  border: 1px solid var(--line);
  border-radius: 0.6875rem;
  padding: 0.9375rem 1rem;
  text-align: center;
  transition: border-color 0.18s var(--ease);
  animation: rise 0.5s var(--ease) 0.1s both;
}
input.email-field:focus { outline: none; border-color: var(--iris); }
input.email-field::placeholder { color: var(--star-faint); }

.email-note {
  margin-top: 0.75rem;
  animation: rise 0.5s var(--ease) 0.2s both;
}

/* ---- the preparation overture --------------------------------------------

   Three movements before the offer: the visitor's wheel with their placements
   dealt onto a card, their traced palm with its lines named, then the finished
   frame with the report's sections locked around it. The card pops on each
   deal; the chips pop in a stagger — this is the one screen in the funnel that
   is allowed to feel like a show, because everything in the show is real. */

.prep-art { display: flex; justify-content: center; margin-top: 0.5rem; min-height: 15rem; }

/* The orrery. Every orbit group spins about the viewBox centre while a
   counter-group inside it spins the other way at the same rate — which is the
   whole trick: the planet rides its orbit and its glyph never turns upside
   down. transform-box is load-bearing twice over: view-box gives the orbit its
   centre, fill-box gives the counter-spin its own. */
.prep-orrery { width: 16rem; height: 16rem; }
.prep-orrery svg { display: block; width: 100%; height: auto; }

.po-ring {
  fill: none;
  stroke: rgba(255, 255, 255, 0.14);
  stroke-width: 1;
  stroke-dasharray: 2.5 6.5;
  stroke-linecap: round;
}

.po-spin {
  transform-box: view-box;
  transform-origin: 50% 50%;
  animation: spin var(--t) linear infinite;
}
.po-counter {
  transform-box: fill-box;
  transform-origin: center;
  animation: po-back var(--t) linear infinite;
}
@keyframes po-back { to { transform: rotate(-360deg); } }

.po-glyph { font-family: var(--f-glyph); fill: #fff; opacity: 0.95; }
.po-glyph-sun { fill: #6B4E1C; }

/* The planet being dealt onto the card glows and grows; everything else keeps
   orbiting. The one thread between the decoration and the data. */
.po-body { transition: transform 0.35s var(--ease), filter 0.35s var(--ease); }
.po-body.is-lit {
  transform: scale(1.3);
  filter: drop-shadow(0 0 0.5rem rgba(128, 140, 238, 0.9));
}
.po-body.po-sun.is-lit { transform: none; }   /* the sun is already the centre */

.po-star {
  fill: #EAEEFF;
  animation: po-twinkle 2.8s ease-in-out infinite;
  animation-delay: var(--d, 0s);
}
@keyframes po-twinkle {
  0%, 100% { opacity: 0.15; }
  50% { opacity: 0.8; }
}

@media (prefers-reduced-motion: reduce) {
  .po-spin, .po-counter, .po-star { animation: none; }
}

.prep-shot {
  width: min(60%, calc(34vh * var(--ar, 0.75)));
  animation: rise 0.5s var(--ease) both;
}
.prep-guide { width: 52%; margin: 0 auto; }

.prep-card {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: min(17rem, 90%);
  margin: 1.25rem auto 0;
  padding: 0.75rem 1rem;
  border-radius: 0.875rem;
  background: linear-gradient(160deg, var(--night-3), var(--night-2));
  border: 1px solid var(--line);
  min-height: 3.25rem;
}
.prep-card:empty { visibility: hidden; }
.prep-card.is-dealt { animation: prep-pop 0.4s var(--ease); }
@keyframes prep-pop {
  0% { transform: scale(0.92); opacity: 0.4; }
  100% { transform: scale(1); opacity: 1; }
}
.prep-card-mark { font-family: var(--f-glyph); font-size: 1.25rem; color: var(--iris-soft); }
.prep-card-mark:empty { display: none; }
.prep-card-icon { display: block; width: 1.625rem; height: 1.625rem; object-fit: contain; }
.prep-card-name { font-weight: 700; }
.prep-card-sub { color: var(--star-dim); font-size: 0.875rem; }

.prep-bar { margin: 1rem auto 0; }

/* ---- the finale: their photograph, and the report locked around it ------- */

.prep-final {
  position: relative;
  display: flex;
  justify-content: center;
  width: 100%;
  padding: 0.5rem 0 1rem;
}

.prep-chip {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.625rem;
  border-radius: 0.75rem;
  background: var(--night-3);
  border: 1px solid var(--line);
  box-shadow: 0 0.5rem 1.25rem rgba(10, 14, 24, 0.5);
  font-size: 0.75rem;
  line-height: 1.25;
  animation: prep-chip-in 0.45s var(--ease) both;
  z-index: 1;
}
.prep-chip-mark { font-size: 1rem; display: flex; align-items: center; }
.prep-chip-icon { display: block; width: 1.25rem; height: 1.25rem; object-fit: contain; }
.prep-chip-body { display: flex; flex-direction: column; }
.prep-chip-body b { font-size: 0.78125rem; }
/* Blurred because it is bought, not because it is fake: every line names a
   real section of the real report. */
.prep-chip-tease { font-style: normal; color: var(--star-dim); filter: blur(3px); user-select: none; }
.prep-chip-lock { font-size: 0.6875rem; opacity: 0.8; align-self: flex-start; }

.chip-tl { top: 4%; left: 0; transform: rotate(-3deg); animation-delay: 0.15s; }
.chip-tr { top: 22%; right: 0; transform: rotate(2deg); animation-delay: 0.3s; }
.chip-bl { bottom: 26%; left: 2%; transform: rotate(2deg); animation-delay: 0.45s; }
.chip-br { bottom: 8%; right: 3%; transform: rotate(-2deg); animation-delay: 0.6s; }

@keyframes prep-chip-in {
  0% { opacity: 0; transform: translateY(0.5rem) scale(0.9); }
  100% { opacity: 1; }
}
/* The end state of the pop must keep each chip's resting tilt, and an
   animation's 100% frame would overwrite it — so the tilt lives on the class
   and the pop animates only opacity and a lift that ends at zero. */
.chip-tl.prep-chip { animation-name: prep-in-tl; }
@keyframes prep-in-tl { 0% { opacity: 0; transform: rotate(-3deg) translateY(0.5rem) scale(0.9); } 100% { opacity: 1; transform: rotate(-3deg); } }
.chip-tr.prep-chip { animation-name: prep-in-tr; }
@keyframes prep-in-tr { 0% { opacity: 0; transform: rotate(2deg) translateY(0.5rem) scale(0.9); } 100% { opacity: 1; transform: rotate(2deg); } }
.chip-bl.prep-chip { animation-name: prep-in-bl; }
@keyframes prep-in-bl { 0% { opacity: 0; transform: rotate(2deg) translateY(0.5rem) scale(0.9); } 100% { opacity: 1; transform: rotate(2deg); } }
.chip-br.prep-chip { animation-name: prep-in-br; }
@keyframes prep-in-br { 0% { opacity: 0; transform: rotate(-2deg) translateY(0.5rem) scale(0.9); } 100% { opacity: 1; transform: rotate(-2deg); } }

@media (prefers-reduced-motion: reduce) {
  .prep-card.is-dealt, .prep-chip { animation-duration: 0.01ms; animation-delay: 0s; }
  .prep-shot { animation: none; }
}

/* ---- no hand in the photograph ------------------------------------------

   The rejected picture stays, dimmed nearly to the background, with the
   funnel's own hand outline sitting over it wearing a question mark. Dimmed
   and not removed: the claim is about THIS photograph, and a claim the viewer
   can check against the evidence reads as care, not as an error page. */

.scan-shot.is-oops img {
  filter: grayscale(0.5) brightness(0.45);
  transition: filter 0.5s var(--ease);
}
.scan-shot.is-oops .scan-sweep::after { animation: none; opacity: 0; }

.oops-badge {
  position: relative;
  z-index: 1;
  width: 5.25rem;
  height: 5.25rem;
  margin: -2.625rem auto 0;      /* half over the photo, like a seal on it */
  border-radius: 50%;
  background: var(--night-2);
  border: 1px solid var(--line);
  box-shadow: 0 0.5rem 1.5rem rgba(10, 14, 24, 0.55);
  display: grid;
  place-items: center;
  animation: rise 0.5s var(--ease) 0.15s both;
}
.oops-hand svg {
  display: block;
  width: 2.5rem;
  height: auto;
  color: var(--iris-soft);
}
.oops-mark {
  position: absolute;
  right: -0.125rem;
  bottom: -0.125rem;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: linear-gradient(150deg, var(--iris-soft), var(--iris-deep));
  border: 2px solid var(--night);
  color: var(--star);
  font-weight: 700;
  font-size: 1rem;
  display: grid;
  place-items: center;
}

.oops-title {
  margin: 1.125rem 0 0;
  font-family: var(--f-display);
  font-size: 1.5rem;
  text-align: center;
  animation: rise 0.5s var(--ease) 0.25s both;
}
.oops-say {
  margin: 0.5rem 0 0;
  max-width: 21rem;
  text-align: center;
  animation: rise 0.5s var(--ease) 0.35s both;
}

@media (prefers-reduced-motion: reduce) {
  .oops-badge, .oops-title, .oops-say { animation-delay: 0s; animation-duration: 0.01ms; }
}

@media (prefers-reduced-motion: reduce) {
  .scan-shot { animation: none; }
  .scan-sweep::after { display: none; }
  .pl-line.is-drawn { animation-duration: 0.01ms; }
}

/* Bigger than it was, and then smaller again. At 52% the hand sat as a small
   mark in a large empty card; at 74% a portrait drawing sized by its width came
   out 91% of the frame's height and the fingertips crowded the top edge. The
   number is a width but the constraint is the height, which is what a tall
   drawing in a 3:4 box does to you. */
.palm-guide { width: 62%; opacity: 0.85; }
.palm-guide svg { display: block; width: 100%; height: auto; color: var(--iris-soft); }
.palm-guide img { display: block; width: 100%; height: auto; }

.disclaimer {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--star-faint);
  margin: 0.75rem auto 0;
  max-width: 36ch;
}
.disclaimer + .disclaimer { margin-top: 0.375rem; }

/* ---- the readiness gauge ------------------------------------------------- */

/* A sphere filling up. The glow behind it is the same one the chart wheel uses,
   because the two screens are a pair — the chart arrives, then this says how
   much of the reading it bought. */
.gauge {
  position: relative;
  width: min(15rem, 66%);
  margin: 0.5rem auto 1.75rem;
}
.gauge::before {
  content: "";
  position: absolute;
  inset: -20%;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(93, 108, 232, 0.28) 0%, rgba(93, 108, 232, 0.08) 50%, transparent 70%);
  filter: blur(6px);
  pointer-events: none;
}
.gauge svg { position: relative; display: block; width: 100%; height: auto; }

/* Stars, scattered wider than the sphere so they read as sky around it rather
   than as decoration stuck to its edge. Drawn as background gradients rather
   than as elements: fourteen dots would otherwise be fourteen nodes, and none
   of them is ever measured, clicked or read.

   background-size is not optional. Without it each gradient is as big as the
   element, background-position slides the whole image instead of the dot inside
   it, and all fourteen end up stacked or off-screen — which is what happened
   the first time. */
.gauge .sky {
  position: absolute;
  inset: -34% -22%;
  pointer-events: none;
  background-repeat: no-repeat;
  background-size: 3px 3px;
}
.gauge .sky-a {
  background-image:
    radial-gradient(circle, rgba(255,255,255,0.85) 0 1.1px, transparent 1.6px),
    radial-gradient(circle, rgba(255,255,255,0.55) 0 1px,   transparent 1.5px),
    radial-gradient(circle, rgba(255,255,255,0.7)  0 1.3px, transparent 1.8px),
    radial-gradient(circle, rgba(255,255,255,0.4)  0 1px,   transparent 1.5px),
    radial-gradient(circle, rgba(255,255,255,0.6)  0 1.1px, transparent 1.6px),
    radial-gradient(circle, rgba(255,255,255,0.45) 0 1px,   transparent 1.5px),
    radial-gradient(circle, rgba(255,255,255,0.75) 0 1.2px, transparent 1.7px);
  background-position:
    14% 12%, 78% 8%, 41% 4%, 90% 26%, 6% 34%, 62% 18%, 27% 24%;
  animation: sky-breathe 7s ease-in-out infinite;
}
.gauge .sky-b {
  background-image:
    radial-gradient(circle, rgba(255,255,255,0.5)  0 1px,   transparent 1.5px),
    radial-gradient(circle, rgba(255,255,255,0.7)  0 1.2px, transparent 1.7px),
    radial-gradient(circle, rgba(255,255,255,0.35) 0 1px,   transparent 1.4px),
    radial-gradient(circle, rgba(255,255,255,0.6)  0 1.1px, transparent 1.6px),
    radial-gradient(circle, rgba(255,255,255,0.45) 0 1px,   transparent 1.5px),
    radial-gradient(circle, rgba(255,255,255,0.55) 0 1.1px, transparent 1.6px),
    radial-gradient(circle, rgba(255,255,255,0.3)  0 1px,   transparent 1.4px);
  background-position:
    30% 9%, 95% 14%, 55% 30%, 3% 18%, 71% 38%, 20% 44%, 86% 47%;
  animation: sky-breathe 11s ease-in-out infinite reverse;
}

/* Never all the way out. A star that disappears entirely is a dead pixel
   coming back; one that only dims is weather. */
@keyframes sky-breathe {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; }
}

.gauge .g-well { fill: var(--night-2); }
.gauge .g-rim  { fill: none; stroke: var(--line); stroke-width: 1.5; }

/* The two surfaces drift at different speeds, so they cross and separate and
   never settle into a pattern the eye can predict. Each travels exactly one
   period, which is what makes the loop seamless — end the run anywhere else
   and the water jumps at the restart.

   They carry the same fill, and that is deliberate rather than lazy. The back
   one was darker and half transparent, which sounded like depth and looked like
   a fault: every time its crest rose above the front one, a thin dark band with
   a hard edge crossed the sphere. At 6s against 9s in opposite directions they
   pass each other every 3.6 seconds, so the band came and went often enough to
   be the first thing anyone noticed.

   Same fill, no boundary. What is left is the union of two sines sliding apart,
   which is a less mechanical line than either of them alone. */
.gauge .g-wave-back {
  animation: g-drift 9s linear infinite;
}
.gauge .g-wave-front {
  animation: g-drift 6s linear infinite reverse;
}
@keyframes g-drift {
  from { transform: translateX(0); }
  to   { transform: translateX(-200px); }
}

.gauge.is-calm .g-wave-back,
.gauge.is-calm .g-wave-front,
.gauge.is-calm .sky { animation: none; }

/* The number sits over the fill, so it has to stay legible against both the
   dark well above the surface and the iris below it. White does both; a colour
   would work against one of them. */
.gauge .g-num {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 44px;
  fill: var(--star);
  text-anchor: middle;
  dominant-baseline: central;
}

.gauge-say { margin-top: 0; }

/* ---- social proof ------------------------------------------------------- */

/* The text is animated, not the pill. Sliding the whole capsule would move its
   border and background against the page; moving only the line inside keeps
   the pill still and makes the change read as a feed updating rather than as a
   box jumping. inline-block because a transform does nothing to an inline. */
.proof-line { display: inline-block; will-change: opacity, transform; }

/* A fixed box, not one that fits its text.
 *
   Sized to its content, the pill was a different width for every name — 270px
   for the shortest line, 283 for the longest — and it swaps line every five
   seconds. That is a reflow of the page on a timer, which is a layout shift in
   its own right and, on a window whose scrollbar only appears when something
   changes, is enough to wake the scrollbar and put a grey line down the right
   edge for a moment. Every five seconds. Which is exactly what it looked like.

   Fixed width, nowrap, centred: the text changes and nothing around it moves. */
.proof {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: min(20rem, 100%);
  /* Was 12px in --star-dim, which measures 6.1:1 against this background and
     still read as background noise: at this size contrast alone does not carry
     it. Up one step on the same scale and one notch in size — no new colour
     invented, and the box widened to match so the longest line still fits on
     one line, which is what keeps the swap from reflowing the page. */
  font-size: 0.8125rem;
  color: var(--star);
  background: var(--night-2);
  border: 1px solid var(--line-soft);
  border-radius: 2rem;
  padding: 0.4375rem 0.875rem;
  margin: 1rem auto 0;
  white-space: nowrap;
  overflow: hidden;
  animation: rise 0.4s var(--ease) both;
}
.proof .dot {
  width: 0.4375rem; height: 0.4375rem; border-radius: 50%;
  background: var(--good); flex: none;
  box-shadow: 0 0 0 0.1875rem rgba(76, 175, 125, 0.18);
}

/* ---- sticky cta --------------------------------------------------------- */

/* The fade runs a long way up, and that length is the point rather than a
   flourish. At 0.75rem of padding the gradient was about twelve pixels: an
   option passing under it was CHOPPED, and a chopped row at the bottom of a
   phone screen looks exactly like the last row. Given two and a half
   centimetres to dissolve in, the same option visibly goes under something,
   which is the whole signal.

   `to top` with the solid stop late means the button still sits on opaque
   ground -- the dissolve happens above it, over the list, not behind the
   label. */
.footer {
  position: sticky;
  bottom: 0;
  padding: 2.75rem 0 0.5rem;
  background: linear-gradient(to top,
              var(--night) 42%,
              color-mix(in srgb, var(--night) 55%, transparent) 74%,
              transparent);
}
@supports not (color: color-mix(in srgb, red 10%, transparent)) {
  .footer { background: linear-gradient(to top, var(--night) 55%, transparent); }
}

/* "5 more below". Deliberately quiet -- mono, small, dimmed -- because it is a
   fact about the screen rather than a thing to do, and a loud one would compete
   with the button underneath it for the same glance. */
/* Taken out of flow on purpose. In normal flow the pill was part of the
   footer's height, so the Continue button dropped thirty pixels the moment the
   last option came into view -- a button that moves under a thumb already on
   its way down. Absolute, it appears and goes without the button knowing, and
   it cannot feed its own size back into the ResizeObserver that measures it. */
.peek {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 0.55rem;
  appearance: none;
  font: inherit;
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  display: block;
  margin: 0;
  padding: 0.4rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--night);
  color: var(--star-dim);
  cursor: pointer;
}
.peek[hidden] { display: none; }
.peek::after { content: ' \2193'; }
.peek:hover { color: var(--star); }

/* Arrives rather than appears. Everything else on the assembly screen has been
   fading and rising for four seconds; a button that simply exists at the end of
   that reads as a different page. */
.cta-arrives { animation: rise 0.45s var(--ease) both; }

.cta {
  appearance: none;
  font: inherit;
  font-size: 1rem;
  font-weight: 600;
  width: 100%;
  border: none;
  border-radius: 2rem;
  padding: 1.0625rem;
  color: #fff;
  background: linear-gradient(180deg, var(--iris-soft), var(--iris-deep));
  cursor: pointer;
  transition: filter 0.18s var(--ease), transform 0.12s var(--ease);
}
.cta:hover:not(:disabled) { filter: brightness(1.08); }
.cta:active:not(:disabled) { transform: scale(0.99); }
.cta:disabled { opacity: 0.35; cursor: not-allowed; }

.cta-stack { display: grid; gap: 0.5rem; }
.cta.secondary {
  background: transparent;
  color: var(--star-dim);
  border: 1px solid var(--line);
}
.cta.secondary:hover { border-color: var(--iris); color: var(--star); filter: none; }

/* ---- site footer -------------------------------------------------------- */

.sitefoot {
  padding: 0.5rem 0 1.5rem;
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--star-faint);
}
.sitefoot a {
  color: var(--star-dim);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.sitefoot a:hover { color: var(--star); }
.sitefoot .legal { display: inline-flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; }

/* ---- processing --------------------------------------------------------- */

.processing { padding: 3rem 0; }

/* ---- the chart arriving -------------------------------------------------- */

/* Four beats, one per line of the copy, driven by data-stage. Everything is
   stacked in one square and faded rather than swapped, so nothing reflows and
   the wheel never jumps.

   0  the ring and its divisions
   1  the signs, the lit sector, the two lights
   2  the wheel steps back and the hand comes forward
   3  both, settled
*/
.proc-art {
  position: relative;
  width: 12rem;
  height: 12rem;
  margin: 0 auto 1.75rem;
}
.proc-art > * {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}

.proc-wheel, .proc-palm {
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease), filter 0.7s var(--ease);
}
.proc-wheel { width: 100%; margin: 0; }
.proc-wheel svg { width: 100%; height: auto; display: block; }

/* Parts of the wheel arrive in their own time. The transition sits on the group,
   the delay staggers them, and stage 0 has only the frame. */
.proc-wheel .rings, .proc-wheel .ticks, .proc-wheel .pending,
.proc-wheel .glyphs, .proc-wheel .sector,
.proc-wheel .sun, .proc-wheel .moon, .proc-wheel .ray, .proc-wheel .asc {
  opacity: 0;
  transition: opacity 0.6s var(--ease);
}
.proc-art[data-stage] .proc-wheel .rings { opacity: 1; }
.proc-art[data-stage] .proc-wheel .ticks { opacity: 1; transition-delay: 0.15s; }
.proc-art[data-stage] .proc-wheel .pending { opacity: 1; transition-delay: 0.3s; }

.proc-art[data-stage="1"] .proc-wheel .glyphs,
.proc-art[data-stage="2"] .proc-wheel .glyphs,
.proc-art[data-stage="3"] .proc-wheel .glyphs { opacity: 1; }
.proc-art[data-stage="1"] .proc-wheel .sector,
.proc-art[data-stage="2"] .proc-wheel .sector,
.proc-art[data-stage="3"] .proc-wheel .sector { opacity: 1; transition-delay: 0.1s; }
.proc-art[data-stage="1"] .proc-wheel .ray,
.proc-art[data-stage="2"] .proc-wheel .ray,
.proc-art[data-stage="3"] .proc-wheel .ray { opacity: 0.5; transition-delay: 0.25s; }
.proc-art[data-stage="1"] .proc-wheel .sun,
.proc-art[data-stage="2"] .proc-wheel .sun,
.proc-art[data-stage="3"] .proc-wheel .sun { opacity: 1; transition-delay: 0.35s; }
.proc-art[data-stage="1"] .proc-wheel .moon,
.proc-art[data-stage="2"] .proc-wheel .moon,
.proc-art[data-stage="3"] .proc-wheel .moon { opacity: 1; transition-delay: 0.5s; }

/* The hand. Absent until its own line, then it comes forward and the wheel
   steps back behind it rather than disappearing — the report joins the two, and
   this is the first time the reader sees them in the same frame. */
/* The width belongs to the drawing, not to the box. The box is inset:0 and
   centres its child; giving it a width as well pins it to the left edge and the
   hand sits off-centre inside the glow. */
.proc-palm { opacity: 0; transform: scale(0.9); }
.proc-palm svg { width: 46%; height: auto; display: block; }
/* The generated drawing is portrait and fuller than the line outline, so it
   takes a touch less of the circle to sit at the same visual weight. */
.proc-palm img { width: 42%; height: auto; display: block; }

.proc-art[data-stage="2"] .proc-palm,
.proc-art[data-stage="3"] .proc-palm { opacity: 1; transform: scale(1); }
.proc-art[data-stage="2"] .proc-wheel { opacity: 0.35; transform: scale(1.04); }
.proc-art[data-stage="3"] .proc-wheel { opacity: 0.7; transform: scale(1); }
.proc-art[data-stage="3"] .proc-palm { transform: scale(0.94); }

/* A sweep behind everything, so the frame reads as working rather than paused.
   It stops on the last beat: the reading is ready, and a spinner that keeps
   spinning after that says the opposite. */
.proc-sweep {
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    transparent 0deg, rgba(93, 108, 232, 0.35) 40deg, transparent 90deg);
  mask: radial-gradient(circle, transparent 61%, #000 62%, #000 70%, transparent 71%);
  -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 70%, transparent 71%);
  animation: spin 2.6s linear infinite;
  opacity: 0.9;
  /* @keyframes spin lives below, with .proc-sweep now its only user — it came
     over with the fallback ring that used to sit above and was the first thing
     to go when that ring did. */
  transition: opacity 0.6s var(--ease);
}
.proc-art[data-stage="3"] .proc-sweep { opacity: 0; animation-play-state: paused; }

@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .proc-sweep { animation: none; }
  .proc-wheel, .proc-palm, .proc-wheel * { transition-duration: 0.01ms !important; }
}

.processing h2 {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 1.375rem;
  margin: 0 auto 1.75rem;
  min-height: 3.3rem;
  max-width: 24ch;
}

/* ---- the recap ----------------------------------------------------------- */

/* Each row lands on its own beat, so the arrival is the whole point of the
   styling here: the card rises, a light passes across it once, and the mark
   pops with a ring going out of it. All of it fires once and settles — a strip
   that keeps shimmering under a wheel that is already turning is two things
   competing for the same glance. */
.recap { display: grid; gap: 0.5rem; text-align: left; max-width: 21rem; margin: 0 auto; }
.recap-row {
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.875rem;
  padding: 0.5rem 0.875rem;
  background: var(--night-2);
  border-radius: 0.625rem;
  border: 1px solid var(--line-soft);
  animation: rise 0.45s var(--ease) both;
}
.recap-row::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg,
    transparent 34%, rgba(128, 140, 238, 0.18) 50%, transparent 66%);
  transform: translateX(-130%);
  animation: sheen 1s var(--ease) 0.12s both;
  pointer-events: none;
}

.rc-mark {
  position: relative;
  width: 2.125rem;
  height: 2.125rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--night-3);
  border: 1px solid var(--line);
  font-size: 1rem;
  line-height: 1;
  color: var(--star-dim);
  animation: pop 0.5s var(--ease) 0.08s both;
}
.rc-mark svg { width: 1.25rem; height: 1.25rem; display: block; }
.rc-mark.is-glyph { font-family: var(--f-glyph); font-variant-emoji: text; }
.rc-mark.is-emoji { font-size: 1.0625rem; }

/* The ring, thrown off once as the mark lands. It is drawn on a pseudo-element
   so it can outgrow the disc without changing the row's height; the row clips
   it, which is what makes it read as a pulse rather than a growing circle. */
.rc-mark::before {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  border: 1px solid currentColor;
  animation: ring 1s var(--ease) 0.22s both;
}

.rc-text {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-width: 0;
}
/* No min-width:0 here on purpose. The detail line is short and fixed — a
   coordinate pair, a weekday — and letting the column shrink under it breaks it
   across two lines with a lone hemisphere letter stranded on the second. The
   value is the elastic half of the row: a long city name wraps, which reads
   fine, and a broken coordinate does not. */
.rc-label { display: grid; gap: 0.1rem; }
.recap-row .k { color: var(--star-faint); }
.recap-row .v {
  color: var(--star);
  font-weight: 600;
  text-align: right;
  animation: fade-in 0.5s var(--ease) 0.28s both;
}
.rc-sub {
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--star-faint);
  animation: fade-in 0.5s var(--ease) 0.4s both;
}

/* The sun's mark wears its element, the same four colours the report uses for
   the same four things. */
.recap-row.is-fire  .rc-mark { color: var(--gilt); }
.recap-row.is-earth .rc-mark { color: var(--good); }
.recap-row.is-air   .rc-mark { color: var(--iris-soft); }
.recap-row.is-water .rc-mark { color: #7BA3C4; }
.recap-row.is-here  .rc-mark { color: var(--iris-soft); }
.recap-row[class*="is-"] .rc-mark {
  border-color: color-mix(in srgb, currentColor 35%, transparent);
  background: color-mix(in srgb, currentColor 10%, var(--night-3));
}

@keyframes sheen { to { transform: translateX(130%); } }
@keyframes pop {
  from { opacity: 0; transform: scale(0.5); }
  60%  { transform: scale(1.08); }
  to   { opacity: 1; transform: none; }
}
@keyframes ring {
  from { opacity: 0.6; transform: scale(1); }
  to   { opacity: 0; transform: scale(2); }
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

/* ---- early access -------------------------------------------------------- */

/* The slot the card form will occupy. Styled as part of the funnel rather than
   as a debug panel, because it is what a visitor actually meets today — a
   product that is not selling yet should look closed, not broken. */
.code-gate { text-align: center; }
.code-row {
  display: flex;
  gap: 0.5rem;
  max-width: 21rem;
  margin: 1.5rem auto 0;
}
.code-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.875rem 1rem;
  font-family: var(--f-mono);
  font-size: 1rem;
  letter-spacing: 0.08em;
  color: var(--star);
  background: var(--night-2);
  border: 1px solid var(--line);
  border-radius: 0.625rem;
}
.code-input::placeholder { color: var(--star-faint); letter-spacing: 0.04em; }
.code-input:focus { outline: none; border-color: var(--iris-soft); }
.code-row .cta { flex: none; width: auto; padding-inline: 1.5rem; margin: 0; }
.code-trouble {
  margin: 0.875rem auto 0;
  max-width: 21rem;
  font-size: 0.875rem;
  color: #E8846C;
}

/* ---- accessibility ------------------------------------------------------ */

:focus-visible {
  outline: 2px solid var(--iris-soft);
  outline-offset: 2px;
  border-radius: 0.25rem;
}

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Delay is switched off with duration, not left behind it.
 *
   A staged reveal is built out of both: the duration is the movement and the
   delay is the queue. Collapsing only the first leaves someone who asked not to
   be animated waiting the full length of the queue for pieces that then appear
   with no animation at all — the slowest version of the screen and the least
   informative. Both go, and they get the finished thing at once. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0.01ms !important;
  }
}

/* Small phones: keep three-across from crushing the labels. */
@media (max-width: 23.4375rem) {
  :root { --pad: 1.125rem; }
  .options[data-cols="3"] .option { padding: 1.125rem 0.25rem 1rem; }
  .options[data-cols="3"] .option .label { font-size: 0.75rem; }
  .option .icon { width: 2.375rem; height: 2.375rem; font-size: 1.1875rem; }
  h1.question { font-size: 1.5rem; }
}

/* ---- the offer ---------------------------------------------------------- */

.offer { padding-bottom: 1.5rem; }

.plans-wrap { margin: 0 0 1.25rem; }
.plans { display: grid; gap: 0.625rem; }

.plan {
  position: relative;
  appearance: none;
  font: inherit;
  color: var(--star);
  background: var(--night-2);
  border: 1.5px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.125rem;
  cursor: pointer;
  display: grid;
  gap: 0.1875rem;
  text-align: left;
  transition: border-color 0.18s var(--ease), background 0.18s var(--ease);
}
.plan:hover { background: var(--night-3); }
.plan.is-chosen { border-color: var(--iris); background: var(--iris-glow); }

.plan-badge {
  position: absolute;
  top: -0.5rem;
  right: 0.875rem;
  font-family: var(--f-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #1A1204;
  background: var(--gilt);
  border-radius: 1rem;
  padding: 0.1875rem 0.5rem;
}
.plan-term { font-size: 0.9375rem; font-weight: 600; }
.plan-price { font-size: 1.375rem; font-variant-numeric: tabular-nums; }
.plan-price s { color: var(--star-faint); font-size: 0.9375rem; margin-right: 0.25rem; }
.plan-price b { font-weight: 700; }
/* `.plan-then` was here twice, in the two copies of this block, and matched
   nothing after the plan rows lost their renewal line. Both are gone. */

/* Above the button, never behind a scroll. */
.terms {
  background: var(--night-2);
  border: 1px solid var(--line);
  border-left: 3px solid var(--gilt);
  border-radius: 0.75rem;
  padding: 0.875rem 1rem;
  margin: 0 0 1rem;
  text-align: left;
}
.terms-line { font-size: 0.8125rem; line-height: 1.5; margin: 0; color: var(--star); }
.terms-line strong { font-weight: 650; }
.terms-line.quiet { color: var(--star-faint); margin-top: 0.375rem; }

.consent {
  display: flex;
  gap: 0.625rem;
  align-items: flex-start;
  text-align: left;
  font-size: 0.8125rem;
  color: var(--star-dim);
  margin: 0 0 1rem;
  cursor: pointer;
}
.consent input { width: 1.125rem; height: 1.125rem; flex: none; margin-top: 0.0625rem; accent-color: var(--iris); }
.consent a { color: var(--star-dim); }

.offer-cta { margin: 0 0 0.875rem; }
.guarantee { font-size: 0.75rem; color: var(--star-faint); margin: 0; }
.guarantee a, .consent a { text-underline-offset: 2px; }

/* ---- the locked report -------------------------------------------------- */

/* Fades out at the bottom rather than ending on a hard edge: the page should
   feel cut off mid-thought, because that is exactly what it is. */
.report-preview {
  position: relative;
  text-align: left;
  background: var(--night-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.125rem 1.25rem 3rem;
  margin: 0 0 1.25rem;
  overflow: hidden;
}
.report-preview::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 5rem;
  background: linear-gradient(to top, var(--night-2) 30%, transparent);
  pointer-events: none;
}

.rp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: 0.75rem;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--line-soft);
}
.rp-title {
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--star-faint);
}
.rp-lock { font-size: 0.875rem; }

.report-preview section + section { margin-top: 1.125rem; }
.report-preview h3 {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 1.0625rem;
  margin: 0 0 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Sits on the cap height of the title beside it rather than on the baseline,
   which is where a 17px drawing next to a serif looks centred. */
.rp-icon {
  width: 1.125rem;
  height: 1.125rem;
  flex: none;
  color: var(--iris-soft);
  margin-top: -0.0625rem;
}
.rp-icon svg { display: block; width: 100%; height: 100%; }

.rp-places {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem 0.875rem;
  margin-bottom: 0.625rem;
}
.rp-place {
  font-size: 0.8125rem;
  color: var(--star);
  background: rgba(93, 108, 232, 0.16);
  border-radius: 1rem;
  padding: 0.1875rem 0.625rem;
  white-space: nowrap;
}
.rp-place i {
  font-family: var(--f-glyph);
  font-variant-emoji: text;
  font-style: normal;
  color: var(--iris-soft);
}

.rp-line { font-size: 0.875rem; line-height: 1.5; margin: 0 0 0.625rem; color: var(--star); max-width: none; }
.rp-line.quiet { color: var(--star-faint); }

/* The teaser paragraph: real prose from the report, held out of focus.

   The blur sits on an inner span rather than on the paragraph itself. A filter
   inherits down the whole subtree, so anything ever placed beside the blurred
   run — a label, a sharp line — would be dragged out of focus with it. Keeping
   the filter one level in leaves that door open. */
.teaser {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--star-dim);
  margin: 0.5rem 0 0;
  max-width: none;
  user-select: none;
}
.teaser .fog {
  filter: blur(3.5px);
  opacity: 0.75;
}

/* ---- the chart, locked --------------------------------------------------- */

/* The blur sits on an inner wrapper, not on .rp-wheel itself: the glow behind
   the wheel is a pseudo-element on .wheel-chart, and blurring the parent would
   blur the glow into a grey square. This way the artwork softens and the light
   behind it stays light. */
.rp-wheel {
  position: relative;
  width: min(13rem, 62%);
  margin: 0 auto 1rem;
  pointer-events: none;
}
.rp-wheel .wheel-chart {
  width: 100%;
  margin: 0;
  filter: blur(2.5px);
  opacity: 0.85;
}

/* A hairline of gold at the rim, so the card reads as a document with something
   held inside it rather than as an image that failed to load. */
.rp-wheel::after {
  content: "";
  position: absolute;
  inset: 4%;
  border-radius: 50%;
  border: 1px solid rgba(214, 166, 76, 0.22);
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .rp-wheel .wheel-chart { transition: filter 0.4s ease; }
}

/* ---- the palm locator ---------------------------------------------------- */

/* These live here rather than in report.css because both pages draw this hand:
   the report beside every mark, and the paywall card in its palm block. When the
   rules were report-only the funnel rendered the silhouette with no stylesheet
   at all — an SVG <path> with no fill declared is filled solid black, so the
   drawing arrived as a black blob and looked like a broken asset.

   Same failure the chart wheel had once. A shape that is styled entirely from
   outside itself has to travel with its styles.
*/
/* ---- the locator --------------------------------------------------------- */

.palm-map {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}
.palm-sil {
  fill: var(--night-2);
  stroke: var(--star-faint);
  stroke-width: 1.2;
  opacity: 0.85;
}
.palm-sil.is-on {
  fill: rgba(93, 108, 232, 0.16);
  stroke: var(--iris-soft);
  stroke-width: 1.8;
  opacity: 1;
}
.palm-map path.heart,
.palm-map path.head,
.palm-map path.life,
.palm-map path.fate,
.palm-map .relationship path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.palm-map .is-dim {
  color: var(--star-faint);
  opacity: 0.22;
}
.palm-map .mounts.is-dim { opacity: 0; }
.palm-map .is-on {
  color: var(--iris-soft);
  opacity: 1;
}

/* Each lit line wears the colour the scan screen introduced it in, so "the
   pink one" on the funnel is the pink one beside its paragraph here. The
   silhouette, mounts and relationship marks stay in the house accent — they
   never had scan colours to keep. */
.palm-map path.heart.is-on { color: #F0648C; }
.palm-map path.head.is-on  { color: #E8B65A; }
.palm-map path.life.is-on  { color: #3FC2AE; }
.palm-map path.fate.is-on  { color: #9B8BF4; }
.palm-map path.is-on,
.palm-map .is-on path { stroke-width: 2.6; }
.palm-map .is-missing {
  stroke-dasharray: 3 3;
  opacity: 0.55;
}
@media (max-width: 28rem) {
  .r-mark { grid-template-columns: 4.5rem 1fr; gap: 0.2rem 0.75rem; }
  .r-palm-map { width: 4.5rem; }
}

/* ---- the element count, sharp -------------------------------------------- */

/* Nothing is withheld in a count, so nothing here is blurred. Four numbers the
   reader can check against their own chart are worth more than a paragraph
   about how carefully they were arrived at. */
.rp-elements {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem 0.5rem;
  margin: 0.625rem 0 0;
}
.rp-el {
  display: inline-flex;
  align-items: baseline;
  gap: 0.375rem;
  padding: 0.1875rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.75rem;
  color: var(--star-dim);
}
.rp-el b { color: var(--star); font-variant-numeric: tabular-nums; }
.rp-el.empty { opacity: 0.45; }

/* ---- the palm's shape, where prose would be a lie ------------------------ */

/* The locator sits beside the rows it stands for, at the size the report uses
   it — the drawing is the honest half of this block, and the rows are the shape
   of what has not been written yet. */
.rp-palm { display: flex; align-items: center; gap: 0.875rem; margin-top: 0.5rem; }
.rp-palm-map { width: 4.5rem; flex: none; opacity: 0.75; }
.rp-palm .rp-skeleton { flex: 1; margin-top: 0; }

@media (max-width: 26rem) {
  .rp-palm-map { width: 3.5rem; }
}


/* The one block on this card that is not withheld text but absent text. It is
   drawn as structure — a row per mark, a bar with nothing in it — so the reader
   sees how much is coming without being told anything about their own hand
   before anyone has looked at it. */
.rp-skeleton { display: grid; gap: 0.4375rem; margin-top: 0.5rem; }

.rp-skel-row {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  align-items: center;
  gap: 0.625rem;
}
.rp-skel-label { font-size: 0.8125rem; color: var(--star-dim); }

.rp-skel-bar {
  height: 0.5rem;
  border-radius: 0.25rem;
  background: linear-gradient(90deg,
    rgba(255, 255, 255, 0.14) 0%,
    rgba(255, 255, 255, 0.06) 60%,
    rgba(255, 255, 255, 0.02) 100%);
}
@media (max-width: 26rem) {
  .rp-skel-row { grid-template-columns: 6.5rem 1fr; }
}

/* ---- the offer ---------------------------------------------------------- */

.offer { padding-bottom: 1.5rem; }

.plans-wrap { margin: 0 0 1.25rem; }
.plans { display: grid; gap: 0.625rem; }

.plan {
  position: relative;
  appearance: none;
  font: inherit;
  color: var(--star);
  background: var(--night-2);
  border: 1.5px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.125rem;
  cursor: pointer;
  display: grid;
  gap: 0.1875rem;
  text-align: left;
  transition: border-color 0.18s var(--ease), background 0.18s var(--ease);
}
.plan:hover { background: var(--night-3); }
.plan.is-chosen { border-color: var(--iris); background: var(--iris-glow); }

.plan-badge {
  position: absolute;
  top: -0.5rem;
  right: 0.875rem;
  font-family: var(--f-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #1A1204;
  background: var(--gilt);
  border-radius: 1rem;
  padding: 0.1875rem 0.5rem;
}
.plan-term { font-size: 0.9375rem; font-weight: 600; }
.plan-price { font-size: 1.375rem; font-variant-numeric: tabular-nums; }
.plan-price s { color: var(--star-faint); font-size: 0.9375rem; margin-right: 0.25rem; }
.plan-price b { font-weight: 700; }

/* Above the button, never behind a scroll. */
.terms {
  background: var(--night-2);
  border: 1px solid var(--line);
  border-left: 3px solid var(--gilt);
  border-radius: 0.75rem;
  padding: 0.875rem 1rem;
  margin: 0 0 1rem;
  text-align: left;
}
.terms-line { font-size: 0.8125rem; line-height: 1.5; margin: 0; color: var(--star); }
.terms-line strong { font-weight: 650; }
.terms-line.quiet { color: var(--star-faint); margin-top: 0.375rem; }

.consent {
  display: flex;
  gap: 0.625rem;
  align-items: flex-start;
  text-align: left;
  font-size: 0.8125rem;
  color: var(--star-dim);
  margin: 0 0 1rem;
  cursor: pointer;
}
.consent input { width: 1.125rem; height: 1.125rem; flex: none; margin-top: 0.0625rem; accent-color: var(--iris); }
.consent a { color: var(--star-dim); }

.offer-cta { margin: 0 0 0.875rem; }
.guarantee { font-size: 0.75rem; color: var(--star-faint); margin: 0; }
.guarantee a, .consent a { text-underline-offset: 2px; }

/* ---- the locked report -------------------------------------------------- */

/* Fades out at the bottom rather than ending on a hard edge: the page should
   feel cut off mid-thought, because that is exactly what it is. */
.report-preview {
  position: relative;
  text-align: left;
  background: var(--night-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.125rem 1.25rem 3rem;
  margin: 0 0 1.25rem;
  overflow: hidden;
}
.report-preview::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 5rem;
  background: linear-gradient(to top, var(--night-2) 30%, transparent);
  pointer-events: none;
}

.rp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: 0.75rem;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--line-soft);
}
.rp-title {
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--star-faint);
}
.rp-lock { font-size: 0.875rem; }

.report-preview section + section { margin-top: 1.125rem; }

/* Two sections instead of four.
 *
   The card's bottom padding and its fade were measured against a four-section
   card: 3rem of floor under a 5rem gradient reads as "there is more of this
   below" when there genuinely is. With the Birth Chart Report closed the card
   is a palm block and a meet block, and that same floor became a third of the
   card, painting a promise of content that is not under it. Set by class rather
   than guessed, so both states are deliberate. See assets/flags.js.

   Applied in the SECOND of the two .report-preview blocks in this file (the
   first, near the top, is a duplicate that loses on order). */
.report-preview.is-palm-only { padding-bottom: 2rem; }
.report-preview.is-palm-only::after { height: 3rem; }

.report-preview h3 {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 1.0625rem;
  margin: 0 0 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Sits on the cap height of the title beside it rather than on the baseline,
   which is where a 17px drawing next to a serif looks centred. */
.rp-icon {
  width: 1.125rem;
  height: 1.125rem;
  flex: none;
  color: var(--iris-soft);
  margin-top: -0.0625rem;
}
.rp-icon svg { display: block; width: 100%; height: 100%; }

.rp-places {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem 0.875rem;
  margin-bottom: 0.625rem;
}
.rp-place {
  font-size: 0.8125rem;
  color: var(--star);
  background: rgba(93, 108, 232, 0.16);
  border-radius: 1rem;
  padding: 0.1875rem 0.625rem;
  white-space: nowrap;
}
.rp-place i {
  font-family: var(--f-glyph);
  font-variant-emoji: text;
  font-style: normal;
  color: var(--iris-soft);
}

.rp-line { font-size: 0.875rem; line-height: 1.5; margin: 0 0 0.625rem; color: var(--star); max-width: none; }
.rp-line.quiet { color: var(--star-faint); }

/* Bars, not blurred text. Nothing is being withheld that has been written. */
/* ---- what is behind the paywall ----------------------------------------- */

.offer-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
  margin: 0 0 1.5rem;
}
.offer-stats .stat {
  background: var(--night-2);
  border: 1px solid var(--line-soft);
  border-radius: 0.75rem;
  padding: 0.625rem 0.25rem;
}
.offer-stats .stat b {
  display: block;
  font-family: var(--f-display);
  font-size: 1.375rem;
  font-weight: 700;
  color: var(--iris-soft);
  line-height: 1.1;
}
.offer-stats .stat span {
  font-family: var(--f-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--star-faint);
}
.offer-stats .stat-note {
  grid-column: 1 / -1;
  font-size: 0.8125rem;
  color: var(--star-dim);
  margin: 0.375rem auto 0;
  max-width: 34ch;
}

/* ---- report-grade chart ------------------------------------------------- */

.report-wheel { width: min(21rem, 92%); margin: 0 auto 1.5rem; }

/* The horizon and the meridian. Heavier than the sign divisions because they
   are the only lines on the chart that sit at a true degree rather than on a
   sign boundary. */
.wheel-chart .axis {
  stroke: var(--star-dim);
  stroke-width: 1.4;
  opacity: 0.7;
}
.wheel-chart .asc-axis { stroke: var(--iris-soft); stroke-width: 1.8; opacity: 0.9; }
.wheel-chart .mc-axis  { stroke: var(--gilt); }

.wheel-chart .axis-label {
  font-family: var(--f-mono);
  font-size: 7px;
  letter-spacing: 0.08em;
  fill: var(--star-faint);
  text-anchor: middle;
  dominant-baseline: central;
}

/* Quieter than the planets, and deliberately so: the house numbers are the
   chart's grid lines written out, and a grid that competes with the data is a
   grid drawn wrong. */
.wheel-chart .house-numbers text {
  font-family: var(--f-mono);
  font-size: 6px;
  fill: var(--star-faint);
  opacity: 0.65;
  text-anchor: middle;
  dominant-baseline: central;
}

/* ---- the shelf ------------------------------------------------------------

   Two reports, listed. Built from the option card the funnel already uses on
   every screen rather than from a second set of rules that happen to look
   similar: same night-2 field, same 1rem radius, same circular badge carrying
   the accent, same hover. A shelf that is visibly the same object the visitor
   has been tapping for fifteen screens does not need to introduce itself. */

.my-label {
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--iris-soft);
  margin: 0.5rem 0 0.85rem;
}

.my-list { display: grid; gap: 0.75rem; margin: 0 0 1.25rem; }

/* .option's shape, said again here rather than by adding a class to the markup:
   these are links to somewhere, not choices in a question, and borrowing the
   name would mean every future rule written for .option lands on them too. */
.my-row {
  display: grid;
  grid-template-columns: 3rem minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.875rem;
  padding: 0.875rem 1rem;
  text-align: left;
  color: var(--star);
  text-decoration: none;
  background: var(--night-2);
  border: 1px solid transparent;
  border-radius: var(--radius);
  transition: background 0.18s var(--ease), border-color 0.18s var(--ease),
              transform 0.12s var(--ease);
}
.my-row:hover { background: var(--night-3); }
.my-row:focus-visible { border-color: var(--iris); background: var(--iris-glow); }
.my-row:active { transform: scale(0.985); }

/* The badge is the accent's main job on this screen, exactly as it is on an
   option row. */
.my-art {
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  background: linear-gradient(150deg, var(--iris-soft), var(--iris-deep));
  display: grid;
  place-items: center;
  color: #fff;
  flex: none;
}
.my-art svg { width: 1.6rem; height: 1.6rem; }

/* A drawing brings its own field, so the badge gives up its gradient — the
   illustrations are a periwinkle outline around a white fill, and on a
   periwinkle disc the outline is the disc. Same answer the funnel reached for
   its own twelve, and the same size lesson with it: an illustration below
   about 44px stops having parts and becomes a smudge, so it takes the whole
   badge rather than sitting inside it. */
.my-art.has-art { background: none; }
.my-art.has-art img { width: 100%; height: 100%; object-fit: contain; display: block; }

.my-text { display: grid; gap: 0.15rem; min-width: 0; }

.my-title { font-size: 1rem; font-weight: 600; line-height: 1.3; }

/* Two lines at most: the state is a reassurance, not a paragraph. */
.my-state {
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--star-dim);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.my-go { font-size: 1.375rem; line-height: 1; color: var(--star-faint); }

/* A row with nothing behind it yet. Gold is the reserved colour for exactly
   this, and the dashed edge says "not yet" without a padlock, which would
   claim the thing is being withheld rather than not made. */
.my-row.is-waiting { background: transparent; border-color: var(--line); border-style: dashed; }
.my-row.is-waiting:hover { background: var(--night-2); }
/* :not(.has-art) is load-bearing. This rule was written when the mark was a
   glyph, which needs a ground to sit on, and it out-specifies
   `.my-art.has-art { background: none }` two classes to one -- so a drawing in
   the waiting state got its disc back while the same drawing in every other
   state did not. The ground belongs to the glyph, not to the badge. */
.my-row.is-waiting .my-art:not(.has-art) {
  background: var(--night-3);
  color: var(--star-faint);
}
.my-cta {
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--gilt);
  white-space: nowrap;
}

.my-note {
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--star-faint);
  margin: 0;
  max-width: 40ch;
}

@media (max-width: 22rem) {
  .my-row { grid-template-columns: 2.5rem minmax(0, 1fr) auto; gap: 0.7rem; }
  /* Not below 2.75rem even here: the drawing needs the room more than the
     text does, and the row has a whole column to give back. */
  .my-art { width: 2.75rem; height: 2.75rem; }
  .my-art svg { width: 1.35rem; height: 1.35rem; }
}

/* The badge on the free row.
 *
   Pinned to the row's top edge and allowed to sit half outside it, which is
   what makes it read as a sticker on the card rather than a field inside it —
   the same trick a price tag uses. The row is the positioning context; nothing
   else in the shelf needed one, so it is set here rather than on .my-row's
   base rule where it would look like a mistake waiting to be tidied away. */
.my-row { position: relative; }

.my-gift {
  position: absolute;
  top: -0.55rem;
  right: 0.9rem;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.55rem 0.2rem 0.45rem;
  border-radius: 999px;
  background: var(--good);
  color: #0E1422;
  font-family: var(--f-mono);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* The row lifts on hover; the sticker has to lift with it or it detaches. */
  pointer-events: none;
}
/* The mark, sized so a drawing has somewhere to be. At 0.75rem the emoji was
   fine — a colour emoji is drawn to work at twelve pixels — but a generated
   silhouette is not, and every pixel here is one the pill can spare. */
.my-gift-mark {
  font-family: var(--f-glyph), "Segoe UI Emoji", "Apple Color Emoji", sans-serif;
  font-size: 0.875rem;
  line-height: 1;
  display: grid;
  place-items: center;
  width: 0.875rem;
  height: 0.875rem;
}
.my-gift-mark img { width: 100%; height: 100%; object-fit: contain; display: block; }

@media (max-width: 22rem) {
  .my-gift { right: 0.6rem; font-size: 0.5625rem; }
}

/* ---- the masthead's avatar ------------------------------------------------

   Three columns so the logo stays optically centred with something on one
   side of it. The avatar was a <details> panel holding an email and a birth
   date; it is a link to profile.html now, which is the page that panel was a
   worse copy of. */

.masthead.has-profile {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
}
.masthead.has-profile .mark { grid-column: 2; }

/* The avatar in the masthead, which is now a link to the profile page rather
   than the summary of a dropdown. The panel it used to open -- an email, a
   birth date, a reset link -- is gone: it was a smaller, worse copy of the
   page it now leads to. */
/* No ring. The drawing is made for this palette and brings its own weight, the
   same reason .option .icon.has-art and .my-art.has-art drop their grounds --
   a bordered circle around it makes the circle the subject and the person the
   filling.

   THE BOX STAYS 2.25rem even though nothing is painted on it. It is what the
   thumb hits, and 36px is already the smallest a target on this page should
   be; shrinking it to the ink would make the link harder to press than the
   ring made it look. */
.my-profile-link {
  justify-self: start;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  color: var(--star-dim);
  font-size: 0.875rem;
  font-weight: 650;
  text-decoration: none;
  transition: opacity 0.18s var(--ease), color 0.18s var(--ease);
}
.my-profile-link:hover { opacity: 0.75; color: var(--star); }
.my-profile-link svg { width: 1.15rem; height: 1.15rem; }
/* Larger than the path, for the reason the profile page's avatar carries: the
   icons in this set are fitted with their own margin, so at the path's size a
   drawing sits shrunken inside the ring. */
.my-profile-art { display: block; width: 1.6rem; height: 1.6rem; }
.my-profile-art img { width: 100%; height: 100%; object-fit: contain; display: block; }

/* ---- the profile --------------------------------------------------------- */

/* The chart this browser worked out, as a page. Everything here is arithmetic
   -- no reading, no payment -- which is why it is the one screen that exists
   before anything has been bought. */

.masthead-pad { width: 2.125rem; }

.pf-who {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  margin: 0.5rem 0 0;
  text-align: left;
}
/* Ringless too, and for the same reason as the badge in the shelf's masthead.
   These are the same drawing in two places; one of them in a circle and one
   not would undo the point of using the same drawing. */
.pf-avatar {
  flex: none;
  width: 3.25rem;
  height: 3.25rem;
  display: grid;
  place-items: center;
  color: var(--star-dim);
  font-family: var(--f-display);
  font-size: 1.35rem;
}
.pf-avatar svg { width: 1.6rem; height: 1.6rem; display: block; }
/* Bigger than the path it replaced. The drawing carries its own margin -- every
   icon in the set is fitted with one -- so at 1.6rem it looked shrunken inside
   the ring where the path, drawn edge to edge, did not. */
.pf-avatar-art { display: block; width: 2.9rem; height: 2.9rem; }
.pf-avatar-art img { width: 100%; height: 100%; object-fit: contain; display: block; }
.pf-name {
  margin: 0;
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 1.4rem;
  line-height: 1.1;
  color: var(--star);
}
/* ---- settings ------------------------------------------------------------ */

/* Every row on this page either states a fact or destroys one. There is no
   third kind, which is why it is short. */

.st-block { margin: 1.6rem 0 0; text-align: left; }
.st-head {
  margin: 0 0 0.7rem;
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--iris-soft);
}
.st-note {
  margin: 0.7rem 0 0;
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--star-faint);
}

.st-rows {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.45rem 1rem;
  margin: 0;
  padding: 0.9rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--night-2);
  font-size: 0.875rem;
}
.st-rows dt { color: var(--star-faint); }
.st-rows dd { margin: 0; color: var(--star); overflow-wrap: anywhere; }

/* A thing that can be thrown away, and the confirmation it turns into. The row
   is the dialog: a real one over a list this short would be a heavier promise
   than any of these deserve. */
.st-do {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 0.55rem 0 0;
  padding: 0.8rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--night-2);
}
/* The mark, and no disc behind it. These are drawn for this palette and bring
   their own weight, exactly as the funnel's own twelve do -- a gradient circle
   under one would make the circle the subject. */
.st-do-art {
  flex: none;
  display: block;
  width: 2.25rem;
  height: 2.25rem;
}
.st-do-art img { width: 100%; height: 100%; object-fit: contain; display: block; }
.st-do-say { flex: 1; min-width: 0; }
.st-do-label {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 650;
  color: var(--star);
}
.st-do-note {
  margin: 0.15rem 0 0;
  font-size: 0.75rem;
  line-height: 1.45;
  color: var(--star-faint);
}
.st-do-pair { display: flex; gap: 0.4rem; flex: none; }
.st-do-go,
.st-do-no,
.st-do-yes {
  appearance: none;
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 650;
  flex: none;
  min-height: 2.25rem;
  padding: 0 0.85rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--star-dim);
  cursor: pointer;
}
.st-do-go:hover, .st-do-no:hover { color: var(--star); border-color: var(--star-faint); }
.st-do-yes {
  border-color: transparent;
  background: var(--bad, #C4707E);
  color: #fff;
}
.st-do-none { flex: none; color: var(--star-faint); }

.st-links {
  display: grid;
  gap: 0.55rem;
}


/* The five faces. Big enough to hit with a thumb and no bigger, because a row
   of large emoji reads as a game. */
.st-contact {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem;
  border-radius: 999px;
  background: var(--iris);
  color: #fff;
  font-size: 0.9375rem;
  font-weight: 650;
  text-decoration: none;
}
/* Class, not tag: the language row has nowhere to go and is a div, and it has
   to look identical to the rows that are links. */
.st-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 3rem;
  padding: 0 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--night-2);
  color: var(--star);
  font-size: 0.9375rem;
  text-decoration: none;
}
a.st-row:hover { border-color: var(--iris); }
.st-row-say { flex: 1; min-width: 0; }
/* Right-aligned and quiet: it is the state of the row, not a second label. */
.st-row-tail { flex: none; font-size: 0.75rem; color: var(--star-faint); }

/* The only red thing on the page, and the last. */
/* Two lines: what it does, and to whom. The address is the half the reference
   screen got right -- on a shared phone it is the only thing that answers
   "whose session is this?" before the button empties it. */
.st-reset {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.05rem;
  min-height: 3.25rem;
  margin: 0.8rem 0 0;
  border: 1px solid color-mix(in srgb, var(--bad, #C4707E) 45%, transparent);
  border-radius: var(--radius);
  color: var(--bad, #C4707E);
  font-size: 0.9375rem;
  font-weight: 650;
  text-decoration: none;
}
@supports not (color: color-mix(in srgb, red 10%, transparent)) {
  .st-reset { border-color: var(--line); }
}
.st-reset:hover { background: rgba(196, 112, 126, 0.1); }
.st-reset-say { font-size: 0.9375rem; font-weight: 650; line-height: 1.2; }
/* Dimmer and smaller: it is a label on the button, not a second button. */
.st-reset-who {
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1.2;
  opacity: 0.72;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- a legal page -------------------------------------------------------- */

/* Prose, ranged left, at a width that can be read rather than scanned. These
   are the two pages on the site somebody reads because they are checking, and
   a checker needs paragraphs they can find their place in again. */
.lg { text-align: left; max-width: 40rem; margin: 0 auto; padding-bottom: 3rem; }
.lg-title {
  margin: 0.25rem 0 0;
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 1.9rem;
  line-height: 1.15;
  color: var(--star);
}
.lg-when {
  margin: 0.4rem 0 0;
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--star-faint);
}
.lg-lead {
  margin: 1.2rem 0 0;
  font-size: 1rem;
  line-height: 1.65;
  color: var(--star-dim);
}
/* The one thing on the page that must not be skimmed past. */
.lg-draft {
  margin: 1.2rem 0 0;
  padding: 0.9rem 1rem;
  border: 1px solid color-mix(in srgb, var(--bad, #C4707E) 40%, transparent);
  border-radius: var(--radius);
  background: rgba(196, 112, 126, 0.08);
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--star);
}
@supports not (color: color-mix(in srgb, red 10%, transparent)) {
  .lg-draft { border-color: var(--line); }
}
.lg h2 {
  margin: 2rem 0 0;
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 1.25rem;
  color: var(--star);
}
.lg h3 {
  margin: 1.4rem 0 0;
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--iris-soft);
  font-weight: 500;
}
.lg p {
  margin: 0.7rem 0 0;
  font-size: 0.9375rem;
  line-height: 1.7;
  color: var(--star-dim);
}
.lg p strong { color: var(--star); font-weight: 650; }
.lg a { color: var(--iris-soft); }
.lg-list {
  margin: 0.7rem 0 0;
  padding-left: 1.1rem;
  font-size: 0.9375rem;
  line-height: 1.7;
  color: var(--star-dim);
}
.lg-list li { margin: 0.35rem 0 0; }
.lg-list strong { color: var(--star); font-weight: 650; }
/* A section that is knowingly unwritten. Set apart so it cannot be mistaken
   for a term that has been agreed. */
.lg-gap {
  padding: 0.8rem 1rem;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  color: var(--star-faint);
  font-style: italic;
}
.lg-foot {
  margin: 2.5rem 0 0;
  padding-top: 1.2rem;
  border-top: 1px solid var(--line);
  font-size: 0.875rem;
  color: var(--star-faint);
}

/* ---- the paywall's new blocks -------------------------------------------
   Built when the offer screen was rebuilt on a reference funnel's structure:
   plans first, then what the plans buy, then the terms and the button. The
   locked palm card on it reuses the email step's classes on purpose -- the
   same card an hour earlier in the funnel, from one stylesheet block, so the
   two screens cannot drift into looking like two products. */

.ofr-block { margin: 1.75rem 0 0; text-align: left; }

.ofr-head {
  margin: 0 0 0.75rem;
  font-family: var(--f-display);
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--star);
  text-align: center;
}

/* What the buyer opens. A tick per row, as the reference has it -- the
   difference is that every row here is a block the report actually contains. */
/* The card. The list used to carry this itself; the rating now sits above the
   list inside the same border, so the chrome belongs to the wrapper. */
.ofr-card {
  background: var(--night-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.625rem 0.875rem;
}

.ofr-gets { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.125rem; }

.ofr-get {
  display: grid;
  grid-template-columns: 0.4rem minmax(0, 1fr) 1.125rem;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
}
.ofr-get + .ofr-get { border-top: 1px solid var(--line-soft); }

/* The four lines carry their own colour, as they do on the card above and on
   the photograph above that. The three rows that are not a line take the
   accent rather than a fifth colour nothing else uses. */
/* `:not(.has-art)` ON ALL FIVE, and it is load-bearing rather than tidy.
 *
   `.ofr-get.is-heart .ofr-get-dot` is three classes and `.ofr-get-dot.has-art`
   is two, so the per-line colour beat the rule that was supposed to clear the
   background once a badge arrived -- and the heart badge came out sitting on a
   28px pink disc, which is exactly the background the disc had just been cut
   out of the file to remove.

   This codebase has the same scar once already: `.my-row.is-waiting .my-art`
   out-specified `.my-art.has-art` on the shelf, three classes to two, and a
   drawing got its disc back in the waiting state only. Same shape, same fix. */
.ofr-get-dot {
  width: 0.4rem; height: 0.4rem;
  border-radius: 50%;
  background: var(--iris-soft);
}
.ofr-get.is-heart .ofr-get-dot:not(.has-art) { background: #F0648C; }
.ofr-get.is-head  .ofr-get-dot:not(.has-art) { background: #E8B65A; }
.ofr-get.is-life  .ofr-get-dot:not(.has-art) { background: #3FC2AE; }
.ofr-get.is-fate  .ofr-get-dot:not(.has-art) { background: #9B8BF4; }

.ofr-get-say { min-width: 0; display: grid; gap: 0.0625rem; }
.ofr-get-say b { font-size: 0.875rem; font-weight: 650; color: var(--star); }
.ofr-get-say span { font-size: 0.75rem; color: var(--star-dim); }

.ofr-get-tick { width: 1.125rem; height: 1.125rem; color: var(--good); }
.ofr-get-tick svg { display: block; width: 100%; height: 100%; }

/* Four links, all of which resolve. Two of them were 404s until the day this
   block was written, one from inside the consent checkbox that gates the pay
   button. */
.ofr-legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.375rem 1rem;
  margin: 1.5rem 0 0;
  font-size: 0.75rem;
}
.ofr-legal a { color: var(--star-faint); text-underline-offset: 2px; }

/* ---- the plan rows -------------------------------------------------------
   A mark, a name, a price. Appended at the END of this file on purpose: the
   older `.plan` rules exist TWICE further up (the file's own note at the second
   copy says so), and a third edit in the middle of that would land in whichever
   copy the editor happened to open. Last wins, and last is here. */

.plans { display: grid; gap: 0.625rem; }

.plan {
  position: relative;
  display: grid;
  grid-template-columns: 1.375rem minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  appearance: none;
  font: inherit;
  text-align: left;
  color: var(--star);
  background: var(--night-2);
  border: 1.5px solid var(--line);
  border-radius: var(--radius);
  padding: 0.875rem 1rem;
  cursor: pointer;
  transition: border-color 0.18s var(--ease), background 0.18s var(--ease);
}
.plan:hover { background: var(--night-3); }
.plan.is-chosen { border-color: var(--iris); background: var(--iris-glow); }

/* The mark. Empty ring when it is not the choice, filled disc with a tick when
   it is -- the two states differ in fill as well as in colour, so the control
   still reads at a glance with the colour taken away. */
.plan-mark {
  width: 1.375rem; height: 1.375rem;
  border-radius: 50%;
  border: 1.5px solid var(--star-faint);
  color: var(--star);
  display: grid;
  place-items: center;
}
.plan-mark svg { display: block; width: 0.875rem; height: 0.875rem; }
.plan.is-chosen .plan-mark { border-color: var(--iris); background: var(--iris); }

.plan-term { min-width: 0; font-size: 0.9375rem; font-weight: 650; }

/* Both figures on one line now that the row is one line. They were stacked
   because a `then` line underneath made the right column two rows deep; with
   that gone, stacking them left the card taller than its content. */
.plan-cost { display: flex; align-items: baseline; gap: 0.4375rem; }
.plan-was { font-size: 0.8125rem; color: var(--star-faint); }
.plan-now { font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; }

/* The badge rides the top edge of the card it belongs to. Periwinkle on the
   recommended plan so it reads as this screen's own accent rather than as the
   gold reserved for locked and premium marks. */
.plan-badge {
  position: absolute;
  top: -0.5rem;
  right: 0.875rem;
  font-family: var(--f-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #1A1204;
  background: var(--gilt);
  border-radius: 1rem;
  padding: 0.1875rem 0.5rem;
}
.plan.is-chosen .plan-badge { color: #fff; background: var(--iris); }

@media (max-width: 22rem) {
  .plan { gap: 0.5rem; padding: 0.75rem 0.75rem; }
  .plan-now { font-size: 1.125rem; }
}

/* ---- the rating ----------------------------------------------------------
   Laid out the way Trustpilot present a score: the figure in a block of its
   own, the wordmark and the stars beside it. Drawn locally because their own
   TrustBox is a script from their CDN and this site's CSP is `script-src
   'self'` -- see the note on TRUSTPILOT in assets/flags.js. */

/* No card of its own any more: it is inside the list's card, separated from
   the rows by a hairline rather than by a second border. */
.ofr-tp {
  margin: 0 0 0.375rem;
  border-bottom: 1px solid var(--line-soft);
}
.ofr-tp-in {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.875rem;
  padding: 0.375rem 0 0.75rem;
  color: inherit;
  text-decoration: none;
}

/* The figure gets a panel rather than a colour: it is the number being quoted
   and it should read as quoted rather than as this page's own voice.

   The panel LIFTS off the card instead of cutting into it. It was var(--night),
   the darkest value in the file, against a card two steps lighter -- which on a
   dark page does not read as a panel, it reads as a hole. A few per cent of
   white sits the figure slightly above its card, which is what a quoted number
   should do, and it keeps working if the card's own ground ever changes. */
.ofr-tp-score {
  flex: none;
  padding: 0.375rem 0.625rem;
  border-radius: 0.5rem;
  background: rgba(255, 255, 255, 0.07);
  font-family: var(--f-display);
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.1;
  color: var(--star);
  font-variant-numeric: tabular-nums;
}

.ofr-tp-say { min-width: 0; display: grid; gap: 0.3125rem; }
.ofr-tp-say b { font-size: 0.875rem; font-weight: 700; color: var(--star); }

.ofr-tp-stars { display: flex; gap: 0.1875rem; }
.ofr-tp-stars svg { display: block; width: 1.125rem; height: 1.125rem; }

.ofr-tp-count { font-size: 0.6875rem; color: var(--star-faint); }

a.ofr-tp-in:hover .ofr-tp-say b { text-decoration: underline; text-underline-offset: 2px; }

/* The row mark. A 6.4px dot until a badge has been drawn for that row, and the
   badge in the same cell once there is one -- so the column does not move as
   the set is filled in one icon at a time. The dot is hidden rather than
   removed, because the image takes itself away on error and the dot has to be
   underneath when it does. */
/* NOTHING BEHIND THE BADGE AT ALL, not even the dot it replaces.
 *
   There was a small coloured dot under the art as a fallback for an image that
   fails to load. It cannot stay: the badge has transparent corners now, so a
   dot behind it shows THROUGH the gaps and reads as a mark the drawing does not
   have. The fallback is handled where it belongs instead -- the error handler
   in offer.js takes `has-art` off the box, and the plain dot rule above takes
   over the moment it does. */
.ofr-get-dot.has-art {
  width: 1.75rem; height: 1.75rem;
  background: none;
  border-radius: 0;
}
/* No border-radius. It was there to hold a lavender disc to its edge; the
   badge ground is cut away now, so the file arrives with transparent corners
   and rounding it would clip the drawing instead. */
.ofr-get-dot img {
  position: relative;
  display: block;
  width: 100%; height: 100%;
}

/* THE WHOLE COLUMN WIDENS, NOT THE ROW THAT HAS THE BADGE.
 *
   Scoped to the list and not to the row on purpose. Written per row, the one
   row with a badge got a 28px column and the six with dots kept a 6.4px one,
   so seven titles started at two different left edges -- and would go on doing
   that for as long as the set is half drawn, which is most of the time a set
   like this exists. One badge anywhere in the list sets the column for all of
   them, and the dots sit centred in it until their own badge arrives. */
.ofr-gets:has(.has-art) .ofr-get {
  grid-template-columns: 1.75rem minmax(0, 1fr) 1.125rem;
}
.ofr-gets:has(.has-art) .ofr-get-dot:not(.has-art) {
  justify-self: center;
}

/* ---- the consent paragraph and the checkout line -------------------------
   The paragraph carries the whole agreement and is rewritten on every plan
   change, so it is denser than the two short lines it replaced. Links inside it
   have to stay legible against a block that is deliberately quiet. */

.terms-line a { color: var(--star); text-underline-offset: 2px; }
.terms-line + .terms-line { margin-top: 0.5rem; }

/* Rendered only when CHECKOUT.methods in flags.js is not empty -- see the note
   above safeCheckout() in offer.js for why it ships empty. */
.ofr-safe { margin: 0.875rem 0 0; text-align: center; }

.ofr-safe-say {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 650;
  color: var(--star);
}
.ofr-safe-ic { width: 1.0625rem; height: 1.0625rem; color: var(--good); }
.ofr-safe-ic svg { display: block; width: 100%; height: 100%; }

/* The marks sit on their own light plate. Card artwork is drawn for white and
   most of it carries its own white keyline; dropped straight onto #232C42 the
   dark halves of those marks disappear. */
.ofr-pay {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.375rem;
  margin: 0.5rem 0 0;
}
.ofr-pay img {
  display: block;
  height: 1.5rem;
  width: auto;
  padding: 0.1875rem 0.3125rem;
  border-radius: 0.25rem;
  background: #FFFFFF;
}

/* ---- the consent paragraph, unboxed --------------------------------------
   Appended at the END of this file: `.terms` is defined twice further up (the
   file's own note at the second copy says so), so an override in the middle
   lands in whichever copy the editor happened to open. Last wins, and last is
   here.

   The card came off and then went back on, minus what made it shout. It keeps
   the ground and the hairline every other block on this screen has, and it has
   lost the gilt bar down its left edge -- that colour is reserved in this file
   for locked and premium marks, and a bar of it beside the price read as an
   ornament on the one paragraph that should not have one.

   ONE COLOUR AND ONE WEIGHT, both at the owner's direction. The renewal amount
   and its date were bold; they are not any more.

   That is worth a note rather than a silent change. On a subscription screen
   that sentence -- what is charged, how often, from when -- is the one a buyer
   is most likely to be surprised by later, and it is now set exactly like the
   sentence about which documents were read. Nothing is hidden: it sits above
   the button, in the same paragraph, in full, and paintTerms() rewrites it for
   whichever plan is chosen. What it no longer does is stand out. */
.terms {
  background: var(--night-2);
  border: 1px solid var(--line);
  border-radius: 0.75rem;
  padding: 0.875rem 1rem;
  /* A top margin, because it had none and the two cards were touching. The
     blocks above carry `margin-top` and no `margin-bottom` (see .ofr-block), so
     spacing on this screen is owned by whatever comes NEXT -- which means a new
     block arrives flush against the one above it until it says otherwise.
     1.25rem matches the gap the `.ofr-block` headings sit on. */
  margin: 1.25rem 0 1rem;
  text-align: left;
  color: var(--star-faint);
}

.terms-line {
  margin: 0;
  font-size: 0.8125rem;
  line-height: 1.55;
  color: inherit;
}
.terms-line + .terms-line { margin-top: 0.625rem; }

/* Not bold. `strong` stays in the markup because the sentence it wraps is the
   substantive one and that is what the element means; only its styling is
   dropped, so re-emphasising it later is one line rather than a hunt through
   offer.js. */
.terms-line strong { font-weight: inherit; color: inherit; }

/* Links carry no colour of their own either -- the underline is what marks
   them, which is enough on a paragraph this quiet and keeps the block to the
   single colour it was asked for. */
.terms-line a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The second line was the quieter of two; with the whole block at one colour it
   has nothing left to be quieter than. */
.terms-line.quiet { color: inherit; }

/* ---- the pinned button and the checkout panel ----------------------------
   Appended at the END of this file: `.offer-cta` and `.consent` are defined
   twice further up (the file says so at the second copy), so an override in the
   middle lands in whichever copy the editor happened to open. */

/* The consent row is gone from the markup; the rule would now match nothing. */
.consent { display: none; }

/* PINNED TO THE FLOOR. Fixed rather than sticky: sticky pins only once its own
   place in the flow has been scrolled past, so the button would have been
   invisible while the reader was at the top looking at the plans -- which is
   most of the time. The scrim under it keeps the page from running into the
   button's own edge as it passes behind. */
.offer-cta {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 40;
  margin: 0;
  padding: 1.25rem var(--pad) calc(0.875rem + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(to top, var(--night) 62%, transparent);
  pointer-events: none;
}
.offer-cta .cta {
  max-width: var(--shell);
  margin: 0 auto;
  pointer-events: auto;
}

/* Away while the inline button is on screen: one visible Start trial at a time.
 *
   Opacity and pointer-events, and deliberately NOT visibility. The first
   version transitioned visibility with a delay so the bar would stay clickable
   through its own fade, and that delay was one of two things making the reveal
   feel broken -- the other was the observer, now gone. Two properties, no
   delays, nothing to get out of step. */
.offer-cta {
  transition: opacity 0.22s var(--ease), transform 0.22s var(--ease);
}
.offer-cta.is-away {
  opacity: 0;
  transform: translateY(0.75rem);
}
/* The wrapper already ignores pointer events; this is the button inside it,
   which must not be pressable while the bar is faded out. */
.offer-cta.is-away .cta { pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .offer-cta { transition: none; }
  .offer-cta.is-away { transform: none; }
}

/* The offer has to end above its own button, or the last thing on the page is
   permanently behind it. A spacer inside the offer rather than padding on the
   mount: the padding used to sit on `.step.offer`, a class the funnel puts on
   its wrapper, so the gap existed in the funnel and nowhere else -- and the
   legal links ended up under the button on every other mount. */
.offer-floor { height: 6.5rem; }

/* The button that sits with the prices. The pinned copy below is a second
   chance at it, not the first. */
.offer-cta-inline { margin: 1rem 0 0; }


/* ---- the offer's button: a sweep and a pulse ------------------------------
   Scoped to the two Start trial buttons and nowhere else. `.cta` is every
   Continue in the funnel, and a wall of pulsing Continues would turn the one
   button that is meant to be pressed into the same object as the twenty that
   merely move the reader along.

   THE SWEEP is a background-position animation, not a moving element: the
   gradient is drawn at twice the button's width and slid across it, so nothing
   is added to the DOM, nothing overflows, and the corners stay rounded without
   a second clipping layer. It runs deep -> soft -> deep, so the light passes
   through and the button never lands on a colour that is not one of its own.

   THE PULSE is on the shadow, not on the size. A button that grows and shrinks
   moves the text under the reader's thumb and nudges the layout around it;
   a ring that breathes says the same thing and moves nothing. */
@keyframes cta-sweep {
  from { background-position: 0% 50%; }
  to   { background-position: -200% 50%; }
}

@keyframes cta-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(93, 108, 232, 0.45); }
  70%      { box-shadow: 0 0 0 0.75rem rgba(93, 108, 232, 0); }
}

.offer-cta .cta,
.offer-cta-inline .cta {
  background-image: linear-gradient(100deg,
    var(--iris-deep) 0%,
    var(--iris-soft) 25%,
    #A9B2F6 38%,
    var(--iris-soft) 50%,
    var(--iris-deep) 75%,
    var(--iris-deep) 100%);
  background-size: 200% 100%;
  animation: cta-sweep 2.8s linear infinite, cta-pulse 2.8s var(--ease) infinite;
}

/* Hover already brightens the whole button; letting the sweep run under it as
   well made the highlight read as a flicker rather than a pass. */
.offer-cta .cta:hover,
.offer-cta-inline .cta:hover { animation-play-state: paused; }

/* Both off. A pulse is decoration and a sweep is motion across a large area --
   between them they are the two things this query exists for. */
@media (prefers-reduced-motion: reduce) {
  .offer-cta .cta,
  .offer-cta-inline .cta {
    animation: none;
    background-image: linear-gradient(180deg, var(--iris-soft), var(--iris-deep));
    background-size: auto;
  }
}

/* ---- the checkout sheet --------------------------------------------------- */

/* The page must not scroll behind the sheet. Set on <body> by offer.js and
   taken off by every one of the sheet's three exits, because a scroll lock left
   behind takes the whole site with it. */
body.is-locked { overflow: hidden; }

.pay-veil {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 1.25rem 1rem;
  overflow-y: auto;
  background: rgba(11, 16, 32, 0.72);
  animation: fade-in 0.18s var(--ease) both;
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

.pay-sheet {
  position: relative;
  width: 100%;
  max-width: 27rem;
  margin: auto;
  padding: 1.5rem 1.25rem 1.25rem;
  text-align: left;
  background: var(--night-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  animation: rise 0.28s var(--ease) both;
}

.pay-close {
  position: absolute;
  top: 0.625rem; right: 0.625rem;
  width: 2.25rem; height: 2.25rem;
  display: grid;
  place-items: center;
  appearance: none;
  border: 0;
  border-radius: 50%;
  color: var(--star-dim);
  background: none;
  cursor: pointer;
}
.pay-close:hover { color: var(--star); background: rgba(255, 255, 255, 0.06); }
.pay-close svg { width: 1.125rem; height: 1.125rem; display: block; }

.pay-title {
  margin: 0 2rem 1.125rem 0;
  font-family: var(--f-display);
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.25;
  color: var(--star);
}

.pay-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 0.5rem;
}
.pay-total span { font-size: 1rem; font-weight: 650; color: var(--star); }
.pay-total b {
  font-size: 1.375rem;
  font-weight: 700;
  color: var(--star);
  font-variant-numeric: tabular-nums;
}

/* The tax split opens rather than shouting: one tap for the reader who wants to
   know why the total is not the price on the card, nothing in the way of the
   one who does not. */
.pay-split-wrap { border-bottom: 1px solid var(--line-soft); padding-bottom: 0.75rem; }
.pay-split-btn {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-size: 0.75rem;
  color: var(--star-faint);
  cursor: pointer;
  display: block;
  margin-left: auto;
}
.pay-split-btn::after { content: " \25BC"; font-size: 0.625rem; }
.pay-split-btn.is-open::after { content: " \25B2"; }

/* `display: grid` beats the user agent's `[hidden] { display: none }`, so the
   split opened itself the moment it was styled. Any rule that sets `display` on
   an element toggled with the hidden attribute has to say this. */
.pay-split { margin-top: 0.5rem; display: grid; gap: 0.25rem; }
.pay-split[hidden] { display: none; }
.pay-split-row {
  display: flex;
  justify-content: space-between;
  font-size: 0.8125rem;
  color: var(--star-dim);
}
.pay-split-row b { color: var(--star); font-variant-numeric: tabular-nums; }

.pay-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--line-soft);
}
.pay-row-key { font-size: 0.875rem; font-weight: 650; color: var(--star); }
.pay-row-val {
  min-width: 0;
  font-size: 0.8125rem;
  color: var(--star-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The card block gets the accent border the chosen plan gets: on this sheet it
   is the one thing being filled in. */
.pay-card {
  margin: 1rem 0 0;
  padding: 0.875rem 1rem 1rem;
  border: 1.5px solid var(--iris);
  border-radius: var(--radius);
}
.pay-card-head {
  font-size: 0.9375rem;
  font-weight: 650;
  color: var(--star);
  margin-bottom: 0.75rem;
}

/* Stripe mounts its iframes in here. It must have a height of its own before
   they arrive, or the sheet jumps the moment they do. */
.pay-fields { min-height: 3.5rem; }
.pay-fields-wait {
  margin: 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--star-faint);
}

.pay-submit { margin-top: 0.875rem; }

.pay-fine {
  margin: 0.875rem 0 0;
  font-size: 0.6875rem;
  line-height: 1.5;
  color: var(--star-faint);
}
.pay-fine a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .pay-veil, .pay-sheet { animation: none; }
}

/* ---- the wallet row ------------------------------------------------------
   Hidden until Stripe's Express Checkout Element reports that a wallet is
   actually available on this device -- see mountCardFields() in offer.js. Both
   this and the rule under it stay out of the way entirely when there is none,
   so a desktop with no wallet configured sees a card form and no empty space
   where one was not. */
.pay-wallets { margin-bottom: 0.875rem; }
.pay-wallets[hidden] { display: none; }

/* The rule between the wallets and the card. A line with a word in it rather
   than a heading, because the two halves are alternatives and neither is the
   fallback for the other. */
.pay-or {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0 0 0.875rem;
  font-size: 0.75rem;
  color: var(--star-faint);
}
.pay-or[hidden] { display: none; }
.pay-or::before,
.pay-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}
