/* =============================================================================
   auth.css  ·  THE auth + onboarding surface                      (2026-08-07)
   =============================================================================
   ONE stylesheet for every credential and terminal-state screen on the public
   site: /signin, /onboarding/create-account, /onboarding/payment-summary, the
   eight state pages (cancelled, failed, summary-error, success, the three
   email-confirmation screens, onboarding-incomplete) and /change-password.

   REPLACES
   -----------------------------------------------------------------------
   static/css/onboarding.css   (3,198 lines, 7 consumers)
   static/css/quantum_signin.css (1,752 lines, 1 consumer)

   Both are deleted. Four documented defects died with them, and none of them
   are re-created here — each is called out at the rule that would have been
   the place to re-introduce it:

     1. `header:not(.login-header) { display: none }` — killed the SHARED site
        header on all seven onboarding pages. Nothing in this file selects a
        bare element outside the reset-free zone below; every selector here is
        class-scoped.
     2. `.login-wrapper { height: 100vh }` on a <body> that already carries
        site-chrome.css's 62px `has-site-header` pad — 100vh + 62px = a
        permanent phantom scrollbar. `.auth-shell` subtracts the header.
     3. `font-family: 'Inter' !important` on `body` AND `body *` — which is why
        the shared header rendered in Inter on these seven pages and General
        Sans on the other nine. Amended 2026-08-21: this file now contains
        exactly ONE `!important` block, the autofill override in §4, which
        cannot be written any other way because it is out-ranking the UA's own
        important declaration. Nothing else uses it. THIS FILE SETS NO
        FONT ON body.
     4. Decorative motion layers behind a credential form (.aurora-container,
        .quantum-bg, .holographic-orb, .particle-field, .cyber-lines, plus
        signin's <canvas> particle field). Deleted at the template, not hidden
        here — there is no rule in this file for any of those class names.

   LOAD ORDER
   -----------------------------------------------------------------------
     fonts.css -> site-tokens.css -> site-base.css -> site-chrome.css -> THIS
   public_base.html renders `additional_head` after `site_chrome_css`, so this
   file is last and wins on source order without needing specificity tricks.

   CONTRACT
   -----------------------------------------------------------------------
   Every colour, radius, duration, easing and type size below comes from
   site-tokens.css. THERE IS NOT ONE HEX IN THIS FILE.

   2026-08-07 · P2. The four private TONE PIGMENTS this file used to declare
   (--tone-success #34d399 / --tone-warning #fbbf24 / --tone-error #f87171 /
   --tone-neutral #8fa3c4) are GONE. They existed because the contract could
   only say "accent" and "not accent"; it can now say five things, so this
   surface reads THE SEMANTIC COLOUR SYSTEM instead of carrying a fifth private
   palette next to guides' and how-to-access's. The mapping is one-to-one and
   is the whole of the change:

       old --tone-success  ->  --sem-master   (ladder 3 · it worked)
       old --tone-warning  ->  --sem-caution  (interrupt · decide before acting)
       old --tone-error    ->  --sem-halt     (interrupt · it did not work)
       old --tone-neutral  ->  --sem-entry    (ladder 1 · arriving, waiting)

   WHY THIS SURFACE. It is the only place on the public site with real OUTCOME
   states — a payment either went through or it did not — so it is the one
   surface where the two INTERRUPT hues are earned rather than decorative.

   COLOUR IS NEVER THE ONLY SIGNAL (WCAG 1.4.1). --sem-entry (#00f2fe) measures
   1.004:1 against --ink-2 over the page black: same brightness, separated by
   hue alone. So every semantic use below ships a second, non-colour channel,
   and the channels are tokenised so a call site cannot quietly drop one:

     1. THE WORD      .state-page__status is a required element, not an option.
                      It defaults per tone and every state page overrides it
                      with its own literal outcome word ("Payment declined",
                      "Confirmed", "Payment cancelled", "Link expired"). A
                      reader who cannot see hue reads the outcome.
     2. THE GLYPH     the status chip's icon is chosen by TONE, never by the
                      page, so the interrupt glyphs stay shape-distinct
                      (triangle / x-circle) from the ladder's round ones even
                      when the page picks a topical hero glyph.
     3. EDGE WEIGHT   --sem-rule-ladder (3px) vs --sem-rule-interrupt (5px) on
                      the card's top rule, and 1px vs 2px on the chip and glyph
                      borders. A rule nearly twice as thick reads as different
                      before hue is processed at all.
     4. FILL vs RING  the onboarding ladder's completed rung is a FILLED disc
                      carrying a tick; an active rung is a RING carrying a
                      numeral. Shape and count, not hue.

   Every inline link here is still underlined; every input state still carries
   a ring + border-weight change on focus and an icon + text on error.
   ============================================================================= */

/* =============================================================================
   0 · LOCAL VOCABULARY
   -----------------------------------------------------------------------------
   There is none any more. The old block declared --auth-on-accent (a mirror of
   site-tokens' file-private --_ink-on-accent) and the four tone pigments. The
   semantic block in site-tokens.css publishes --sem-ink-on-fill for exactly the
   first job and the five roles for the second, so both are read directly now.
   The only local custom properties left in this file are the --st-* level group
   on .state-page and the --rung-* group on the onboarding ladder, and both are
   PARAMETERS (they change per tone / per step), not pigments.
   ============================================================================= */

/* =============================================================================
   1 · SHELL
   -----------------------------------------------------------------------------
   The centred stage every auth screen sits in. `min-height` SUBTRACTS the
   header pad site-chrome.css writes onto `body.has-site-header` (62px, 56px
   under 620px). The old sheet used a flat 100vh here and produced a scrollbar
   on a page with nothing to scroll — see defect 2 in the header docblock.
   `svh` with a `vh` fallback: on mobile Safari `vh` is the LARGE viewport, so
   the toolbar overlaps the card's bottom edge on first paint.
   ============================================================================= */
.auth-shell {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: calc(100vh - 62px);
  min-height: calc(100svh - 62px);
  padding: clamp(24px, 5vh, 64px) var(--space-5);

  /* 2026-08-20 · RAISED ABOVE THE ATMOSPHERE. §1c renders a `position: fixed`
     backdrop at z-index 0. A positioned z-index:0 element paints in CSS 2.1
     Appendix E step 8; in-flow non-positioned blocks paint in step 4 — and
     `.site-main` is exactly that (site-base.css:136, `display: block`, no
     positioning). Left alone the backdrop paints OVER this card. individual.css
     solves the same problem the same way at :272.

     `position: relative` + z-index creates a STACKING CONTEXT but NOT a
     containing block for `position: fixed`, so `.site-header` is untouched.
     That distinction is the whole reason this is safe here and a `transform`
     would not be. --z-content is 70, well under --z-header's 600. */
  position: relative;
  z-index: var(--z-content);
}

@media (max-width: 620px) {
  .auth-shell {
    min-height: calc(100vh - 56px);
    min-height: calc(100svh - 56px);
    padding-inline: var(--space-4);
  }
}

/* =============================================================================
   1b · SPLIT STAGE                                              (2026-08-20)
   -----------------------------------------------------------------------------
   The two FORM surfaces only: /signin and /onboarding/create-account. The other
   eleven consumers of this file stay a single centred column, deliberately — a
   state page is one decision surface, and a marketing column beside "your card
   was declined" is the same failure §7 already forbids with tinted cards.
   change_password renders for a SIGNED-IN user (acquisition copy is tone-deaf
   there) and payment_summary's right column already IS a summary.

   NO transform / filter / perspective / will-change on ANY of these wrappers.
   `.site-header` is `position: fixed` and resolves against its nearest
   transformed ancestor rather than the viewport (public_base.html:57-59), so
   anything here that establishes a containing block silently detaches the
   header from the top of the window. A grid does not; that is why this is a
   grid and not something cleverer.

   1024, NOT 900. At 900 the aside gets 900 - 48 (shell padding) - 520 (the
   --wide card) - 40 (minimum gap) = 292px, which is an offcut rather than a
   column. At 1024 it is 408px. 1023/1024 is also already this repo's
   breakpoint (individual.css:1140).
   ============================================================================= */
.auth-split {
  width: 100%;
  max-width: 460px;          /* below 1024 this IS the card, centred as before */
  margin-inline: auto;
}

.auth-split--wide { max-width: 520px; }

@media (min-width: 1024px) {
  .auth-split {
    /* The card track. An explicit modifier rather than :has(.auth-card--wide):
       the fallback for a browser without :has() would be a 520px card in a
       460px track, and two class names cost less than that support question. */
    --auth-col: 460px;
    display: grid;
    grid-template-columns: minmax(320px, 1fr) var(--auth-col);
    align-items: center;
    gap: clamp(48px, 6vw, 96px);
    max-width: 1120px;
  }

  .auth-split--wide { --auth-col: 520px; max-width: 1180px; }

  /* Grid children default to min-content; without this a long unbroken string
     in the card can push the track wider than its declared width. */
  .auth-stage { min-width: 0; }
  .auth-stage .auth-card { max-width: none; }
}

/* -----------------------------------------------------------------------------
   THE ASIDE
   Two variants, because the two pages need opposite collapse behaviour.

   `.auth-aside--decor` (signin) is purely decorative and DISAPPEARS below 1024.
   Nothing is lost: the wordmark is already in `.sh-brand__name` in the fixed
   header, the positioning line is already the meta description, and the trust
   cues are already the wallet button label and `.auth-hint--center`. Restacking
   would push the form below a screenful of restated marketing on exactly the
   viewport where the fold is tightest.

   Bare `.auth-aside` (onboarding) STAYS IN FLOW at every width, because it
   carries the progress ladder — and `aria-current="step"` is the only thing
   telling a screen-reader user where they are in the funnel. It is the one
   element here that is not duplicated elsewhere on the page. Only its
   decorative children hide.
   ----------------------------------------------------------------------------- */
.auth-aside {
  display: flex;
  flex-direction: column;
  align-items: center;        /* stacked: the ladder centres as it does today */
  gap: var(--space-5);
  width: 100%;
}

.auth-aside--decor { display: none; }

.auth-aside__glass,
.auth-aside__lockup,
.auth-aside__brand,
.auth-aside__line,
.auth-aside__proof { display: none; }

@media (min-width: 1024px) {
  .auth-aside {
    align-items: flex-start;
    max-width: 460px;
  }
  .auth-aside--decor { display: flex; }
  .auth-aside__glass  { display: block; }
  .auth-aside__lockup { display: block; }
  .auth-aside__brand { display: block; }
  .auth-aside__line  { display: block; }
  .auth-aside__proof { display: flex; }
}

/* -----------------------------------------------------------------------------
   THE MARK IN THE RAIL                                          (2026-08-21)
   Two layers of the same object: an oversized ghost bleeding off the panel edge
   for atmosphere, and a small crisp one in the lockup. One object at two scales
   reads as depth; a single centred logo reads as a placeholder.

   2026-08-21 (b) · SWAPPED FROM THE AUTHORED VECTOR TO THE BAKED RENDER.
   static/landing/mark-baked/pose-front-900.webp is the offline-baked 3D glass
   mark -- the same render the site header's brand mark and the favicon come
   from -- and it has real refraction, thickness and specular depth that a flat
   SVG cannot fake. Kaust's call, and he is right: a vector of this mark reads
   as an app icon, the render reads as glass.

   THE TRADE, STATED: the render cannot be re-tinted per level, so the mark no
   longer walks cyan -> violet -> mint through the funnel. That cue is not lost;
   the onboarding ladder beside it still carries the level on every rung, which
   is where it was load-bearing. What went was decoration, not signal.

   THE GHOST IS A GLOW, NOT A SECOND MARK. It was a huge low-opacity copy of the
   vector, which worked while the mark was flat: crisp shapes at 20% read as a
   watermark. The baked render carries its own soft bloom, so blown up and
   masked it turned into a grey rectangular haze that read as a rendering fault
   behind the copy. It is now a pure radial gradient -- the same atmosphere,
   none of the silhouette, and one fewer image.

   THE LOCKUP IS SIZED TO THE GLASS, NOT TO THE CANVAS. Measured off the asset's
   alpha channel: the render is 900x900 but the glass occupies only x 103-797,
   y 189-709, i.e. 695x521 centred in a lot of transparent padding. Sized
   naively, 184px of <img> is 145px of visible glass sitting 21px inboard of the
   text below it, which is what made the lockup look indented. So the canvas is
   sized so the GLASS is 184px (184 * 900/695 = 238) and the padding is pulled
   back with negative margins derived from the same measurement
   (103/900 * 238 = 27 horizontally, 189/900 * 238 = 50 vertically). Re-measure
   these four numbers if the asset is ever re-baked.

   The ghost is `position: absolute` inside the aside, so `.auth-aside` takes
   `position: relative` below. Relative positioning is safe here — it is
   `transform`/`filter`/`will-change` that would make an ancestor a containing
   block and tear the fixed .site-header off the viewport, and there are none.

   The mask is doing real work: without it the ghost's hard glass edges cut
   across the trust list and the copy has to compete with a logo. Faded to
   nothing before it reaches the text, it reads as light in the room instead.
   ----------------------------------------------------------------------------- */
.auth-aside { position: relative; }

.auth-aside__glass {
  position: absolute;
  z-index: -1;
  /* inset rather than a centring transform: with no transform anywhere on this
     surface there is nothing that could become a containing block for the fixed
     .site-header, so the safety note above needs no exception. */
  inset: -10% -34% auto -34%;
  height: 82%;
  pointer-events: none;
  background: radial-gradient(ellipse 48% 42% at 44% 36%, var(--sem-entry-glow), transparent 70%);
  opacity: .5;
}

/* The crisp one. Sized off the type scale so it sits in the lockup rather than
   floating: roughly two lines of the display face. */
.auth-aside__lockup { display: block; margin-bottom: var(--space-4); }
/* 238px of canvas = 184px of actual glass; the negative margins reclaim the
   transparent padding so the glass's left edge lines up with the wordmark. */
.auth-aside__lockup .mk {
  display: block;
  width: 238px;
  height: auto;
  margin: -50px -27px;
}

.auth-aside__brand {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--type-d3);
  font-weight: 600;                    /* inside Clash Display's 200-700 range */
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  color: var(--ink-1);
}

.auth-aside__line {
  margin: 0;
  max-width: 24ch;
  font-size: var(--type-u3);
  line-height: var(--lh-snug);
  color: var(--ink-3);
}

/* The trust cues take .ind-note's left-rail idiom (individual.css:464-490)
   rather than inventing a third callout vocabulary — but as a LIST, because
   there are three of them and three stacked callouts is the same slab wall §7
   exists to break up. */
.auth-aside__proof {
  margin: 0;
  padding: 0 0 0 var(--space-4);
  list-style: none;
  flex-direction: column;
  gap: var(--space-3);
  /* This rail sits directly on the page black, so --sem-entry-edge would in
     fact be legal here. It still uses the -line tier: this is 1px of framing
     next to 13px text, and the quieter tier is what reads as a rail rather
     than as a border. */
  border-left: var(--sem-rule-ladder) solid var(--sem-entry-line);
}

.auth-aside__proof li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--type-u6);
  line-height: var(--lh-snug);
  color: var(--ink-3);
}

.auth-aside__proof li i {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--sem-entry-line);
  border-radius: var(--r-sm);
  background: var(--sem-entry-wash);
  color: var(--sem-entry);
  font-size: .78em;
}

/* -----------------------------------------------------------------------------
   THE LADDER GOES VERTICAL IN THE RAIL
   At 408px the horizontal ladder is tight: 2 x (26 circle + 10 + ~90 label +
   10 + 22 pips) + 2 x 12 gap + 40 connector is about 380px with nothing spare.
   Vertical is both safer and better in a rail, and ALL FOUR non-colour channels
   survive — the connector's WEIGHT channel simply swaps axis, 1px vs
   --sem-rule-ladder measured across instead of along.

   The <=560px `.onboarding-progress__label { display: none }` rule in §8 is
   untouched and still bites only in stacked mode, exactly as today.
   ----------------------------------------------------------------------------- */
@media (min-width: 1024px) {
  .auth-aside .onboarding-progress {
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    gap: var(--space-2);
    max-width: none;
    margin: 0;
  }

  .auth-aside .onboarding-progress__connector {
    flex: 0 0 24px;
    width: 1px;
    max-width: none;
    height: 24px;
    /* centres the run under the 26px circle: 13 - half the travelled 3px */
    margin-inline-start: 12px;
  }

  .auth-aside .onboarding-progress__connector--travelled {
    width: var(--sem-rule-ladder);
    height: 24px;
    margin-inline-start: 11px;
  }
}


/* =============================================================================
   1c · ATMOSPHERE                                               (2026-08-20)
   -----------------------------------------------------------------------------
   Renders from public_base.html's `page_background_markup` block (:198), so it
   is a DIRECT CHILD of <body> and a SIBLING of `.site-header`. It wraps nothing
   and carries no transform / filter / perspective / will-change, so it cannot
   become a containing block for the fixed header. individual.html:120 already
   uses this exact block for `.ind-backdrop`.

   ZERO ANIMATION. Four static radials, the same construction and the same alpha
   tiers as `.ind-backdrop` (individual.css:263-270). This is the deliberate
   no-motion alternative to guide-system.css's drifting mesh orbs: commit
   8b73fe1 removed six infinite animations from behind this very password field,
   and nothing here reintroduces one.

   ON THE HEX CONTRACT. The header docblock says there is not one hex in this
   file, and there still is not — these are rgba() channel values, the same form
   .ind-backdrop uses. site-tokens.css publishes no atmosphere alpha tier; its
   lowest is --sem-*-wash at .10, which is two to three times too strong to sit
   behind reading text without moving the measured surface luminance the
   contrast numbers in this file depend on.
   ============================================================================= */
.auth-backdrop {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    /* the header horizon, in ENTRY cyan: every page on this surface is a
       ladder-1 arrival, and the atmosphere should say so */
    radial-gradient(ellipse 90% 55% at 50% -12%, rgba(0, 242, 254, .07), transparent 70%),
    /* the left field, behind the aside */
    radial-gradient(ellipse 60% 45% at 4% 34%, rgba(179, 136, 255, .05), transparent 72%),
    /* the low-right settle */
    radial-gradient(ellipse 55% 40% at 96% 80%, rgba(105, 240, 174, .035), transparent 70%),
    radial-gradient(ellipse 70% 50% at 88% 108%, rgba(88, 185, 255, .05), transparent 72%);
}

/* A decorative fixed wash in a forced-colours theme is either invisible or an
   opaque block the UA cannot reason about. Remove it there. */
@media (forced-colors: active) {
  .auth-backdrop { display: none; }
}


/* =============================================================================
   2 · CARD
   -----------------------------------------------------------------------------
   NO backdrop-filter. A blurred plate on a page with no content behind it buys
   nothing and costs a compositor layer on the one screen where first paint is
   the conversion. STILL TRUE after the 2026-08-20 pass: the depth below is
   box-shadow, which is painted, not composited, so it costs no layer. There is
   no `filter` on this card either, for the same reason.

   2026-08-20 · the flat --sf-1 rectangle became a lit surface. --sf-1 is one
   uniform 3% white top to bottom, which is what made the card read as a plane
   rather than an object; the gradient below is the same top-lit wash .ind-card
   uses (individual.css:584-585), so the head reads as the lit edge and the foot
   falls away. Peak alpha is .05, under --sf-2's .055, so the surface stays
   inside the contract's resting/hover band rather than inventing a tier.
   ============================================================================= */
.auth-card {
  position: relative;
  /* The ::before level rule runs corner to corner and has to clip to the
     radius. Verified safe against focus rings: the only absolutely-positioned
     descendant on this surface is `.auth-reveal`, inset inside `.auth-password`
     with 12-14px of field padding around it, and the widest ring on the page is
     site-base.css's 2px outline at 2px offset. Nothing reaches the card edge. */
  overflow: hidden;
  width: 100%;
  max-width: 460px;
  border: 1px solid var(--line-1);
  border-radius: var(--r-lg);
  background: linear-gradient(180deg,
              rgba(255, 255, 255, .05),
              rgba(255, 255, 255, .012));
  box-shadow:
    /* lifts the card off the atmosphere behind it */
    0 24px 64px -24px rgba(0, 0, 0, .60),
    /* and a level halo bleeding down from the rule below */
    0 0 48px -20px var(--sem-entry-glow);
  padding: clamp(28px, 4vw, 44px);
}

/* THE LEVEL RULE. The `.gs-card::before` idiom (guide-system.css:643-653),
   which is this repo's existing answer to exactly this. ENTRY, because every
   page that renders a bare `.auth-card` is a ladder-1 arrival surface: signing
   in, creating an account, changing a password, reviewing an order. The tone
   pages in §7 set their own from --st-grad. */
.auth-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--sem-rule-ladder);
  background: var(--grad-entry);
  z-index: 1;
}

/* Signup carries three fields plus a strength meter; 520 keeps the meter and
   the label on one line at 320px zoomed to 200%. */
.auth-card--wide { max-width: 520px; }

.auth-head {
  margin-bottom: var(--space-5);
}

.auth-head__eyebrow {
  display: block;
  margin-bottom: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--type-m3);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--ink-4);
}

.auth-head h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--type-u1);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-display);
  color: var(--ink-1);
}

.auth-head p {
  margin: var(--space-2) 0 0;
  font-size: var(--type-u5);
  color: var(--ink-3);
}

/* ACCENT-SPLIT HEADING · the closing phrase of an <h1> takes the entry
   gradient. Lifted from `.ind-accent` (individual.css:192-205) INCLUDING its
   forced-colors fallback, which guide-system.css's equivalent does not have:
   `background-clip: text` with a transparent fill is INVISIBLE in a
   forced-colours theme, because the UA replaces the background and the text is
   left with nothing to paint with.

   Split at a phrase boundary, never mid-clause. NOT used on the state page's
   <h1> — that heading is the outcome, and §7 already spends its colour budget
   on the rule, the glyph and the chip.

   SIZE FLOOR: `.auth-head h1` is --type-u1 (24 -> 32px) at weight 600, which
   clears WCAG's 18.66px-bold large-text threshold at every step of the clamp,
   so the 3:1 tier applies to both gradient stops. */
.auth-accent {
  background: var(--grad-entry);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

@media (forced-colors: active) {
  .auth-accent {
    background: none;
    -webkit-text-fill-color: currentColor;
    color: CanvasText;
  }
}

/* =============================================================================
   3 · ANTI-PHISHING BLOCK
   -----------------------------------------------------------------------------
   Sits ABOVE the fields, so it is read before a password is typed rather than
   after. Swyftx (the client's own named reference) ships this on its login;
   Mirror Ai did not. Mono + a wash chip so the URL reads as a machine fact,
   not marketing copy.
   ============================================================================= */
.auth-origin {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-1);
  border-radius: var(--r-md);
  /* 2026-08-20 · stepped down from --sf-1. The card now paints its own .05
     wash, so --sf-1's .03 on top of it was the same fill on the same fill and
     the block simply disappeared. .02 reads as an inset well against a lit
     card, which is what this always wanted to be. */
  background: rgba(255, 255, 255, .02);
}

.auth-origin__label {
  font-family: var(--font-mono);
  font-size: var(--type-m3);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-3);
}

.auth-origin__url {
  padding: 4px 12px;
  border: 1px solid var(--sem-entry-line);
  border-radius: var(--r-pill);
  background: var(--sem-entry-wash);
  font-family: var(--font-mono);
  font-size: var(--type-m2);
  letter-spacing: var(--track-data);
  color: var(--ink-1);
  word-break: break-all;
}

/* =============================================================================
   4 · FIELDS
   -----------------------------------------------------------------------------
   The label is a real, persistent <label for>. Placeholder-as-label was the old
   pattern here and it removes the field's name the moment the user starts
   typing, which is precisely when a password manager or a screen reader needs
   it most.
   ============================================================================= */
.auth-field {
  margin-bottom: var(--space-5);
}

.auth-label {
  display: block;
  margin-bottom: var(--space-2);
  /* 2026-08-21 · 14px/600 -> 13px/500. A bold, near-full-size label sitting a
     long way above its field is one of the loudest "form-builder" tells; Linear,
     Vercel and Stripe all run a smaller, lighter label tucked close to the
     input so the pair reads as one unit. The label is NOT floated: floating
     labels are actively contested on legibility grounds and static labels are
     what those same products ship. Only weight and size came down. */
  font-size: var(--type-u6);
  font-weight: 500;
  color: var(--ink-2);
  transition: color var(--dur-2) var(--ease-out);
}

/* THE LABEL JOINS THE FOCUS STATE. Focus stops being one bolted-on ring and
   becomes a choreographed change across border, fill, ring and label together
   — which is most of the difference between a designed form and a default one.
   Both wrappers are named: .auth-field is signin's, .form-group is signup's. */
.auth-field:focus-within .auth-label,
.form-group:focus-within .auth-label { color: var(--sem-entry); }

.auth-input {
  display: block;
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  /* 2026-08-20 · an INSET WELL, not a raised chip. The card is lit now, so a
     field brighter than the surface it sits in read as a floating tile. .02
     plus a shallow inner shadow puts it back below the surface. */
  background: rgba(255, 255, 255, .02);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .28);
  color: var(--ink-1);
  font-family: var(--font-ui);
  font-size: var(--type-u4);
  transition: border-color var(--dur-2) var(--ease-out),
              background   var(--dur-2) var(--ease-out),
              box-shadow   var(--dur-2) var(--ease-out);
}

.auth-input::placeholder { color: var(--ink-4); }

/* -----------------------------------------------------------------------------
   AUTOFILL                                                       (2026-08-21)
   A real defect, not a taste call: Chrome, Edge and Safari force their own
   background and text colour onto an autofilled control with an internal
   !important, so on this form the browser was painting a pale yellow-white box
   over a dark field the moment a password manager filled it. Anyone using a
   password manager — i.e. most of the people signing in — saw a broken form.

   The UA lock covers `background-color` and `color` but NOT `box-shadow` or
   `-webkit-text-fill-color`, which is why the fix is an oversized inset shadow
   rather than a background. Three stacked insets, painted first-on-top: the
   resting well, then a white wash, then an opaque base. --bg-page plus .045
   white lands within about one RGB step of the real field colour over the
   card, so an autofilled field is indistinguishable from a typed one.

   THE ONLY `!important` IN THIS FILE, and it is unavoidable: it exists solely
   to out-rank the UA's own important declaration. The header docblock's "no
   !important" claim is amended for this block and this block only.

   `transition: background-color 5000s` is the standard trick for suppressing
   the browser's flash of yellow on load: the transition never completes, so
   the forced colour never visibly arrives.
   ----------------------------------------------------------------------------- */
.auth-input:-webkit-autofill,
.auth-input:-webkit-autofill:hover,
.auth-input:-webkit-autofill:focus,
.auth-input:-webkit-autofill:active {
  -webkit-box-shadow:
    inset 0 1px 2px rgba(0, 0, 0, .28),
    inset 0 0 0 1000px rgba(255, 255, 255, .045),
    inset 0 0 0 1000px var(--bg-page) !important;
  -webkit-text-fill-color: var(--ink-1) !important;
  caret-color: var(--ink-1);
  border-color: var(--line-2);
  transition: background-color 5000s ease-in-out 0s;
}

/* The unprefixed standard, for engines that ship it. Same intent, and harmless
   where the prefixed rule above already won. */
.auth-input:autofill {
  -webkit-text-fill-color: var(--ink-1);
  caret-color: var(--ink-1);
}

.auth-input:hover {
  border-color: var(--line-3);
  background: rgba(255, 255, 255, .035);
}

/* TWO channels, not one: the border changes AND a ring appears. site-base.css
   already paints an accent :focus-visible outline on everything, so a hue-blind
   user still gets the ring's geometry. `outline` is deliberately NOT reset. */
.auth-input:focus {
  border-color: var(--sem-entry);
  /* 2026-08-21 · A LAYERED HALO, not a flat ring. A single solid 3px ring is
     the browser default with the colour swapped, which is why it reads cheap.
     This is the Geist/Linear construction: the well stays, a 1px ring sits
     flush to the border to define it, a wider low-alpha ring blooms out, and a
     soft blur finishes it. The alphas are deliberately low — the line between
     "premium glow" and "synthwave" is saturation, not size.
     The fill also lifts, which is the cue you feel before you read the colour.
     Meaning is still carried by the border change, the ring geometry and
     site-base.css's untouched :focus-visible outline, never by hue alone. */
  background: rgba(255, 255, 255, .05);
  box-shadow:
    inset 0 1px 2px rgba(0, 0, 0, .25),
    0 0 0 1px var(--sem-entry),
    0 0 0 5px var(--sem-entry-wash),
    0 0 18px -4px var(--sem-entry-glow);
}

/* THE GLOBAL OUTLINE IS REPLACED HERE, NOT REMOVED. site-base.css:124 paints
   `outline: 2px solid var(--accent)` at a 2px offset on every :focus-visible.
   On a field whose border ALSO goes solid cyan on focus that lands as two hard
   concentric rings with a dark gap between them, which is what made the focused
   field look like a neon sign and buried the soft halo in the middle. Only the
   ring geometry changes: the 1px border plus the flush 1px shadow ring above
   give a continuous 2px solid cyan perimeter, so the indicator is no thinner
   than the outline it replaces, and it now applies on click as well as on
   keyboard rather than the two paths looking like different designs.

   FORCED COLORS RESTORES IT. box-shadow is not painted in a forced-colours
   theme, so without this block a high-contrast user would have had no focus
   indicator at all -- an outline suppressed with nothing behind it is exactly
   the regression this rule exists to avoid. */
.auth-input:focus-visible { outline: none; }

@media (forced-colors: active) {
  .auth-input:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
}

/* Error is announced (aria-describedby + the message below), outlined, and
   captioned. The colour is the third signal, not the first. */
/* `.error` / `.success` are what static/js/onboarding.js writes on the signup
   step (it sets aria-invalid alongside them); the attribute selector is what
   signin.html's own handler writes. Both spellings are honoured so neither
   surface needs to know how the other validates. */
/* HALT, not "red": a rejected field is the interrupt role's literal
   definition. The border also goes to 2px, so the invalid field is heavier
   than its neighbours before hue is read at all. */
.auth-input[aria-invalid="true"],
.auth-input.error {
  border-color: var(--sem-halt);
  border-width: 2px;
  /* 12px - 1px, so the 2px border does not shift the text baseline. */
  padding: 11px 13px;
  /* 2026-08-20 · THE SHAPE CHANNEL, adopted from contact.html:186-190. Before
     this, `.error` and `.success` BOTH stepped to a 2px border and were then
     told apart by hue alone — the weight step separated touched from
     untouched, but never valid from invalid. That is a WCAG 1.4.1 failure and
     contact.html had already solved it. Bar-present vs bar-absent is shape.
     It overrides the resting well shadow on purpose: an invalid field is not
     a resting field. */
  box-shadow: inset var(--sem-rule-interrupt) 0 0 0 var(--sem-halt);
}

.auth-input[aria-invalid="true"]:focus,
.auth-input.error:focus {
  border-color: var(--sem-halt);
  box-shadow: 0 0 0 3px var(--sem-halt-wash);
}

/* 2026-08-20 · --sem-master-edge -> the SOLID hue, and the same 2px/11px pair
   as `.error`.

   Two reasons. (1) THE -edge TRAP: -edge's 3.03:1 was measured against the page
   black, and this field now sits on a card that paints its own .05 wash, where
   the same value lands under 3:1 (individual.css:100-106 measured the sibling
   token at 2.95:1 in exactly this situation). The solid hue is unambiguous.
   (2) `.error` and `.success` are now GEOMETRICALLY IDENTICAL — same border
   width, same padding — and are separated only by the invalid state's inset
   bar. That is the point: shape, not hue. Bar-absent is the valid signal. */
.auth-input.success {
  border-color: var(--sem-master);
  border-width: 2px;
  padding: 11px 13px;
}

.auth-hint {
  margin-top: var(--space-2);
  font-size: var(--type-u6);
  color: var(--ink-4);
}

.auth-hint--center { text-align: center; }

/* The one spacing utility in this file. Named for what it does (separate a
   trailing block from the stack above it), not for its value. */
.auth-gap { margin-top: var(--space-5); }

/* The live region. Empty by default; `:empty` collapse keeps the layout from
   jumping when a message arrives and leaves. */
.auth-error {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-top: var(--space-2);
  font-size: var(--type-u6);
  line-height: var(--lh-snug);
  color: var(--sem-halt);
}

.auth-error:empty { display: none; }

/* --- password field + reveal ------------------------------------------- */
.auth-password {
  position: relative;
}

.auth-password .auth-input {
  padding-right: 46px;
}

.auth-reveal {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
  transition: color var(--dur-2) var(--ease-out);
}

.auth-reveal:hover {
  color: var(--ink-1);
  background: rgba(255, 255, 255, .04);
}

/* The JS already flips aria-pressed AND swaps fa-eye / fa-eye-slash
   (signin.html and change_password.html both), so the glyph is the shape
   channel and this is the hue beside it. Nothing new is asked of the JS. */
.auth-reveal[aria-pressed="true"] { color: var(--sem-entry); }

/* --- checkbox row ------------------------------------------------------- */
.auth-check {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: var(--space-5);
  /* 2026-08-20 · the ROW is the target, not the 16px box. A bare UA checkbox
     floating on a card is the least considered control on the page. */
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-1);
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .02);
  font-size: var(--type-u5);
  color: var(--ink-3);
  transition: border-color var(--dur-2) var(--ease-out),
              background   var(--dur-2) var(--ease-out);
}

.auth-check:hover {
  border-color: var(--line-2);
  background: rgba(255, 255, 255, .035);
}

/* Checked is reinforced on the ROW as well as the box. The native checkmark is
   still the primary signal; this is redundancy. :has() rather than a class so
   nothing is asked of the JS, and a browser without :has() simply loses the
   reinforcement — the checkbox itself is unaffected. */
.auth-check:has(input:checked) {
  border-color: var(--sem-entry-line);
  background: var(--sem-entry-wash);
}

.auth-check input[type="checkbox"] {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  accent-color: var(--sem-entry);
  cursor: pointer;
}

.auth-check label { cursor: pointer; }

/* =============================================================================
   5 · ACTIONS
   ============================================================================= */
.auth-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 13px 20px;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  /* --grad-entry, not --grad-accent: same two stops, but named for the job.
     ENTRY is the ladder's "the recommended path, the primary action" rung, and
     the primary action is literally what this is. --sem-ink-on-fill measures
     13.67:1 on the near stop and 8.85:1 on the far one. */
  background: var(--grad-entry);
  color: var(--sem-ink-on-fill);
  font-family: var(--font-ui);
  font-size: var(--type-u5);
  font-weight: 600;
  letter-spacing: var(--track-tight);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  /* 48px floor, matching .ind-btn's tap target across the rest of the site.
     The resting glow is what makes the primary action read as lit rather than
     as a coloured rectangle; box-shadow is painted, not composited, so it
     costs no layer on the conversion screen. */
  min-height: 48px;
  /* The inset top highlight is what makes a filled button read as a physical
     surface catching light rather than as a coloured rectangle. The brand
     gradient itself is deliberately NOT changed: site-chrome.css's .sh-cta runs
     the same --grad-entry with dark ink, and a one-off single-hue button here
     would make the header CTA and the form CTA two different products. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .25),
    0 8px 22px -10px var(--sem-entry-glow);
  transition: filter var(--dur-2) var(--ease-out),
              box-shadow var(--dur-2) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}

.auth-btn:hover {
  filter: brightness(1.08);
  transform: translateY(-1px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .3),
    0 14px 30px -12px var(--sem-entry-glow);
}

.auth-btn:active { transform: translateY(1px); }

.auth-btn[disabled],
.auth-btn.loading {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
  /* A glowing disabled button is a lie about what it will do. */
  box-shadow: none;
}

.auth-btn--ghost {
  /* 2026-08-20 · was transparent on --line-2, which made the secondary action
     a hairline rectangle indistinguishable from a container. It now carries
     the entry wash, so it reads as an action at rest. */
  background: var(--sem-entry-wash);
  border-color: var(--sem-entry-line);
  color: var(--ink-1);
  box-shadow: none;
}

.auth-btn--ghost:hover {
  filter: none;
  /* SOLID hue, not -edge: this button paints its own wash, so its border is
     measured a surface higher than the tier -edge was calibrated against. Same
     reasoning as .ind-btn--ghost (individual.css:424-428). */
  border-color: var(--sem-entry);
  background: var(--sem-entry-wash);
  box-shadow: 0 0 24px -12px var(--sem-entry-glow);
}

/* THE THIRD TIER                                              (2026-08-20)
   `success.html` stacked three identical full-width buttons — primary,
   secondary and "Manage subscription" — and three slabs of equal weight read
   as a wall rather than as a choice. The fix is a hierarchy, not another
   colour: this tier is a rule and a label and nothing else, so the eye sorts
   the three by weight before it reads a word. See §7's `.state-actions`. */
.auth-btn--subtle {
  background: transparent;
  border-color: var(--line-1);
  color: var(--ink-3);
  box-shadow: none;
  font-weight: 500;              /* inside General Sans' declared 200-700 */
}

.auth-btn--subtle:hover {
  filter: none;
  transform: none;
  background: var(--sf-1);
  border-color: var(--line-2);
  color: var(--ink-1);
  box-shadow: none;
}

/* Underline, always. See the colour note in the header docblock. */
.auth-link {
  color: var(--sem-entry);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.auth-link:hover { color: var(--accent-2-tint); }

.auth-foot {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line-1);
  font-size: var(--type-u6);
  color: var(--ink-3);
  text-align: center;
}

.auth-foot p { margin: 0 0 var(--space-2); }
.auth-foot p:last-child { margin-bottom: 0; }

/* =============================================================================
   6 · NOTICES
   -----------------------------------------------------------------------------
   The beta / educational / security disclosures, AND signin's flashed
   authentication errors. A left rule instead of a tinted fill: a tinted card on
   a dark page reads as an error state whatever the copy says.

   The BARE .auth-note is deliberately toneless — an "informational content
   only" disclosure is not a state and must not borrow one of the five roles.
   The --tone modifiers below are for the flash region, where the category
   really is an outcome, and they carry the interrupt rule weight so a failed
   sign-in is heavier than a disclosure before hue is read.
   ============================================================================= */
.auth-note {
  display: flex;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-1);
  border-left: 2px solid var(--line-3);
  border-radius: var(--r-sm);
  /* 2026-08-20 · stepped down from --sf-1. The card now paints its own .05
     wash, so --sf-1's .03 on top of it was the same fill on the same fill and
     the block simply disappeared. .02 reads as an inset well against a lit
     card, which is what this always wanted to be. */
  background: rgba(255, 255, 255, .02);
  font-size: var(--type-u6);
  line-height: var(--lh-snug);
  color: var(--ink-3);
}

.auth-note i { color: var(--ink-4); }
.auth-note strong { display: block; color: var(--ink-2); }

/* Every toned note ships a <strong> WORD ("Sign-in failed", "Heads up",
   "Success", "Notice") — see signin.html. The hue is the third channel. */
.auth-note--halt {
  border-left: var(--sem-rule-interrupt) solid var(--sem-halt);
}

.auth-note--caution {
  border-left: var(--sem-rule-interrupt) solid var(--sem-caution);
}

.auth-note--master {
  border-left: var(--sem-rule-ladder) solid var(--sem-master);
}

.auth-note--entry {
  border-left: var(--sem-rule-ladder) solid var(--sem-entry);
}

.auth-note--halt i    { color: var(--sem-halt); }
.auth-note--caution i { color: var(--sem-caution); }
.auth-note--master i  { color: var(--sem-master); }
.auth-note--entry i   { color: var(--sem-entry); }

/* --- OR divider + wallet list ------------------------------------------- */
.auth-divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-5) 0;
  font-family: var(--font-mono);
  font-size: var(--type-m3);
  letter-spacing: var(--track-label);
  color: var(--ink-4);
}

.auth-divider::before,
.auth-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--line-1);
}

.auth-wallets {
  display: none;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.auth-wallets__item {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 12px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-2);
  font-size: var(--type-u6);
  cursor: pointer;
  transition: background var(--dur-2) var(--ease-out);
}

.auth-wallets__item:hover { background: var(--sf-2); }

/* =============================================================================
   7 · STATE PAGE  ·  components/state_page.html
   -----------------------------------------------------------------------------
   One centred column, 520px, shared header present. NEVER a chrome-less shell:
   a full-viewport card with no site header on a page that just took a payment
   is the exact shape of a phishing interstitial.

   TONE STILL MAPS TO A HAIRLINE AND A GLYPH, NEVER A TINTED CARD. That rule is
   unchanged and is the reason this surface can afford saturated hues at all:
   the card fill and its three other borders stay --sf-1 / --line-1 in every
   tone, because a rose-tinted card turns a recoverable "your card was declined"
   into a page that looks like data loss. What P2 adds is a THIRD tone-bearing
   element — the status chip — and that one is a word first, a glyph second and
   a hue third.

   THE LEVEL GROUP. Every tone-aware rule below reads --st-*, and only the five
   modifiers write it. That is copied deliberately from guide-system.css's
   --lvl-* pattern: a level can never be half-applied, because there is exactly
   one place per tone where it is set.

     .state-page--entry    ladder 1 · arriving, waiting        · cyan   · 3px
     .state-page--build    ladder 2 · working inside           · violet · 3px
     .state-page--master   ladder 3 · it worked, confirmed     · mint   · 3px
     .state-page--caution  interrupt · decide before you act   · amber  · 5px
     .state-page--halt     interrupt · this did not work       · rose   · 5px

   The four legacy tone names the component shipped with are kept as ALIASES on
   the same rules, so any consumer still passing success/warning/error/neutral
   renders the correct role rather than falling through to the default.
   ============================================================================= */
.state-page {
  width: 100%;
  max-width: 520px;
  margin-inline: auto;
  text-align: center;

  /* DEFAULT LEVEL = entry. A state page with no tone is a page telling you
     where you have arrived, which is what ENTRY means. */
  --st:        var(--sem-entry);
  --st-wash:   var(--sem-entry-wash);
  --st-edge:   var(--sem-entry-edge);
  --st-grad:   var(--grad-entry);
  --st-glow:   var(--sem-entry-glow);
  --st-rule:   var(--sem-rule-ladder);
  --st-border: 1px;
}

/* LADDER · 3px rule, 1px chip border ------------------------------------- */
.state-page--entry,
.state-page--neutral {
  --st:        var(--sem-entry);
  --st-wash:   var(--sem-entry-wash);
  --st-edge:   var(--sem-entry-edge);
  --st-grad:   var(--grad-entry);
  --st-glow:   var(--sem-entry-glow);
  --st-rule:   var(--sem-rule-ladder);
  --st-border: 1px;
}

.state-page--build {
  --st:        var(--sem-build);
  --st-wash:   var(--sem-build-wash);
  --st-edge:   var(--sem-build-edge);
  --st-grad:   var(--grad-build);
  --st-glow:   var(--sem-build-glow);
  --st-rule:   var(--sem-rule-ladder);
  --st-border: 1px;
}

.state-page--master,
.state-page--success {
  --st:        var(--sem-master);
  --st-wash:   var(--sem-master-wash);
  --st-edge:   var(--sem-master-edge);
  --st-grad:   var(--grad-master);
  --st-glow:   var(--sem-master-glow);
  --st-rule:   var(--sem-rule-ladder);
  --st-border: 1px;
}

/* INTERRUPT · 5px rule, 2px chip border ---------------------------------- */
.state-page--caution,
.state-page--warning {
  --st:        var(--sem-caution);
  --st-wash:   var(--sem-caution-wash);
  --st-edge:   var(--sem-caution-edge);
  --st-grad:   var(--grad-caution);
  --st-glow:   var(--sem-caution-glow);
  --st-rule:   var(--sem-rule-interrupt);
  --st-border: 2px;
}

.state-page--halt,
.state-page--error {
  --st:        var(--sem-halt);
  --st-wash:   var(--sem-halt-wash);
  --st-edge:   var(--sem-halt-edge);
  --st-grad:   var(--grad-halt);
  --st-glow:   var(--sem-halt-glow);
  --st-rule:   var(--sem-rule-interrupt);
  --st-border: 2px;
}

.state-page__card {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line-1);
  /* The rule is a ::before now, so the border-top goes. */
  border-top: 0;
  border-radius: var(--r-lg);
  /* Same lit surface as .auth-card in §2, for the same reason. */
  background: linear-gradient(180deg,
              rgba(255, 255, 255, .05),
              rgba(255, 255, 255, .012));
  box-shadow:
    0 24px 64px -24px rgba(0, 0, 0, .60),
    /* THE TONE, AS DEPTH. A halt page glows rose, a master page mint. This is
       the FOURTH channel on a page that already has three (the rule's weight,
       the glyph, the chip's word), so it is redundancy and never load-bearing.
       And it is emphatically NOT a tinted card: the fill and the other three
       borders stay neutral in every tone, because a rose-tinted card turns a
       recoverable "your card was declined" into a page that looks like data
       loss. That rule is why this is a halo and not a background. */
    0 0 56px -22px var(--st-glow);
  padding: clamp(28px, 5vw, 44px) clamp(22px, 4vw, 40px);
}

/* THE WEIGHT CHANNEL, PRESERVED EXACTLY: 3px ladder vs 5px interrupt, still
   read from --st-rule. What changed is the fill — a gradient rather than a flat
   --st-edge — and that is also the -edge fix. The card now paints its own .05
   wash, and --sem-*-edge was calibrated against the page black; on a washed
   surface the sibling token measured 2.95:1. --st-grad's stops are
   full-strength hue, so the rule cannot fall under the floor. */
.state-page__card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--st-rule);
  background: var(--st-grad);
  z-index: 1;
}

.state-page__glyph {
  width: 56px;
  height: 56px;
  margin: 0 auto var(--space-4);
  display: flex;
  align-items: center;
  justify-content: center;
  /* 2026-08-20 · --st-edge -> the solid hue. THE -edge TRAP: this border sits
     on --st-wash, which sits on a card that now paints its own .05 wash, so it
     is measured two surfaces above the page black -edge was calibrated against
     (individual.css:100-106 measured the sibling token at 2.95:1 there). The
     --st-border weight channel, 1px ladder vs 2px interrupt, is untouched. */
  border: var(--st-border) solid var(--st);
  border-radius: var(--r-full);
  background: var(--st-wash);
  color: var(--st);
  font-size: 21px;
  box-shadow: 0 0 32px -10px var(--st-glow);
}

/* --- THE WORD  ·  the second channel, and it is not optional -------------
   state_page.html renders this element for EVERY state, with a per-tone
   default, so a page cannot ship a hue with no word attached. The glyph is
   picked by TONE rather than by the page, which is what keeps the interrupt
   shapes (triangle, x-circle) distinct from the ladder's round ones even on a
   page whose hero glyph is a credit card or an envelope.
   --st on --st-wash measures 11.3-11.9:1 for every one of the five roles. */
.state-page__status {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 0 0 var(--space-3);
  padding: 5px 13px;
  /* Same -edge -> solid switch and the same reason as the glyph above. */
  border: var(--st-border) solid var(--st);
  border-radius: var(--r-pill);
  background: var(--st-wash);
  font-family: var(--font-mono);
  font-size: var(--type-m2);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--st);
}

.state-page__status i { font-size: .82em; }

.state-page h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--type-u1);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-display);
  color: var(--ink-1);
}

.state-page__body {
  margin: var(--space-3) 0 0;
  font-size: var(--type-u5);
  line-height: var(--lh-body);
  color: var(--ink-3);
}

.state-page__subject {
  display: inline-block;
  margin-top: var(--space-3);
  padding: 5px 14px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.04);
  font-family: var(--font-mono);
  font-size: var(--type-m2);
  letter-spacing: var(--track-data);
  color: var(--ink-1);
  word-break: break-all;
}

/* --- optional <dl> detail block ---------------------------------------- */
.state-details {
  margin: var(--space-5) 0 0;
  border-top: 1px solid var(--line-1);
  text-align: left;
}

.state-details__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 10px 0;
  border-bottom: 1px solid var(--line-1);
}

.state-details__row dt {
  font-size: var(--type-u6);
  color: var(--ink-3);
}

.state-details__row dd {
  margin: 0;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--type-m2);
  letter-spacing: var(--track-data);
  color: var(--ink-1);
  text-align: right;
}

/* --- what-next list ----------------------------------------------------- */
.state-list {
  margin: var(--space-5) 0 0;
  padding: 0;
  list-style: none;
  text-align: left;
}

.state-list li {
  display: flex;
  gap: 10px;
  padding: 7px 0;
  font-size: var(--type-u6);
  line-height: var(--lh-snug);
  color: var(--ink-3);
}

.state-list li::before {
  content: '';
  flex: 0 0 auto;
  width: 5px;
  height: 5px;
  margin-top: 8px;
  border-radius: var(--r-full);
  /* A ring rather than a solid disc: it stays the same SHAPE, but stops
     competing with the chip and the glyph for the eye. */
  border: 1px solid var(--st);
  background: var(--st-wash);
}

/* --- next-step links ---------------------------------------------------- */
.state-next {
  margin: var(--space-5) 0 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--line-1);
  text-align: left;
}

.state-next__eyebrow {
  display: block;
  margin-bottom: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--type-m3);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--ink-4);
}

.state-next a {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 8px 0;
  font-size: var(--type-u5);
  color: var(--ink-2);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.state-next a:hover { color: var(--ink-1); }
.state-next a span { color: var(--ink-4); text-decoration: none; font-size: var(--type-u6); }

/* --- actions ------------------------------------------------------------ */
/* THE SLAB WALL                                               (2026-08-20)
   success.html stacked three identical full-width buttons and they read as a
   wall rather than as a choice. Three mechanisms, none of them colour: a rule
   separating the outcome from the doors, a two-up pairing for the first two
   actions, and a quieter third tier (`.auth-btn--subtle`, §5). */
.state-actions {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line-1);
}

/* .state-details' last row already draws a border-bottom. Two hairlines 24px
   apart is a mistake, not a rhythm. */
.state-details + .state-actions {
  border-top: 0;
  padding-top: 0;
}

/* The card's content box is 440-476px at its 520px cap, so two tracks at
   1.2/1 give roughly 240 / 200 — enough for "Enter Mirror Ai" and "View
   profile" on one line each at --type-u5. Below 560 it stays one column. */
@media (min-width: 560px) {
  .state-actions--pair { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); }
}

.state-page__note {
  margin-top: var(--space-5);
  font-size: var(--type-u6);
  color: var(--ink-4);
}

/* =============================================================================
   8 · ONBOARDING PROGRESS  ·  THE LADDER  ·  multi_step_signup.html
   -----------------------------------------------------------------------------
   An <ol>, not a nav — there is nothing to navigate to. This is the one place
   on the site where the ladder is not a metaphor: step 1 is ENTRY (arriving),
   step 2 is BUILD (inside the product), and a finished step is MASTER (it
   worked). Each rung declares its own --rung-* group; the components below read
   only those, so a rung can never be half-coloured.

   THREE NON-COLOUR CHANNELS, so the position is legible with no hue at all:

     · FILL vs RING   a completed rung is a FILLED disc carrying dark ink; an
                      active rung is a RING; a pending rung is a dimmer, thinner
                      ring. Shape survives greyscale absolutely.
     · TICK vs NUMERAL a completed rung shows a tick, every other rung shows its
                      number. Also the only channel a 1-bit renderer keeps.
     · PIP COUNT      --sem-pip discs, filled = the rung's depth (rung 1 shows
                      one filled + one hollow, rung 2 shows two filled). The
                      contract's own ordered channel, and it is the one that
                      still works at 390px where the text labels are hidden.

   The connector's HEIGHT is a fourth: 1px while the step ahead is unreached,
   --sem-rule-ladder (3px) once it has been travelled.
   ============================================================================= */
.onboarding-progress {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  width: 100%;
  max-width: 520px;
  margin: 0 auto var(--space-5);
  padding: 0;
  list-style: none;
}

/* PENDING is the base state, so a rung that is neither active nor complete
   needs no modifier of its own. */
.onboarding-progress__step {
  display: flex;
  align-items: center;
  gap: 10px;

  --rung:      var(--ink-4);
  --rung-edge: var(--line-2);
  --rung-wash: transparent;
}

/* The <ol> is step · connector · step, so :first-child / :last-child name the
   two rungs without the template having to number them in a class.

   AND-ed with --active on purpose. Colouring a rung by its POSITION alone put
   two filled violet pips on step 2 while the user was still on step 1, which
   reads as "you have reached rung 2" — the exact opposite of what the pips are
   for. An UNREACHED rung keeps the dim default above; a rung only takes its
   ladder hue once you are standing on it. Verified in a browser at 1440 and
   390, not reasoned about. */
.onboarding-progress__step--active:first-child {
  --rung:      var(--sem-entry);
  --rung-edge: var(--sem-entry-edge);
  --rung-wash: var(--sem-entry-wash);
}

.onboarding-progress__step--active:last-child {
  --rung:      var(--sem-build);
  --rung-edge: var(--sem-build-edge);
  --rung-wash: var(--sem-build-wash);
}

/* MASTER. No conflict with the two rules above — a rung is never both active
   and completed — but it stays below them so the intent reads top-to-bottom:
   unreached, standing on it, passed it. */
.onboarding-progress__step--completed {
  --rung:      var(--sem-master);
  --rung-edge: var(--sem-master-edge);
  --rung-wash: var(--sem-master-wash);
}

.onboarding-progress__circle {
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  font-family: var(--font-mono);
  font-size: var(--type-m3);
  color: var(--ink-4);
}

.onboarding-progress__label {
  font-size: var(--type-u6);
  color: var(--ink-4);
}

.onboarding-progress__pips {
  display: inline-flex;
  align-items: center;
  gap: var(--sem-pip-gap);
}

.onboarding-progress__pip {
  width: var(--sem-pip);
  height: var(--sem-pip);
  border-radius: var(--r-full);
  border: 1.5px solid var(--rung-edge);
  background: transparent;
  flex-shrink: 0;
}

.onboarding-progress__pip--on {
  background: var(--rung);
  border-color: var(--rung);
}

.onboarding-progress__connector {
  flex: 1 1 40px;
  max-width: 72px;
  height: 1px;
  background: var(--line-1);
}

/* Travelled: three times the height, and mint. The height is what carries it
   in greyscale. */
.onboarding-progress__connector--travelled {
  height: var(--sem-rule-ladder);
  border-radius: var(--r-xs);
  background: var(--sem-master);
}

/* ACTIVE · a RING. 2px, so it is heavier than a pending rung's 1px. */
.onboarding-progress__step--active .onboarding-progress__circle {
  border: 2px solid var(--rung-edge);
  background: var(--rung-wash);
  color: var(--ink-1);
}

.onboarding-progress__step--active .onboarding-progress__label {
  color: var(--ink-1);
  font-weight: 600;
}

/* COMPLETED · a FILLED disc. --sem-ink-on-fill on --sem-master measures
   12.6:1, which is the whole reason the fill can be the full-strength hue
   rather than a wash. */
.onboarding-progress__step--completed .onboarding-progress__circle {
  border: 1px solid var(--rung);
  background: var(--rung);
  color: var(--sem-ink-on-fill);
}

.onboarding-progress__step--completed .onboarding-progress__label { color: var(--ink-3); }

@media (max-width: 560px) {
  /* The pips deliberately STAY. With the words gone they are the only ordered
     channel left, and they are 7px. */
  .onboarding-progress__label { display: none; }
}

/* =============================================================================
   9 · SIGNUP FORM EXTRAS
   -----------------------------------------------------------------------------
   Class names and ids in this section are a hard contract with
   static/js/onboarding.js: .form-group, .input-feedback (+ .error/.success/
   .show), .strength-fill (+ .weak/.medium/.strong), .email-suggestion,
   .btn-text, .loading, .form-message__text/__actions/__hint. Renaming any of
   them silently breaks inline validation on the create-account step.
   ============================================================================= */
.selected-plan-row {
  margin-bottom: var(--space-5);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-1);
  border-radius: var(--r-md);
  /* 2026-08-20 · stepped down from --sf-1. The card now paints its own .05
     wash, so --sf-1's .03 on top of it was the same fill on the same fill and
     the block simply disappeared. .02 reads as an inset well against a lit
     card, which is what this always wanted to be. */
  background: rgba(255, 255, 255, .02);
}

.selected-plan-row__label {
  display: block;
  margin-bottom: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--type-m3);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-4);
}

.selected-plan-row__select {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--bg-raise);
  color: var(--ink-1);
  font-size: var(--type-u5);
  cursor: pointer;
}

.form-group { margin-bottom: var(--space-5); }

.input-feedback {
  display: none;
  align-items: flex-start;
  gap: 8px;
  margin-top: var(--space-2);
  font-size: var(--type-u6);
  line-height: var(--lh-snug);
  color: var(--ink-3);
}

.input-feedback.show { display: flex; }
.input-feedback.error { color: var(--sem-halt); }
.input-feedback.success { color: var(--sem-master); }

.email-suggestion:empty { display: none; }
.email-suggestion {
  margin-top: var(--space-2);
  font-size: var(--type-u6);
  color: var(--ink-3);
}

.email-suggestion a,
.form-message__actions a {
  color: var(--sem-entry);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.password-strength {
  margin-top: var(--space-3);
}

.strength-bar {
  /* 3 -> 4px, and off --line-1: that is a BORDER token being used as a fill,
     which is why the track read as a hairline rather than as a gauge. */
  height: 4px;
  border-radius: var(--r-xs);
  background: rgba(255, 255, 255, .06);
  overflow: hidden;
}

.strength-fill {
  width: 0;
  height: 100%;
  border-radius: var(--r-xs);
  background: var(--ink-4);
  transition: width var(--dur-3) var(--ease-out);
}

/* TWO CALLERS, TWO MECHANISMS — verified in a browser, not assumed:
     · multi_step_signup.html is driven by static/js/onboarding.js, which
       writes an INLINE `style.width` in 20% steps (score 0-5 of 5) and only
       uses these classes for the colour. Inline wins, so the widths below are
       inert there. At score 0 the bar is genuinely 0px wide and the WORDING
       ("Weak password") is what carries the meaning.
     · change_password.html sets only the class, so these widths are the live
       signal there.
   Either way the bar's LENGTH and the label's WORDING both change; colour is
   the third channel, never the only one. */
.strength-fill.weak   { width: 33%;  background: var(--sem-halt); }
.strength-fill.medium { width: 66%;  background: var(--sem-caution); }
.strength-fill.strong { width: 100%; background: var(--sem-master); }

.strength-text {
  display: inline-block;
  margin-top: 6px;
  font-size: var(--type-u6);
  color: var(--ink-4);
}

.form-error,
.form-message {
  display: none;
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-1);
  border-left: var(--sem-rule-interrupt) solid var(--sem-halt);
  border-radius: var(--r-sm);
  /* 2026-08-20 · stepped down from --sf-1. The card now paints its own .05
     wash, so --sf-1's .03 on top of it was the same fill on the same fill and
     the block simply disappeared. .02 reads as an inset well against a lit
     card, which is what this always wanted to be. */
  background: rgba(255, 255, 255, .02);
  font-size: var(--type-u6);
  line-height: var(--lh-snug);
  color: var(--ink-2);
}

.form-error.show,
.form-message.show { display: block; }
/* Both of these step DOWN to the ladder weight as well as the ladder hue —
   a success or an info message is not an interrupt. */
.form-error.success {
  border-left: var(--sem-rule-ladder) solid var(--sem-master);
}

.form-error.info {
  border-left: var(--sem-rule-ladder) solid var(--sem-entry);
}

.form-message__actions { display: block; margin-top: var(--space-2); }
.form-message__hint { color: var(--ink-4); }

/* --- verify-email step (step 2) ----------------------------------------- */
.verify-steps {
  margin: 0 0 var(--space-5);
  padding: 0;
  list-style: none;
  text-align: left;
  counter-reset: verify;
}

.verify-steps li {
  display: flex;
  gap: var(--space-3);
  padding: 9px 0;
  font-size: var(--type-u6);
  line-height: var(--lh-snug);
  color: var(--ink-3);
  counter-increment: verify;
}

.verify-steps li::before {
  content: counter(verify);
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line-2);
  border-radius: var(--r-full);
  font-family: var(--font-mono);
  font-size: var(--type-m3);
  color: var(--ink-4);
}

.verify-steps li strong { display: block; color: var(--ink-2); }

.email-providers {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.email-providers__btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 13px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--ink-2);
  font-size: var(--type-u6);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur-2) var(--ease-out);
}

.email-providers__btn:hover { background: var(--sf-2); }

.polling-indicator {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  margin-top: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--type-m3);
  letter-spacing: var(--track-data);
  color: var(--ink-4);
}

.countdown:empty { display: none; }

/* =============================================================================
   10 · PAYMENT SUMMARY
   -----------------------------------------------------------------------------
   Money is mono + tabular-nums so the amounts line up on the decimal and a
   changed digit does not reflow the row.
   ============================================================================= */
.pay-lines {
  margin-bottom: var(--space-5);
  border-top: 1px solid var(--line-1);
}

.pay-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 12px 0;
  border-bottom: 1px solid var(--line-1);
}

.pay-line__label {
  font-size: var(--type-u5);
  color: var(--ink-2);
}

.pay-line__value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--type-m1);
  letter-spacing: var(--track-data);
  color: var(--ink-1);
}

.pay-line__sub { color: var(--ink-4); }

.pay-line--total .pay-line__label { font-weight: 600; color: var(--ink-1); }
.pay-line--total .pay-line__value { color: var(--sem-master); }

.pay-footnote {
  margin-top: var(--space-3);
  font-size: var(--type-u6);
  line-height: var(--lh-snug);
  color: var(--ink-4);
}

.pay-footnote strong { color: var(--ink-2); }

/* =============================================================================
   11 · ENTRANCE                                                 (2026-08-20)
   -----------------------------------------------------------------------------
   ONE TIME. No loop, no ambient layer, nothing that repeats.

   That is not a promise, it is the language default: the keyframe below has no
   `infinite` and no `alternate`, and `animation-iteration-count` defaults to 1.
   Making this loop would mean DELETING a line rather than adding one. That
   matters here more than anywhere else in the codebase — commit 8b73fe1
   removed six infinite background animations from behind this very password
   field, and this section is the closest thing in the file to that mistake.

   WHY AN ANIMATION AND NOT A TRANSITION. individual.css's .ind-reveal pair
   needs js/individual.js to add `.is-in` on intersection. These pages have no
   page script and none is being added; a CSS animation with `forwards` fires
   once on load with nothing but the flag class below.

   DOUBLE-GATED, the same construction as individual.css:1106-1127:
     · no `html.auth-js`  -> not one rule matches -> a plain, readable,
       fully interactive document;
     · reduced motion     -> the media query excludes -> static at full
       opacity, no delay, no shift.
   The flag is set by a SYNCHRONOUS inline <head> script in
   components/auth_head.html. Synchronous and in <head> is the whole point: a
   class arriving from a deferred bundle would paint the card visible and then
   yank it to opacity 0 for a frame.

   THE TRANSFORM IS SAFE. `.auth-rise` lands only on `.auth-card`,
   `.state-page__card` and the three `.auth-aside` children. None of them is an
   ancestor of `.site-header` — which is a sibling of <main> and `position:
   fixed`, so a transformed ancestor would detach it from the viewport. There is
   deliberately no `will-change` anywhere: it is a live compositor promise, and
   public_base.html names it alongside transform for exactly this reason.
   ============================================================================= */
@keyframes auth-rise-in {
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: no-preference) {
  html.auth-js .auth-rise {
    opacity: 0;
    transform: translateY(10px);
    animation: auth-rise-in var(--dur-4) var(--ease-out) forwards;
    /* --stagger-block (100ms) rather than --stagger-unit (50ms): this is a
       short block run of at most four items, which is what site-tokens.css
       says that token is for. Worst case --i:3 is a 350ms lead and settles at
       850ms. The CARD is deliberately --i:1, never last: staging the primary
       action behind the decorative column would leave the sign-in button
       transparent-but-clickable for most of half a second, which is an
       interaction hazard rather than a cosmetic one. */
    animation-delay: calc(var(--stagger-lead) + var(--i, 0) * var(--stagger-block));
  }
}


/* =============================================================================
   12 · MOTION
   -----------------------------------------------------------------------------
   The transitions declared above, plus the entrance. A 400ms colour crossfade
   under a screen reader's focus ring is still motion.
   ============================================================================= */
@media (prefers-reduced-motion: reduce) {
  /* Belt and braces. The entrance is already excluded by its own
     no-preference query; this catches the case where a later edit moves it out
     of that query, and it is the only place in this file that says
     `animation`. */
  html.auth-js .auth-rise {
    opacity: 1;
    transform: none;
    animation: none;
  }

  .auth-input,
  .auth-btn,
  .auth-reveal,
  .auth-check,
  .auth-wallets__item,
  .email-providers__btn,
  .strength-fill,
  .state-next a {
    transition: none;
  }
}
