/* ==========================================================================
   NOTICE AUTH PAGES — the PlanetScale split (founder pick, 2026-08-16)
   --------------------------------------------------------------------------
   Serves BOTH public/login.html and public/signup.html. PlanetScale's own
   sign-in and sign-up are near-identical, so ours share one sheet and cannot
   drift apart.

   MEASURED DNA. Every number below was read off the pixels, not chosen.

   ⚠️ GROUND TRUTH MOVED (2026-08-17). The first pass measured
   docs/rebrand-refs/login-hunt/desktop-planetscale.webp — a thum.io
   screenshot-service render, i.e. a DOWNSCALED raster of the page. Its scale
   was off, and every number derived from it came out roughly 15-20% LARGE
   (heading 26 vs 22, inputs 40 vs 34, buttons 44 vs 34, form column 460 vs
   384). The founder put the two pages side by side and the drift was visible
   as "feels a bit different".
   The numbers below now come from a SingleFile capture of the live page
   (Sign_in__PlanetScale, 1440x1080), rendered in Chromium and read off the
   real layout boxes with getBoundingClientRect + a canvas pixel scan. A
   capture of the real DOM is ground truth; a screenshot service is not.
   RULE: measure DNA off a DOM capture or a full-scale screenshot. Never off a
   thumbnail service render — the numbers arrive plausibly wrong, which is the
   worst kind.

   ROUND 2 (2026-08-17). GROUND TRUTH is now the same-window capture PAIR
   taken by the founder minutes apart (Sign_in._Notice / Sign_in__PlanetScale,
   1440), rendered together and diffed. Round 1 got the desktop form column
   right — re-measured here, every rhythm metric in it came back 0.0 against
   their page. What it missed were two things a box measurement cannot see:
     1. THE WORDMARK WAS OPTICALLY 45% LARGE while measuring the same 32px
        box, because their word fills 69% of the lockup and ours filled 98%.
        Fixed by sizing the lockup to 24. See the note at .auth-rail__lock.
     2. THE TURNSTILE BADGE was spending 86px between the forgot row and the
        submit. That single delta WAS the whole "spacing is still not exactly
        PlanetScale" — nothing else in the column was off. Fixed by rendering
        the widget interaction-only. See the note at #bs-turnstile.
   Mobile was also re-derived here for the first time from a real capture
   (round 1's mobile numbers came from the discredited source): gutters and
   the lockup gap moved; the 44px/16px touch sizes did NOT, on purpose.
   Rig: scratchpad/auth-parity2/ — probe.js (layout boxes), ink.js (optical
   ink scan), sizes.js (candidate strip), parity.js (final side-by-side),
   turnstile-states.js (both widget states). Pick sheet:
   docs/rebrand-refs/login-hunt-2026-08-16.md

   DESKTOP ANATOMY (measured at 1440 viewport)
     hairline x            959/960  (1px, #EBEBEB, runs the full height)
     right rail width      480  ->  implemented as a fixed rail, which
                           reproduces the capture exactly at 1440 and degrades
                           by shrinking the left pane, not the brand.
     form column           x 288 .. 671, width 384  (their Tailwind max-w-sm)
     left gutters          288 + 384 + 288 = 960 exactly, i.e. the form is
                           CENTRED in the left pane.
     wordmark              lockup svg 32px tall, 200 wide. The MARK IS CENTRED
                           ON THE LINE: mark spans x 944..975, centre 959.5,
                           line at 959. The wordmark text flows right into the
                           rail. The line breaks y 492..588 (96px), i.e. a
                           page-coloured box padded 32px above and below the
                           lockup, and the gap tracks the rail's vertical
                           centre. Padding-based so the gap sizes itself and
                           cannot go stale.

   VERTICAL RHYTHM (layout boxes measured in the form column)
     base type          14px / line-height 21px (1.5) on the body. THIS is
                        what made ours read larger: we were on a 16px base.
     heading            313..346  22px / 600 / lh 33 / ls -0.34px (-.0155em)
     sub line           346..367  14px / lh 21, NO gap under the heading
     form top           399       32px under the sub line
     field box          399..463  = 4 lead + label 14 + 12 + input 34
     label              14px / 600 / lh 14, 12px above its input
     input              34px tall, 384 wide, radius 6, fill #FAFAFA,
                        border 1px #E1E1E1
     field gap          24px between field boxes
     forgot link        575..596  24px under the password input, 14px
     primary button     628..662  -> 34px tall, full 384 width, fill #2B2B2B,
                        32px under the forgot row
     divider            rule at 698, label 12px, 32px under the button
     oauth row          733..767  -> 34px tall, two buttons 184 wide, 16 gap,
                        label 14/600, 24px under the divider

   COLOURS (canvas-sampled from the capture)
     page #FFFFFF   input fill #FAFAFA   input border #E1E1E1
     hairline + divider #EBEBEB   their primary fill #2B2B2B

   MOBILE (390): the split stops existing. No hairline, no rail. Lockup sits
   top-left, form runs full width under it. Confirmed in mobile-planetscale.

   NOTICE TRANSLATION
     Ink #14141A. Neutral greys only: #FFF page, #FAFAFA fills,
     rgba(0,0,0,.08) hairlines. NO CREAM ANYWHERE. The warm cream hairlines
     the old auth pages carried (rgba(31,22,17,.12)) died in this pass.
     Face is the app register (Inter Variable, app-shell.css section 0), not
     Cal Sans: these are app surfaces, not marketing.

   THEME. Auth pages have no toggle, so prefers-color-scheme decides (the
   Attio behaviour the founder asked for). Dark uses the app's dark family
   from css/dark.css: page #20201F, card #262624, raised #302F2C, neutral
   ink ramp #F4F4F2 / #A6A6A2 / #8A8A86. EVERY light effect has an explicit
   dark counterpart below. Dark never inherits.

   WIRING IS FROZEN. This file is presentation only. Class names changed but
   every id, form, input and script hook the page JS reads is untouched:
   login keeps its single form and the /api/auth/login one-call contract
   (scripts/login-page-test.js), and both pages keep the Turnstile mount and
   the token in the POST body (scripts/turnstile-gate-test.js).
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. THE FACE. Same @font-face as app-shell.css section 0. THE TRAP: naming
   'Inter Variable' in a stack WITHOUT loading it renders system sans while a
   computed-style probe still reports the right family. The @font-face has to
   travel with the rule, so it is repeated here rather than assumed.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: 'Inter Variable';
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  src: url('/fonts/inter-variable-normal.woff2') format('woff2');
}

/* --------------------------------------------------------------------------
   1. TOKENS
   -------------------------------------------------------------------------- */
:root {
  --a-page:        #FFFFFF;
  --a-ink:         #14141A;
  --a-ink-mute:    #6B6B72;
  --a-ink-soft:    #8A8A90;
  --a-hair:        rgba(0, 0, 0, .08);
  --a-rule:        #EBEBEB;   /* the long vertical hairline (sampled) */
  --a-field:       #FAFAFA;
  --a-field-line:  #E1E1E1;
  --a-field-focus: rgba(0, 0, 0, .28);
  --a-btn:         #14141A;
  --a-btn-hover:   #26262E;
  --a-btn-ink:     #FFFFFF;
  --a-ghost:       #FFFFFF;
  --a-ghost-hover: #FAFAFA;
  --a-link:        #14141A;
  --a-err-bg:      #FEF2F2;
  --a-err-line:    #FECACA;
  --a-err-ink:     #DC2626;
  --a-warn-bg:     #FFFBEB;
  --a-warn-line:   #FDE68A;
  --a-warn-ink:    #92400E;
  --a-rail:        480px;     /* measured: 1440 - 960 */
  --a-form:        384px;     /* measured form column width */
  --a-autofill:    #FAFAFA;   /* what the field fill must stay under autofill */
}

@media (prefers-color-scheme: dark) {
  :root {
    --a-page:        #20201F;
    --a-ink:         #F4F4F2;
    --a-ink-mute:    #A6A6A2;
    --a-ink-soft:    #8A8A86;
    --a-hair:        rgba(255, 255, 255, .10);
    --a-rule:        rgba(255, 255, 255, .10);
    --a-field:       #262624;
    --a-field-line:  rgba(255, 255, 255, .09);
    --a-autofill:    #262624;
    --a-field-focus: rgba(255, 255, 255, .32);
    --a-btn:         #F4F4F2;
    --a-btn-hover:   #FFFFFF;
    --a-btn-ink:     #20201F;
    --a-ghost:       #262624;
    --a-ghost-hover: #302F2C;
    --a-link:        #F4F4F2;
    --a-err-bg:      #2A1F1F;
    --a-err-line:    rgba(248, 113, 113, .30);
    --a-err-ink:     #F87171;
    --a-warn-bg:     #2A241A;
    --a-warn-line:   rgba(253, 230, 138, .26);
    --a-warn-ink:    #E7C77A;
  }
}

/* --------------------------------------------------------------------------
   2. SHELL. Desktop is a two column grid: flexible form pane, fixed 468 rail.
   The rule between them is the rail's left border, so it is exactly one
   hairline and always full height.
   -------------------------------------------------------------------------- */
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html { background: var(--a-page); }

body {
  font-family: 'Inter Variable', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--a-page);
  color: var(--a-ink);
  /* Measured base. The first pass sat on the 16px UA default, which scaled
     every unstyled descendant up and is most of what read as "bigger". */
  font-size: 14px;
  line-height: 1.5;
  min-height: 100vh;
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }

.auth-split {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--a-rail);
}

/* Form pane. The form is centred in it, which reproduces the measured
   256 / 460 / 256 at a 1440 viewport without hardcoding the gutters. */
.auth-pane {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 56px 40px;
  min-width: 0;
}

.auth-col {
  width: 100%;
  max-width: var(--a-form);
}

/* Brand rail. Its left border IS the hairline. */
.auth-rail {
  position: relative;
  border-left: 1px solid var(--a-rule);
}

/* The lockup sits ON the line: the mark's centre lands on the border.
   The Notice lockup viewBox is 3.047 3.047 132.084 25.922; the mark spans
   x 3.047..28.969, so its centre is 9.81% across the lockup's width. Shift
   the box left by that fraction and the mark is centred on the hairline,
   exactly as PlanetScale sets theirs.
   The page-coloured background with 32px of vertical padding (their literal
   token, measured off their lockup container: y492 h96 padding 32px 0) is
   what BREAKS the line: the gap sizes itself to the lockup and can never go
   stale. With our 24px lockup that reads an 88px break against their 96 —
   8px on a hairline gap, below noticing, and it keeps their padding token
   rather than inventing one. */
.auth-rail__lock {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translate(-9.81%, -50%);
  padding: 32px 0;
  background: var(--a-page);
  color: var(--a-ink);
  display: block;
  line-height: 0;
}
/* 24px, not 32 — an OPTICAL match, not a bounding-box one (founder, round 2:
   "the Notice wordmark is now TOO BIG").
   The round-1 pass matched the BOX: both lockups measured exactly 32px tall,
   which is why it read as done. A dpr-2 ink scan of the two captures showed
   the boxes agree and the INK does not:
       PlanetScale   mark ink 32px   word ink 22px   coverage .099
       Notice @32    mark ink 32px   word ink 32px   coverage .141
   Their word fills 69% of the lockup box; ours filled 98%, so our word ran
   45% larger and carried 42% more ink. The mark:word ratio is baked into
   each SVG's geometry, so no CSS height can fix the ratio — only the overall
   size. Rendering ours at 32/28/26/24/22 beside theirs, 24 is where the
   optical mass lands: our word ink 24 vs their 22 (+9%), and our smaller
   band is offset by a solid-blob mark against their cut-out circle, which
   carries more ink per unit height.
   RULE: a wordmark matches when its INK matches. Two lockups with identical
   bounding boxes can differ by half again in what the eye reads.
   Rig: scratchpad/auth-parity2/ (ink.js, sizes.js). */
.auth-rail__lock svg { height: 24px; width: auto; display: block; }

/* The mobile lockup, top-left. Hidden on desktop where the rail owns it. */
.auth-brand { display: none; }
.auth-brand svg { height: 24px; width: auto; display: block; color: var(--a-ink); }

/* --------------------------------------------------------------------------
   3. TYPE. Measured: heading 22/600/lh33, sub 14/lh21, NO gap between them.
   -------------------------------------------------------------------------- */
.auth-h {
  font-size: 22px;
  line-height: 1.5;
  font-weight: 600;
  letter-spacing: -.0155em;
  color: var(--a-ink);
}

.auth-sub {
  font-size: 14px;
  line-height: 1.5;
  color: var(--a-ink-mute);
}
.auth-sub a {
  color: var(--a-link);
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--a-hair);
}
.auth-sub a:hover { text-decoration-color: currentColor; }

/* 32px from the sub line to the form (measured 367 -> 399). */
.auth-form { margin-top: 32px; }

/* --------------------------------------------------------------------------
   4. FIELDS. label 14/600/lh14, 12px to a 34px input, 24px between groups.
   The 4px padding-top is the label's measured lead inside its field box
   (their box is 64 = 4 + 14 + 12 + 34).
   -------------------------------------------------------------------------- */
.auth-field { padding-top: 4px; margin-bottom: 24px; }
/* THE LAST FIELD OWNS NO BOTTOM GAP — the submit's own 32px is the measured
   gap above it, and a field's 24 stacking under that read 56 (visible as a
   loose button on login, where the "Forgot password?" link lives INSIDE the
   password field).
   `:last-of-type` cannot do this job: it is by ELEMENT type, and both pages
   put more divs after the last field (the error box, the Turnstile mount), so
   the field was never the last div and kept its margin. `:not(:has(~ X))` is
   "no later sibling of this class", which is what was actually meant. */
.auth-field:not(:has(~ .auth-field)) { margin-bottom: 0; }

.auth-form label {
  display: block;
  font-size: 14px;
  line-height: 14px;
  font-weight: 600;
  color: var(--a-ink);
  margin-bottom: 12px;
}

.auth-labelrow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}
.auth-labelrow label { margin-bottom: 0; }

.auth-form input {
  width: 100%;
  height: 34px;
  border: 1px solid var(--a-field-line);
  border-radius: 6px;
  padding: 0 12px;
  font: inherit;
  font-size: 14px;
  background: var(--a-field);
  color: var(--a-ink);
  outline: none;
  transition: border-color .12s ease, box-shadow .12s ease;
}
.auth-form input::placeholder { color: var(--a-ink-soft); }

/* AUTOFILL. Chrome/Safari paint a UA-forced pale blue over an autofilled
   field, and that blue is not reachable through `background-color` at any
   specificity — the only thing that covers it is a large inset box-shadow,
   and the text needs -webkit-text-fill-color because `color` loses too.
   The founder saw the blue on his own saved login. Both themes get their own
   fill: a light effect never inherits, and here the dark page would otherwise
   show a white-ish field. `transition: background-color 0s 9999s` is the
   standard defeat for the fade-in Chrome runs on top. */
.auth-form input:-webkit-autofill,
.auth-form input:-webkit-autofill:hover,
.auth-form input:-webkit-autofill:focus,
.auth-form input:-webkit-autofill:active {
  -webkit-box-shadow: 0 0 0 1000px var(--a-autofill) inset;
  box-shadow: 0 0 0 1000px var(--a-autofill) inset;
  -webkit-text-fill-color: var(--a-ink);
  caret-color: var(--a-ink);
  transition: background-color 0s 9999s;
}
/* Focus must still read as focus while autofilled: restate the ring OUTSIDE
   the 1000px inset fill, or the shadow above swallows it. */
.auth-form input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--a-autofill) inset, 0 0 0 3px var(--a-hair);
  box-shadow: 0 0 0 1000px var(--a-autofill) inset, 0 0 0 3px var(--a-hair);
}
.auth-form input:focus {
  border-color: var(--a-field-focus);
  box-shadow: 0 0 0 3px var(--a-hair);
}

/* Show/hide password, set inside the field's right edge like PlanetScale's
   "show". Kept as a plain text button so the JS hook is untouched.
   THE TRAP (caught by opening the step-2 screenshot, not by any probe): the
   relative box must be the INPUT's own wrapper, never the whole .auth-field.
   A field contains its label, so top:50% against the field centres the
   button on the label/input BLOCK and it rides up over the input's top edge. */
.auth-pwbox { position: relative; }
.auth-pwbox input { padding-right: 62px; }
.auth-showpw {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  border: 0;
  background: none;
  padding: 4px;
  font-size: 12px;
  color: var(--a-ink-mute);
}
.auth-showpw:hover { color: var(--a-ink); }

.auth-minor {
  display: inline-block;
  margin-top: 24px;
  font-size: 14px;
  color: var(--a-ink-mute);
}
.auth-minor:hover { color: var(--a-ink); }

/* --------------------------------------------------------------------------
   5. BUTTONS. Primary is the app's dark ink fill, 44px, full width.
   PlanetScale's dark Sign in translates straight across.
   -------------------------------------------------------------------------- */
.auth-submit {
  width: 100%;
  height: 34px;
  margin-top: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: var(--a-btn);
  color: var(--a-btn-ink);
  font-size: 14px;
  font-weight: 600;
  transition: background .14s ease, opacity .14s ease;
}
.auth-submit:hover { background: var(--a-btn-hover); }
.auth-submit:disabled { opacity: .6; cursor: wait; }

/* FLEX, not a fixed 2-col grid, because the number of VISIBLE buttons is
   decided at runtime: each provider ships [hidden] and reveals itself only
   when its server half is configured (Google always, then Microsoft, then
   Apple when the founder's developer account lands). display:none children
   take no flex slot, so the row self-balances: two buttons split in half,
   three split in thirds. A `1fr 1fr` grid instead dropped the third button
   onto a second row at half width. */
.auth-oauth-row {
  display: flex;
  gap: 16px;
}
.auth-oauth-row > .auth-oauth {
  flex: 1 1 0;
  /* Lets the label's ellipsis engage instead of forcing the row wider. */
  min-width: 0;
}

.auth-oauth {
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 12px;
  border: 1px solid var(--a-field-line);
  border-radius: 6px;
  background: var(--a-ghost);
  color: var(--a-ink);
  font-size: 14px;
  font-weight: 600;
  transition: background .14s ease, border-color .14s ease;
}
/* DISPLAY BEATS HIDDEN. `.auth-oauth` sets an author `display`, and an author
   rule outranks the UA's `[hidden] { display: none }` whatever the
   specificity — so without this restatement every provider button renders
   even while its JS has set `.hidden = true`, and an unconfigured provider
   offers a button that 503s. Same trap as `.ms-page svg[hidden]` and
   `.set-row[hidden]` elsewhere in the app. Any new author display on this
   class needs this line to stay. */
.auth-oauth[hidden] { display: none !important; }
.auth-oauth:hover { background: var(--a-ghost-hover); border-color: var(--a-field-focus); }
.auth-oauth svg { width: 16px; height: 16px; flex-shrink: 0; }
.auth-oauth .auth-oauth__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Apple's glyph is a solid fill, so it has to follow the ink in dark mode.
   A light effect never inherits: this is its explicit dark counterpart. */
.auth-oauth .auth-glyph-ink { fill: var(--a-ink); }

.btn-spinner {
  width: 14px;
  height: 14px;
  border: 2px solid rgba(255, 255, 255, .32);
  border-top-color: #fff;
  border-radius: 50%;
  animation: auth-spin .6s linear infinite;
  display: inline-block;
}
@media (prefers-color-scheme: dark) {
  .btn-spinner { border-color: rgba(0, 0, 0, .28); border-top-color: #20201F; }
}
@keyframes auth-spin { to { transform: rotate(360deg); } }

/* --------------------------------------------------------------------------
   6. DIVIDER. Measured: 32px above the rule, 24px below it to the oauth row.
   -------------------------------------------------------------------------- */
.auth-sep {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 32px 0 24px;
  color: var(--a-ink-mute);
  font-size: 12px;
  /* Their divider box is 15px tall, and the 32/24 margins are measured
     AGAINST that box. On a 1.5 line-height ours came out 18 and pushed the
     oauth row 3px low. Pin the box, not the margins. */
  line-height: 15px;
}
.auth-sep::before,
.auth-sep::after { content: ''; flex: 1; height: 1px; background: var(--a-rule); }

/* --------------------------------------------------------------------------
   7. MESSAGES
   -------------------------------------------------------------------------- */
.error-box {
  display: none;
  align-items: center;
  gap: 8px;
  margin-top: 16px;
  padding: 10px 12px;
  border: 1px solid var(--a-err-line);
  border-radius: 6px;
  background: var(--a-err-bg);
  color: var(--a-err-ink);
  font-size: 13.5px;
  font-weight: 500;
}
.error-box.is-on { display: flex; }
.error-box svg { flex-shrink: 0; }

.session-banner {
  display: none;
  margin-bottom: 20px;
  padding: 10px 12px;
  border: 1px solid var(--a-warn-line);
  border-radius: 6px;
  background: var(--a-warn-bg);
  color: var(--a-warn-ink);
  font-size: 13.5px;
}
.session-banner.is-on { display: block; }

/* The locked email chip on signup step 2. */
.email-locked {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 24px;
  padding: 10px 12px;
  border: 1px solid var(--a-field-line);
  border-radius: 6px;
  background: var(--a-field);
  font-size: 14px;
  color: var(--a-ink-mute);
}
.email-locked > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.email-locked button {
  border: 0;
  background: none;
  padding: 0;
  font-size: 13px;
  font-weight: 500;
  color: var(--a-ink);
  flex-shrink: 0;
}
.email-locked button:hover { text-decoration: underline; }

/* Turnstile mount. FUNCTIONAL — the widget carries the token the auth POST
   needs (scripts/turnstile-gate-test.js), so it is styled, never removed.

   The widget renders appearance:'interaction-only' (founder, round 2), so in
   the normal case it occupies ZERO height and the owner never sees it. The
   measured cost of the old always-visible badge was exact: it pushed
   forgot-row -> submit from their 32px to 118px, an 86px intrusion, and that
   single delta WAS the whole "spacing still not exactly PlanetScale" — every
   other metric in the column already measured 0.0 against their capture.

   THE GATING IS BY OCCUPIED HEIGHT, NOT BY :empty. Interaction-only still
   injects a child element, so `:not(:empty)` matches even while the widget is
   invisible and would reserve a 24px gap around nothing. The page JS adds
   .is-live only when the mount actually takes up space (ResizeObserver, with
   Turnstile's before/after-interactive callbacks as a belt), so:
     invisible -> no class, no margin, submit keeps its measured 32px
     interactive -> 24/24 around the widget, which is the balanced rhythm
   The mount is a direct previous sibling of .auth-submit on BOTH pages —
   keep it that way or this spacing silently reverts. */
#bs-turnstile { margin: 0; }
#bs-turnstile.is-live { margin-top: 24px; }
#bs-turnstile.is-live + .auth-submit { margin-top: 24px; }

.auth-legal {
  margin-top: 32px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--a-ink-soft);
}
.auth-legal a { color: var(--a-ink-mute); text-decoration: underline; text-underline-offset: 2px; }
.auth-legal a:hover { color: var(--a-ink); }

/* Signup steps. Presentation only: the JS toggles .is-active exactly as before. */
.step { display: none; }
.step.is-active { display: block; }

/* --------------------------------------------------------------------------
   8. MOBILE. Below 900 the split stops existing: no rail, no hairline,
   lockup top-left, form full width. Matches mobile-planetscale.
   Inputs go to 16px so iOS cannot auto-zoom on focus.
   -------------------------------------------------------------------------- */
@media (max-width: 899.98px) {
  .auth-split { display: block; min-height: 0; }
  .auth-rail { display: none; }

  /* 32px gutters and a 32px top, measured off THEIR page rendered at 390 in
     round 2. The old 28/24 came from the discredited thum.io source (see the
     GROUND TRUTH note at the top) and was never re-derived when the desktop
     numbers were; their mobile reads gutter 32, lockup y 32. */
  .auth-pane {
    display: block;
    padding: 32px 32px 40px;
    min-height: 100vh;
    min-height: 100dvh;
  }
  /* One form width across the whole product: the measured 384. The old 480
     here was 25% wider than the desktop column, which read as two different
     forms on a tablet. */
  .auth-col { max-width: var(--a-form); margin: 0 auto; }

  /* 64px lockup -> heading, measured off their 390 render (lockup bottom 64,
     heading top 128). Was 44. */
  .auth-brand { display: block; margin-bottom: 64px; }

  /* THEIR mobile heading is 18px/27; ours stays 22px/33 DELIBERATELY — that
     is a hierarchy call, not a measurement, and it is the founder's to make.
     Flagged in the round-2 report rather than changed silently. */
  .auth-h { font-size: 22px; }

  /* Touch targets stay 44px — the desktop 34 is a mouse size. 16px on the
     input is the iOS no-auto-zoom floor and is load-bearing.
     THEIR mobile input is 34px at a 14px font, which trips iOS zoom-on-focus.
     We do NOT match it: parity stops where it would cost the owner a working
     tap target. Also flagged in the round-2 report. */
  .auth-form input { height: 44px; font-size: 16px; }
  .auth-submit { height: 44px; }
  .auth-oauth { height: 44px; }
  .auth-oauth-row { gap: 12px; }
}

@media (max-width: 380px) {
  /* One per line on the narrowest phones — three 16px glyphs + labels do not
     fit across. (Was grid-template-columns: 1fr, dead since the row moved to
     flex above.) */
  .auth-oauth-row { flex-direction: column; }
}

/* Narrow desktop: keep the rail from eating the form pane. */
@media (min-width: 900px) and (max-width: 1180px) {
  :root { --a-rail: 340px; }
  .auth-pane { padding: 56px 32px; }
}

@media (prefers-reduced-motion: reduce) {
  .btn-spinner { animation-duration: 1.6s; }
  .auth-form input,
  .auth-submit,
  .auth-oauth { transition: none; }
}
