/* The sign-up page: a single-purpose screen shared as a bare link, so it
   carries only what it needs and inherits the site's type and colour. */
.join-page { display: block; }

/* ===== THIS PAGE SCROLLS. THE LANDING PAGE DOES NOT. =====
   styles.css locks html and body to height:100% with overflow:hidden, because
   the landing page is designed to fit one viewport exactly. That lock followed
   this page in, and it is wrong here: measured at a 921px window the form ran
   to 1017px, so the closing paragraph sat 96px below the fold and could not be
   reached — not clipped by a container, but unscrollable, which is why it read
   as "cut off at the bottom" rather than as a page that needed scrolling.

   A form has no fixed height. It grows with a validation message, with a
   longer error, with a larger default font size, and on any window shorter
   than a desktop one. Trimming content until it fits at 921px would only move
   the same failure to the next laptop, so the page is made to scroll instead.

   No class needed: join.css is loaded by join.html alone, so overriding the
   bare selectors here cannot reach any other page. */
html,
body.join-page {
  height: auto;
  min-height: 100%;
  overflow: visible;
}

.join-wrap {
  max-width: 560px;
  margin: 0 auto;
  /* Top gap read from --brand-top, so this page's brand block starts the same
     distance below the nav as the home page's does. See styles.css. */
  padding: var(--brand-top) 24px 80px;
}

/*
 * ===== THE PAGES THAT LEAD WITH A HEADING, NOT A LOGO =====
 *
 * privacy.html and terms.html open with "What is actually stored" and similar —
 * no logo above it. --brand-top is 6px because that is what the home page's
 * LOGO needs, and 6px under a nav bar is far too tight for a line of type.
 *
 * 18px restores exactly the 24px those pages had before (6 + 18), so tightening
 * the brand block did not quietly cramp two pages that have no brand block.
 *
 * The blog's own heading is handled by the more specific rule further down —
 * there the wordmark sits above it and 6px is right.
 */
.join-wrap > .join-title:first-child {
  margin-top: 18px;
}

/*
 * ===== THE GAP UNDER THE LOGO WAS THREE GAPS, NOT ONE =====
 *
 * Reported by the owner on 17 September 2026: the wordmark sat too far below
 * the mark. Nothing here said "leave 40px" — it accumulated:
 *
 *   10px   margin-bottom on this wrapper
 *   ~20px  the <h1>'s OWN default top margin (0.67em at up to 32px)
 *   plus   transparent padding baked into the GIF itself
 *
 * The browser default is the part that is easy to miss, because it appears in
 * no stylesheet. Setting margin-bottom to 0 here alone would have looked like
 * the rule had no effect and invited somebody to try a negative number.
 */
.join-logo-wrap {
  display: flex;
  justify-content: center;
  margin-bottom: 0;
}

/*
 * ===== THE GAP IS INSIDE THE GIF, AND THAT IS WHY 4px DID NOT FIX IT =====
 *
 * First attempt set this to 4px and the owner reported the gap still there. He
 * was right, and no margin was going to close it, because the space is not
 * margin at all.
 *
 * MEASURED FROM THE FILE'S OWN ALPHA CHANNEL rather than guessed:
 *
 *     logo-shimmer-transparent.gif   480 x 480
 *     visible artwork                y 96 -> 383
 *     transparent above              96px  (20.0%)
 *     transparent below              97px  (20.2%)
 *
 * The image is rendered 150px tall, so 20.2% of that — about 30px — is empty
 * pixels between the bottom of the diamond and the bottom of the element. The
 * box model said the heading sat 4px below the image. The reader saw 34.
 *
 * ===== SO THE HEADING IS PULLED UP BY WHAT THE FILE WASTES =====
 *
 * -26px = the 30px of transparent padding, less the 4px that should actually
 * separate them.
 *
 * ============================================================================
 * AND THEN THE OWNER ASKED FOR THE OPPOSITE, WHICH IS WHY IT IS NOW SHARED
 * ============================================================================
 *
 * That -26px closed the gap on THIS page and nowhere else, so the home page sat
 * at 24px and this one at 4px. On 26 September the owner asked for every page
 * carrying the mark to match, with the home page as the reference — which
 * reverses the tightening above rather than refining it.
 *
 * Both requests were reasonable and they cannot both hold, so the earlier one is
 * recorded here as superseded rather than quietly deleted: the next person to
 * read "close the gap" should be able to see that it was closed, and then
 * deliberately reopened.
 *
 * The number now lives in --brand-pull in styles.css, which the hero reads too.
 * One value, four pages, nothing to keep in step by hand.
 *
 * THE BETTER FIX IS STILL TO CROP THE GIF. Then every use of the mark — this
 * page, the hero, the blog, the chat avatar — stops carrying 40% dead height,
 * and rules like this one disappear entirely. Not done here because
 * re-exporting an animated GIF is a change to the owner's artwork, and this is
 * a layout task. Recorded so the next person knows the negative margin is a
 * symptom, not a design.
 *
 * ===== SCOPED TO THE ONE CASE =====
 *
 * The adjacent-sibling selector matters. `.join-title` is shared with
 * privacy.html, where the heading is "What is actually stored" and has no logo
 * above it — pulling the class up by 26px would have slid that heading under
 * the nav bar on a page nobody was looking at.
 */
.join-logo-wrap + .join-title {
  margin-top: var(--brand-pull);
}

/*
 * The generic page heading — used by privacy.html for "What is actually
 * stored" as well as here.
 *
 * DELIBERATELY NOT GIVEN THE BRAND TREATMENT. Adding uppercase and wide
 * letter-spacing to this class would have applied them to that sentence too,
 * turning a plain heading into "WHAT IS ACTUALLY STORED". The brand styling
 * lives on .join-brand below, which only the wordmark carries.
 */
.join-title {
  font-size: clamp(22px, 3.4vw, 30px);
  text-align: center;
  margin-bottom: 28px;
  letter-spacing: 0.5px;
}

.join-tabs {
  display: flex;
  gap: 8px;
  margin-bottom: 26px;
  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}

.join-tab {
  font-family: inherit;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.4px;
  color: inherit;
  opacity: 0.55;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  padding: 10px 14px;
  cursor: pointer;
}

.join-tab.is-active {
  opacity: 1;
  border-bottom-color: #9a8032;
}

.join-panel { display: none; }
.join-panel.is-active { display: block; }

.join-lede {
  font-size: 14.5px;
  line-height: 1.65;
  opacity: 0.8;
  margin-bottom: 24px;
}

.join-field { display: block; margin-bottom: 20px; }

.join-label {
  display: block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  margin-bottom: 8px;
}

/* ===== ONE CONTROL, NOT TWO =====
   The field and its button share a single border so they read as one object
   with one next move. The border lives on the ROW, not on the input, and the
   input's own border is removed — two nested borders produce a hairline seam
   that looks like a rendering fault at exactly the size nobody can quite
   place. */
.join-field-row {
  display: flex;
  align-items: stretch;
  border: 1px solid rgba(0, 0, 0, 0.18);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.9);
  overflow: hidden; /* keeps the button's square corners inside the radius */
}

/* The focus ring has to move to the row too, or focusing the input lights up a
   border that is no longer there and nothing appears to happen. */
.join-field-row:focus-within { border-color: currentColor; }

.join-field input {
  /* flex-basis 0 with min-width 0: without the min-width an input refuses to
     shrink below its default size and pushes the button off the right edge on
     a narrow phone — the one place this layout is most likely to be seen. */
  flex: 1 1 0;
  min-width: 0;
  font-family: inherit;
  font-size: 15px;
  padding: 12px 14px;
  border: none;
  background: transparent;
  color: inherit;
}

.join-field input:focus { outline: none; }

/*
 * The consent block is deliberately as prominent as the form itself, and the
 * box is never pre-ticked. Someone should be able to read what they are
 * agreeing to without opening anything, which is why the substance is here
 * rather than only behind the link.
 */
.join-consent {
  display: flex;
  gap: 11px;
  align-items: flex-start;
  font-size: 12.5px;
  line-height: 1.6;
  opacity: 0.85;
  padding: 14px 16px;
  border-left: 3px solid #c9a54a;
  background: rgba(201, 165, 74, 0.09);
  border-radius: 0 6px 6px 0;
  margin-bottom: 22px;
  cursor: pointer;
}

.join-consent input {
  flex: 0 0 auto;
  margin-top: 2px;
  width: 16px;
  height: 16px;
  cursor: pointer;
}

.join-consent a { text-decoration: underline; }

.join-submit {
  font-family: inherit;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.5px;
  padding: 13px 28px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  background: #9a8032;
  color: #fff;
}

.join-submit:disabled { opacity: 0.6; cursor: default; }

/* ===== The button, inside the field =====
   Square-cornered because the row's own radius and overflow:hidden do the
   rounding. Giving it a radius of its own would round the outer corners twice
   and leave the inner two rounded against nothing. */
.join-submit.is-inline {
  border-radius: 0;
  /* Does not shrink and does not stretch: the label is two words and a button
     that resizes with the viewport makes the field jump about as the page is
     resized. */
  flex: 0 0 auto;
  padding: 12px 24px;
  white-space: nowrap;
}

/* Under about 380px a 24px-padded button and a usable field cannot both fit, so
   the row stacks rather than squeezing the address box to nothing. Tested at
   the narrow end because that is where a joined control fails first. */
@media (max-width: 380px) {
  .join-field-row { flex-direction: column; align-items: stretch; }
  .join-submit.is-inline { padding: 12px 24px; }
}

.join-status {
  font-size: 13.5px;
  line-height: 1.6;
  margin-top: 14px;
}

.join-status.is-error { color: #a8443c; }
.join-status.is-done { color: #2e7d4f; }
.join-status a { text-decoration: underline; }

.join-hint {
  display: block;
  font-size: 12px;
  line-height: 1.55;
  opacity: 0.65;
  margin-top: 7px;
}

/*
 * The two offers sit side by side, because they are a comparison rather than a
 * sequence. Stacked, the paid one read as an afterthought below the free one;
 * beside it, the trade is visible at a glance — smaller and free with a
 * deadline, or larger and paid with none.
 */
/* With the free-trial card gone there is one offer, and a lone card stranded
   in the left half of a two-column grid reads as a missing second card rather
   than as a single deliberate one.
 *
 * ===== FULL WIDTH, NOT 420px =====
 *
 * This read `minmax(0, 420px)` with the column centred, which left the card
 * 92px narrower than the email field directly beneath it and centred inside
 * the gap. Two centred boxes of different widths on one short page do not read
 * as a hierarchy; they read as a mistake, because nothing explains why the
 * price is inset and the field is not.
 *
 * 1fr means the card is exactly as wide as .join-wrap's content box — which is
 * what sets the field's width too. They are now the same width BECAUSE they
 * share a cause, not because two numbers were matched by hand and will drift
 * apart the first time the wrap changes. */
/*
 * ===== SIDE BY SIDE, PLANNER ON THE LEFT =====
 *
 * The owner's instruction, 17 September 2026. They were stacked for one
 * afternoon and the reasoning then was real: .join-wrap is 560px, so two
 * columns are about 265px each, and the credits card carries a countdown —
 * four two-digit units with separators and labels — which overflowed that
 * width.
 *
 * SO THE ROW WIDENS, RATHER THAN THE CARDS SHRINKING. 860px gives each column
 * about 415px, comfortably more than the clock needs. .join-wrap itself stays
 * 560px, so the email field, the tabs and the notice keep the proportions of a
 * sign-up form — an 860px-wide email field would look like a mistake.
 *
 * ===== EQUAL HEIGHT, AND THE FOOTER LINES PINNED TO THE BOTTOM =====
 *
 * This said `align-items: start`, and the reasoning was that these cards are
 * not peers: a credits pack with a live discount AND A COUNTDOWN beside an
 * US$18 planner with one price. Measured then, they were 455px and 309px — a
 * 146px difference. Stretched to match, the shorter one grew a void that read
 * as a missing feature, so each was left to take its own height.
 *
 * THE COUNTDOWN WAS REMOVED ON THE OWNER'S INSTRUCTION LATER THAT DAY, and
 * that changed the arithmetic: 364 against 309, a 55px difference. Ragged
 * bottoms on two cards that are nearly the same height read as a mistake,
 * where a 146px difference read as two different things. So he asked for them
 * aligned, and he is right — the premise the old rule was solved against no
 * longer exists.
 *
 * ===== STRETCHING ALONE WOULD HAVE LOOKED WORSE, NOT BETTER =====
 *
 * Equal height on its own leaves the shorter card's closing rule floating 55px
 * above the other's, with blank space beneath it. The fix is both halves: the
 * card body fills the height, and the closing note is pushed to the bottom
 * with `margin-top: auto`. Then the two footer rules line up and the extra
 * space falls where a card naturally has air — between the claim and the
 * footer — instead of below the last line.
 *
 * `align-items` is simply omitted rather than set to `stretch`: stretch is the
 * default, and writing it would imply a choice was needed.
 *
 * ===== THE ORDER IS IN THE MARKUP, NOT IN `order:` =====
 *
 * The planner is first in join.html. A CSS `order` property would have moved
 * the picture and left the tab order crossing over itself — somebody tabbing
 * would go right, then left. DOM order and visual order agree instead.
 *
 * ===== AND THEY STILL STACK ON A PHONE =====
 *
 * Below 720px there is no room for two, and the countdown is the first thing
 * to break. The breakpoint is 720 rather than the 560 used elsewhere on this
 * page so the switch happens BEFORE the clock is squeezed rather than after.
 */
.join-offers.is-pair {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 16px;

  /* Breaks out of .join-wrap's 560px without a second wrapper. Capped against
     the viewport so it can never cause a horizontal scrollbar. */
  width: min(860px, calc(100vw - 32px));
  margin-left: 50%;
  transform: translateX(-50%);
}

@media (max-width: 720px) {
  .join-offers.is-pair {
    grid-template-columns: minmax(0, 1fr);
    width: auto;
    margin-left: 0;
    transform: none;
  }
}

.join-offers {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
  margin-top: 28px;
}

/* ===== THE PRICE MOVED TO THE TOP =====
   It was the last thing in the panel, so it carried a 28px gap ABOVE it to
   separate it from the form. Sitting first, that margin now pushes the whole
   page down for nothing and the gap is needed on the other side instead.
   Scoped to :first-child so the rule follows the card if it ever moves back,
   rather than having to be remembered and undone. */
.join-panel > .join-offers:first-child {
  margin-top: 0;
  margin-bottom: 22px;
}

@media (max-width: 560px) {
  .join-offers { grid-template-columns: 1fr; }
}

/* What the trial is, and what it costs after — stated on the page where
   someone decides, rather than discovered later. */
.join-offer {
  display: flex;
  flex-direction: column;
  padding: 16px 18px;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.55);
}

.join-offer.is-buy { background: rgba(201, 165, 74, 0.08); }

/* ===== The pack, with its radio =====
 *
 * ===== THE RADIO IS OUT OF FLOW, AND THAT IS THE WHOLE FIX =====
 *
 * This was a flex row: radio, then body. The body was therefore 29px narrower
 * than the card and started 29px in from its left edge — so everything the
 * price block centres (kicker, figures, sentence, clock) was centred on the
 * BODY's axis, sitting 15px right of the card's own centre. Small enough that
 * nobody can name it, large enough that the card reads as slightly askew.
 *
 * Taking the radio out of flow puts the body back on the card's true axis, so
 * the centred stack is centred against the border the reader can actually see.
 *
 * THE PADDING IS SYMMETRIC AND WIDER THAN THE RADIO. 36px each side against a
 * radio that ends at 29px: the text can never run under the control, at any
 * width, without anyone having to solve an offset against it. That is the
 * point — every previous version of this file positioned things by measuring a
 * neighbour, and every one of them broke when the neighbour moved.
 *
 * STILL A REAL RADIO. Absolute positioning is paint; it is still in the tab
 * order, still announced as a selected option, still shows its own focus ring. */
.join-offer.has-radio {
  display: block;
  position: relative;
  padding: 18px 36px;
  cursor: pointer;
}

/* accent-color paints the browser's own radio in the brand gold, which keeps
   the native control — and with it the focus ring, the keyboard behaviour and
   the high-contrast rendering — rather than replacing it with a div that only
   looks like one. */
.join-offer-radio {
  position: absolute;
  top: 20px;
  left: 12px;
  width: 17px;
  height: 17px;
  margin: 0;
  accent-color: var(--gold-dark, #a9822f);
}

.join-offer-body { display: block; }
.join-offer.has-radio .join-offer-line,
.join-offer.has-radio .join-offer-note { display: block; }

/*
 * ===== THE CLOSING LINE, GIVEN A REASON TO BE WHERE IT IS =====
 *
 * "Buy when you need it; there is no subscription." was left-aligned inside a
 * card where every other line is centred, and butted straight up against the
 * countdown with no separation. It read as a stray caption that had been
 * dropped in rather than as the last line of the offer.
 *
 * It is a DIFFERENT KIND of statement from the four lines above it — those are
 * the offer and its deadline, this is a standing fact about how buying works,
 * true after the timer expires as much as before. The rule above it is what
 * says so, and it is why this line stays visible in .is-expired while the
 * promotional lines are hidden.
 */
.join-offer.has-radio .join-offer-note {
  text-align: center;
  margin-top: 16px;
  padding-top: 13px;
  border-top: 1px solid rgba(169, 130, 47, 0.22);
}

/* Selected state drawn on the card, not just in the box.
 *
 * WRITTEN FOR A DAY THAT ARRIVED. This rule was added while there was one
 * locked option, with the note that it existed "so that adding a second pack
 * later shows which one is live without further work". The planner was added
 * on 17 September 2026 and it did exactly that — no change needed here, and
 * :has(:checked) reads a checkbox as happily as a radio.
 *
 * It matters more now than it did then. With two boxes that can each be on or
 * off, the tick alone is a 17px square at the edge of the card; the border is
 * what makes "you are buying this one and not that one" legible at a glance. */
.join-offer.has-radio:has(.join-offer-radio:checked) {
  border-color: rgba(169, 130, 47, 0.55);
}

.join-offer-title {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.9px;
  text-transform: uppercase;
  opacity: 0.6;
  margin-bottom: 8px;
}

.join-offer-line { font-size: 16px; line-height: 1.4; margin-bottom: 4px; }

/* The term is the thing people actually compare, so it gets its own line
   rather than being buried in the paragraph below. */
.join-offer-terms {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.2px;
  color: #7a6220;
  margin-bottom: 8px;
}
.join-offer-note { font-size: 12.5px; line-height: 1.6; opacity: 0.7; }

.join-foot {
  font-size: 12px;
  line-height: 1.6;
  opacity: 0.6;
  margin-top: 34px;
}

/* ===== Google / LinkedIn, not yet wired ===== */
/* Sits BELOW the submit button now, so the breathing room moved to the top:
   it needs separating from the button it is an alternative to, and there is
   nothing after it to separate from. */
.join-providers {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 16px 0 0;
}

/* A rule with the word sitting on it, rather than a bare divider — "or" is
   what tells you these are alternatives to the field above, not extra steps. */
.join-or {
  position: relative;
  text-align: center;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--muted, #6b6354);
  margin: 4px 0 6px;
}

.join-or::before,
.join-or::after {
  content: "";
  position: absolute;
  top: 50%;
  width: calc(50% - 22px);
  height: 1px;
  background: rgba(169, 130, 47, 0.28);
}

.join-or::before { left: 0; }
.join-or::after { right: 0; }

.join-provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 10px 16px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--dark, #141414);
  background: #fff;
  border: 1px solid rgba(169, 130, 47, 0.35);
  border-radius: 6px;
  cursor: pointer;
}

/* Visibly not ready. Greyed and not-allowed rather than merely inert: a
   disabled control that looks live is a worse lie than no control. */
.join-provider[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
  background: #faf8f4;
}

.join-provider-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 3px;
  font-size: 12px;
  font-weight: 700;
  color: #fff;
  background: #4285f4;   /* Google blue */
}

.join-provider-mark.is-in {
  background: #0a66c2;   /* LinkedIn blue */
  font-size: 11px;
}

/* The "coming soon" note that sat here was removed at the client's request;
   its rule went with it rather than being left behind as dead weight. */

/* ===== The notice under the form =====
   Quiet, but not hidden: it is the only thing on the page describing what
   happens to what somebody writes, so it is legible rather than fine print.
   Sits under BOTH the submit button and the provider buttons, because it
   covers every route into the product. */
.join-notice {
  margin: 18px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted, #6b6354);
  text-align: center;
}

.join-notice a { color: inherit; text-decoration: underline; }

/* ===== The consent detail, stated in the open =====
   This was a <details> with a caret and a summary to click. Unfolded at the
   client's request, so the whole disclosure block is gone and the paragraph
   stands on its own.

   The 26px left indent is the one thing carried over: it lines the text up
   under the checkbox's LABEL rather than under the box itself, so the two read
   as one statement instead of as a control with an unrelated note beneath. */
.join-terms-text {
  margin: -2px 0 18px 26px;
  font-size: 12px;
  color: var(--muted, #6b6354);
  line-height: 1.55;
}

/* ===== The early-bird price =====
 *
 * A struck old price, the new one beside it, and a quiet label saying why.
 *
 * WHY THE RULE IS DRAWN RATHER THAN `text-decoration: line-through`. The
 * default rule sits on the text's midline and cuts every digit of "US$28"
 * horizontally in half, which is exactly where the numerals carry their
 * shape — an 8 becomes two stacked circles, a 2 loses its waist. The reader
 * has to work to see what they are not paying, and a discount they cannot
 * read is not a discount.
 *
 * A shallow diagonal crosses each glyph at one point instead of along its
 * length, so the figure stays legible while unmistakably cancelled.
 */
.join-was {
  position: relative;
  text-decoration: none;
  opacity: 0.55;
  margin-right: 2px;
  /* Old prices sit better a touch smaller: the eye should land on the new one. */
  font-size: 0.92em;
}

.join-was::after {
  content: "";
  position: absolute;
  left: -2px;
  right: -2px;
  /* Just above centre. Dead centre reads as a deletion; slightly high reads as
     a cancellation, which is the tone wanted. */
  top: 46%;
  height: 1px;
  background: currentColor;
  transform: rotate(-8deg);
  transform-origin: center;
  /* The stroke is decoration; the meaning is carried by the <s> element. */
  pointer-events: none;
}

.join-now { font-weight: 700; }

/* ===== "Early-bird" IS A LABEL, NOT A COUNTDOWN =====
 *
 * Deliberately no timer and no "ends soon". The home page already carries a
 * countdown; a second urgency device on the page where somebody is deciding
 * to hand over an email reads as pressure rather than information, and this
 * product's whole pitch is that it does not do that.
 */
.join-early {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  font-size: 10.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  vertical-align: 2px;
  color: var(--gold-dark, #a9822f);
  border: 1px solid currentColor;
  border-radius: 3px;
  opacity: 0.85;
}

/* The diagonal is decorative. Anyone who has asked for less motion or simpler
   rendering gets the browser's own line-through, which still communicates the
   meaning without the transform. */
@media (prefers-reduced-motion: reduce) {
  .join-was::after { display: none; }
  .join-was { text-decoration: line-through; }
}

/* ===== THE PRICE STACK =====
 *
 * Four lines, read top to bottom: the promo label frames the number before it
 * arrives, the number anchors, the sentence says what happens if you wait, and
 * the clock says how long you have.
 *
 * Centred, because this block is an announcement rather than part of the form
 * beneath it — left-aligned it reads as another field label.
 */
.join-price {
  display: block;
  text-align: center;
  padding: 4px 0 2px;
}

/* Small and muted on purpose. It sets the frame and then gets out of the way;
   competing with the figure below would flatten the hierarchy the whole block
   exists to create. */
.join-price-kicker {
  display: block;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: #8a8272;
  margin-bottom: 6px;
}

.join-price-figures {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 10px;
  margin-bottom: 6px;
}

/* The old price: large enough to be legible, light enough to lose. A struck
   figure nobody can read is not an anchor, it is decoration. */
.join-price-figures .join-was {
  font-size: clamp(20px, 3.4vw, 26px);
  font-weight: 400;
  color: #a9a294;
}

/* The live price. The one high-contrast element in the block, and the only
   place the gold is used at size. */
.join-price-figures .join-now {
  font-size: clamp(32px, 5.6vw, 44px);
  font-weight: 800;
  line-height: 1;
  color: var(--gold-dark);
  letter-spacing: -0.01em;
}

.join-price-what {
  display: block;
  font-size: 15px;
  line-height: 1.4;
  color: var(--dark);
  margin-bottom: 4px;
}

/* The mechanism, not the offer. Smaller and quieter than the outcome above it,
   because a reader who is still deciding does not care what a credit is — but a
   reader who has decided will want to know exactly what they are buying, so it
   is on the page rather than one click away. */
.join-price-unit {
  display: block;
  margin-top: 3px;
  font-size: 12.5px;
  color: #6b6354;
}

/* The differentiator, and the last thing read before the price argument starts.
   Italic to separate it from the offer mechanics above and below: it is a claim
   about the product, not a term of the sale. */
.join-price-claim {
  display: block;
  max-width: 42ch;
  margin: 0 auto 12px;
  font-size: 12.5px;
  font-style: italic;
  line-height: 1.5;
  color: #6b6354;
}

/* Crisp rather than muted: this is the sentence that makes the timer mean
   something, and a reader who skips it sees a countdown with no stake. */
.join-price-note {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: #4a453b;
  margin-bottom: 8px;
}

.join-clock { display: block; margin: 0 auto; }

/* ===== AFTER THE DEADLINE =====
 *
 * countdown.js adds .is-expired. Everything promotional goes and the plain
 * list price takes its place — the page must never keep offering a price that
 * has closed. */
.join-price-after { display: none; }

.join-price.is-expired .join-price-kicker,
.join-price.is-expired .join-price-figures,
.join-price.is-expired .join-price-note,
.join-price.is-expired .join-clock { display: none; }

.join-price.is-expired .join-price-after {
  display: block;
  font-size: clamp(15px, 2.2vw, 18px);
  line-height: 1.5;
  color: var(--dark);
}

@media (prefers-reduced-motion: reduce) {
  .join-price-figures .join-now { transition: none; }
}


/*
 * ===== WHAT MAKES EQUAL HEIGHT LOOK DELIBERATE =====
 *
 * The grid gives both cards the same height. On its own that leaves the
 * shorter card's closing rule floating above the other's with blank space
 * under it, which is the "void that reads as a missing feature" the cards were
 * previously left ragged to avoid.
 *
 * So the body fills the card and the closing note is pushed to the bottom. The
 * two footer rules then line up, and the spare height lands between the claim
 * and the footer — where a card naturally has air.
 *
 * THE NOTE IS NOT A DIRECT CHILD of .join-offer; it sits inside
 * .join-offer-body, which was `display: block`. `margin-top: auto` does
 * nothing in a block container, so the body has to become the flex column that
 * pushes it. That is why both rules are needed and why neither works alone.
 */
/*
 * ===== `flex: 1` DID NOTHING, AND min-height IS WHY =====
 *
 * First attempt gave the body `flex: 1`. The two cards came out exactly equal
 * — 348px, same top, same bottom — and the footer rules were STILL 56px apart.
 *
 * `.join-offer.has-radio` is `display: block` (it was switched from flex when
 * the radio became absolutely positioned). So the body is not a flex item and
 * `flex: 1` is inert. The body shrank to its content, `margin-top: auto` had
 * no free space to push into, and the closing note sat wherever the text ended
 * — 75px above the card's bottom on the shorter one, 19px on the taller.
 *
 * Measuring the CARDS said the job was done. Measuring the NOTES said it was
 * not. `min-height: 100%` gives the body the card's full content height, which
 * is what `margin-top: auto` needs to work against.
 */
.join-offer.has-radio .join-offer-body {
  display: flex;
  flex-direction: column;
  min-height: 100%;
}

.join-offer.has-radio .join-offer-note {
  margin-top: auto;
}

/*
 * ===== THE BLOG =====
 *
 * Added 17 September 2026. blog.html reuses this reading column, so a post
 * needs only the parts a post has that a legal page does not: a date, a
 * standfirst, and space between entries.
 *
 * The body paragraphs reuse .join-terms-text on purpose. Measure, size and
 * leading were already settled there for long reading, and a second set of
 * body-text rules is a second set to keep in step.
 */
.blog-post {
  margin-top: 40px;
  padding-top: 28px;
  /* A rule between entries, not around them: a box per post turns a reading
     page into a list of cards, which is the wrong shape for longform. */
  border-top: 1px solid rgba(255, 255, 255, 0.10);
}

/* No line above the first post — it would read as a divider from the lede. */
.blog-post:first-child {
  margin-top: 26px;
  padding-top: 0;
  border-top: 0;
}

.blog-date {
  display: block;
  margin-top: 6px;
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  opacity: 0.55;
}

.blog-dek {
  margin-top: 10px;
  font-size: 1.02rem;
  line-height: 1.55;
  opacity: 0.85;
}

.blog-where {
  margin-top: 12px;
  font-size: 0.85rem;
  opacity: 0.55;
}

/*
 * ===== THE BLOG COVER IMAGE =====
 *
 * width/height are written into the markup from the stored intrinsic size, so
 * the browser reserves the right box before the file arrives and the text below
 * does not jump. These two lines are what let that work while still allowing
 * the image to scale down: without `height: auto` the attribute height is
 * honoured literally and a wide image is squashed.
 */
.blog-cover {
  display: block;
  width: 100%;
  height: auto;
  margin: 0 0 22px;
  border-radius: 10px;
}

/* ---------------------------------------------------------------- index -- */

.blog-item.has-cover {
  display: grid;
  grid-template-columns: 120px minmax(0, 1fr);
  /* Three text rows plus a 1fr spacer: the thumbnail spans all of them, so
     its height lands in the spacer instead of stretching the title's row.
     Without this the date and dek auto-flowed into a second row BELOW the
     thumbnail, leaving a gap under the title. */
  grid-template-rows: auto auto auto 1fr;
  column-gap: 16px;
  row-gap: 0;
  align-items: start;
}
.blog-item.has-cover > .blog-item-thumb { grid-column: 1; grid-row: 1 / -1; }
.blog-item.has-cover > .blog-item-title { grid-column: 2; grid-row: 1; }
/* Dek directly under the title, date as a single line under the dek --
   placed by CSS so the generated markup order does not matter. */
.blog-item.has-cover > .blog-dek  { grid-column: 2; grid-row: 2; margin-top: 6px; }
.blog-item.has-cover > .blog-date { grid-column: 2; grid-row: 3; margin-top: 6px; white-space: nowrap; }

/* The thumbnail is a second link to the same place as the title, so it is
   hidden from assistive technology and skipped in the tab order rather than
   announced twice. Marked in the markup; this is only its size. */
.blog-item-thumb img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
}

@media (max-width: 560px) {
  .blog-item.has-cover { grid-template-columns: 84px minmax(0, 1fr); gap: 12px; }
}

/* ==========================================================================
 * A PICTURE INSIDE A POST
 * ======================================================================== */

/*
 * A figure, not a bare img: the caption belongs to the picture, and <figure>
 * plus <figcaption> is what says so to a screen reader and to a parser
 * reading the page for meaning.
 */
.blog-figure {
  margin: 26px 0;
}

.blog-figure img {
  display: block;
  width: 100%;
  /* height:auto is what lets the width/height attributes reserve the right
     box WITHOUT the attribute height being honoured literally and squashing
     a wide picture. Both are needed; neither works alone. */
  height: auto;
  border-radius: 10px;
}

.blog-figure figcaption {
  margin-top: 8px;
  font-size: 0.86rem;
  line-height: 1.45;
  opacity: 0.7;
}

/* Shown only when a token survived into the page with a path that is not
   recognised — the save route refuses those, so this should never appear.
   Visible rather than silent: a picture that vanished without trace is the
   harder thing to notice. */
.blog-image-missing {
  font-size: 0.85rem;
  opacity: 0.55;
  font-style: italic;
}

/* ==========================================================================
   THE MAILING-LIST FORM ON THE BLOG INDEX
   ========================================================================== */

.sub-form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: stretch;
  margin: 0 0 8px;
}

/* ===== THE LABEL IS PRESENT AND NOT VISIBLE =====
   A placeholder is not a label: it disappears the moment somebody types, and a
   screen reader may not announce it at all. So the label exists for assistive
   technology and is taken out of the visual layout — clipped rather than
   display:none, because display:none removes it from the accessibility tree too
   and would defeat the whole point. */
.sub-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ===== THE BORDER WAS WHITE ON A CREAM PAGE, SO IT WAS NOT THERE =====

   This was `1px solid rgba(255, 255, 255, 0.22)` — white at 22% — and
   `background: rgba(255,255,255,0.04)`. Both were written for a dark panel. The
   blog page is light, with a pale damask pattern behind it, so white-on-cream at
   22% is invisible: the field had no edge at all and the placeholder floated
   beside a gold button with nothing to say it was a box you could type in.

   Nothing failed and nothing logged. It renders perfectly — as a blank space. It
   took a screenshot to see it, which is why a colour is never checked by reading
   the value: the value is fine, the page it sits on is what decides.

   Now the gold of the button, so the field and the button read as one control,
   with a solid white fill to lift it off the pattern. */
.sub-input {
  /* Grows to fill the row, and is allowed to shrink — min-width:0 because a flex
     item's automatic minimum size would otherwise hold it at the placeholder's
     width and push the button off a narrow screen. */
  flex: 1 1 220px;
  min-width: 0;
  font-family: inherit;
  font-size: 14.5px;
  padding: 11px 13px;
  color: var(--dark, #141414);
  background: #fff;
  /* --gold-dark, not --gold: #c9a54a on white is 2.34:1, which is visible but
     under the 3:1 WCAG floor for a control boundary. #a9822f is 3.55:1 and is
     still plainly gold. Measured, not judged by eye. */
  border: 1px solid var(--gold-dark, #a9822f);
  border-radius: 5px;
}

/* Muted, not just faint. `opacity: 0.45` on a light field leaves grey-on-white
   at roughly 2:1 — legible only if you already know what it says. */
.sub-input::placeholder { color: var(--muted, #6b6354); opacity: 0.75; }

/*
 * ===== :focus, NOT :focus-visible =====
 *
 * :focus-visible is right for a button, where a mouse click should not leave a
 * ring behind. A text field is different: the person is about to type into it,
 * and they clicked it deliberately. Chrome does treat a clicked text input as
 * focus-visible, so the two behave the same here — but the rule that says what
 * is meant is the one to write.
 */
.sub-input:focus {
  outline: 2px solid var(--gold-dark, #a9822f);
  outline-offset: 1px;
  border-color: var(--gold-dark, #a9822f);
}

.sub-btn {
  flex: 0 0 auto;
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  padding: 11px 22px;
  cursor: pointer;
  color: #1a1a1a;
  background: var(--gold, #c9a54a);
  border: 1px solid var(--gold, #c9a54a);
  border-radius: 5px;
}

.sub-btn:hover { filter: brightness(1.08); }

.sub-btn:disabled {
  /* Visibly inert while a request is in flight, so a second click is obviously
     pointless rather than just ignored. */
  opacity: 0.55;
  cursor: default;
}

/* ===== THE HONEYPOT =====
   Off-screen rather than display:none or hidden. A bot reading the markup skips
   fields that are plainly hidden; one that is positioned away looks ordinary in
   the DOM and is invisible to a person. tabindex="-1" and aria-hidden in the
   markup keep keyboard and screen-reader users out of it. */
.sub-trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.sub-note {
  min-height: 1.4em; /* Reserved, so the list below does not jump when a message
                        appears. Layout shift is the thing this costs otherwise. */
  margin: 0 0 24px;
  font-size: 13px;
  line-height: 1.55;
}

.sub-note.is-ok { color: var(--gold, #c9a54a); }
.sub-note.is-err { color: #e08a8a; }

@media (max-width: 480px) {
  /* Stacked and full width. A 22px-padded button and a text field on one line at
     phone width leaves the field too narrow to read back what was typed. */
  .sub-form { flex-direction: column; }
  .sub-btn { width: 100%; }
}

/* ==========================================================================
   A POST, AS SOMETHING TO READ

   ==========================================================================
   THERE WAS NO .blog-single RULE AT ALL
   ==========================================================================

   The post page had been borrowing .join-wrap — a 560px column built for a
   sign-up form — and styles.css opens with `* { margin: 0 }`, so every
   paragraph, heading and list arrived with its spacing stripped and nothing
   put it back. Measured in Chrome before this block existed:

     text column     512px        (560 less 24px padding each side)
     body <p>        margin 0     paragraphs ran together
     standfirst      weight 400   indistinguishable from body copy
     date line       margin 0     touching the text above and below

   Nothing errored. A form stylesheet renders an article perfectly well; it
   just makes it unreadable, and only in the one place nobody had styled.
   ========================================================================== */

.join-wrap.blog-single {
  /*
   * ===== 720px, AND THE NUMBER IS A MEASURE NOT A GUESS =====
   *
   * Typographic convention puts a comfortable line at 60-75 characters; past
   * about 80 the eye loses its place returning to the next line.
   *
   * 720px MEASURED AT 79 CHARACTERS, which is the top of the range rather than
   * the middle of it — I had estimated 68 and the browser said otherwise. 680
   * gives about 74. The form's 560px gave about 52: short enough that long
   * sentences break three or four times and the page reads as a narrow ribbon.
   */
  max-width: 680px;
}

/* The reading size, set on the article rather than the page so nothing else
   shifts. 1.7 leading because long-form needs more air between lines than a
   form label does. */
.blog-single article {
  font-size: 17px;
  line-height: 1.7;
}

/*
 * ===== THE HEADLINE DOES NOT WANT THE BODY'S LEADING =====
 *
 * 1.7 is right for a paragraph and wrong for a 30px headline — it measured
 * 51px of line height, so a two-line headline floated apart into two separate
 * lines. Display type needs tighter leading the larger it gets.
 */
.blog-single article h1 {
  line-height: 1.2;
}

/*
 * ===== BODY PARAGRAPHS ONLY =====
 *
 * :not([class]) is doing real work. Every direct child of <article> is a <p> —
 * the standfirst, the date and the sign-off included — and they each want
 * different spacing. The generated body paragraphs are the only ones with no
 * class, so that is what selects them, rather than a list of exceptions that
 * goes stale the next time a part is added.
 */
.blog-single article > p:not([class]) {
  margin: 0 0 1.35em;
}

/* Subheads need far more space above than below: the gap belongs to the
   section it opens, not to the text it leaves behind. */
.blog-single article h2 {
  margin: 2em 0 0.55em;
  font-size: 1.25em;
  line-height: 1.35;
}

.blog-single article ul,
.blog-single article ol {
  margin: 0 0 1.35em;
  padding-left: 1.3em;
}

.blog-single article li { margin-bottom: 0.4em; }

/*
 * ===== THE STANDFIRST IS BOLD =====
 *
 * It is the sentence that decides whether the piece gets read, and at weight
 * 400 it was the same colour and weight as the body — so it read as the first
 * paragraph rather than as a promise about the rest.
 *
 * SCOPED TO THE POST. .blog-dek is shared with the index, where the title above
 * it is already bold; two bold lines stacked there would be muddy.
 */
.blog-single .blog-dek {
  font-size: 1.12em;
  font-weight: 600;
  line-height: 1.55;
  opacity: 1;
  margin: 0 0 18px;
}

/* The date had no air above or below and sat against the standfirst and the
   first paragraph alike. */
.blog-single .blog-meta {
  /* A top margin as well, so the date keeps its air even on a post with no
     standfirst above it to provide the gap by collapsing. */
  margin: 14px 0 32px;
  font-size: 0.82em;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  opacity: 0.55;
}

.blog-single .blog-crumb { margin: 0 0 26px; }

/*
 * The sign-off. Set apart from the last paragraph with a rule, because a name
 * on its own line directly under body copy reads as one more sentence.
 */
.blog-single .blog-signoff {
  margin: 2.2em 0 0;
  padding-top: 1.1em;
  border-top: 1px solid rgba(0, 0, 0, 0.1);
  font-style: italic;
  opacity: 0.7;
}

@media (max-width: 760px) {
  /* The column is already narrower than the window here, so the only thing to
     do is keep the side padding and let the measure fall where it falls. */
  .blog-single article { font-size: 16px; }
}

/* ==========================================================================
   LEGAL PAGES (built from website/legal/*.md)
   ==========================================================================

   These used .join-offer-note for every paragraph: 12.5px at 0.7 opacity.
   That class was built for a one-line footnote under an offer, and a whole
   terms page set in it is a wall of faint grey nobody finishes. Headings were
   spaced with inline style="margin-top:26px" repeated on each one, which the
   generator cannot write and should not have to.

   Same reading size as a blog article, because it is the same job: continuous
   prose a person reads top to bottom. Full opacity - this is the text that
   says what someone agreed to. */

.legal-page { max-width: 680px; }

.legal-body {
  font-size: 15.5px;
  line-height: 1.7;
  opacity: 1;
}

.legal-body h2 {
  margin: 2em 0 0.6em;
  font-size: 17px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--dark, #2a2a2a);
}

/* No gap above the first heading: the lede already sits above it. */
.legal-body > h2:first-child { margin-top: 0; }

.legal-body p { margin: 0 0 1.15em; }

.legal-body ul { margin: 0 0 1.15em; padding-left: 20px; }
.legal-body li { margin-bottom: 0.4em; }

/* Underlined, because an unstyled link in a wall of prose is invisible and
   these pages exist partly to send people to each other. */
.legal-body a { text-decoration: underline; }
