/* Shared stylesheet for EVERY standalone page in public/. Created #W40, completed #W41.
   These pages are NOT built from site/template.html - they are standalone by design so they render
   even if the build is broken - so this file is where their one copy of the brand lives.

   Three MODES, because these pages are not one kind of page:
     DOCUMENT  (default)      /privacy-policy  /terms  /delete-account
                              white ground, .site-head + main{680px} + .site-foot, prose rhythm
     SHELL     body.auth-action /auth/action (#W68)
               body.handoff     /stripe-return (#W69)  /stripe-refresh  /review (#W70)
                              ONE phone-first composition: the page fills the viewport (band ->
                              centred content -> footer at the bottom edge), a 460px measure
     FORM      body.form-page /reset-password
                              document mode with a 420px form measure and no footer

   🔴 THE PANEL MODE (body.panel: one centred card on Deep Ocean, NO header and NO footer on purpose,
   "a nav row is an invitation to wander off between Stripe and Northy") WAS RETIRED IN #W70 BY
   RULING: his verdict on #W69's shell on a phone was "not good looking", and the ruling put every
   hand-off page and /auth/action into one composition WITH the band and the footer. That overturns
   the no-header/no-footer ruling for these pages; it does not reach the FORM page. The retired block,
   comments included, is in WEB_HISTORY.md (#W70) - its measured focus-ring and inverted-button
   findings are about a Deep Ocean ground no page here uses any more.

   Loaded as a second request per page. firebase.json sets `max-age=0, must-revalidate` on `**`
   with no asset exception, so it revalidates every visit by design. Do not "optimise" that by
   inlining this back into each page - that recreates the three-way duplication W40 removed. */

:root {
  --deep-ocean:    #022C50;  /* primary ink, dark bands */
  --ice-blue:      #B3DEF8;  /* links and mark ON DARK */
  --north-blue:  #0F4C81;  /* THE action colour - links on light */
  --surface:       #FFFFFF;
  --muted:         #48607A;  /* body copy on white: 6.50 */
  --muted-on-dark: #9FBCD4;  /* on Deep Ocean: 7.18 */
  --border-strong: #6B8CAA;  /* input borders on white: 3.53, clears 1.4.11's 3.0 */
  --card-border:   #D3E6F5;  /* the spinner track */
}

/* Measured pairs used here, all >= 4.5 except where noted as headline-scale:
   #022C50 on #FFFFFF 14.20 - #48607A on #FFFFFF 6.50 - #0F4C81 on #FFFFFF 8.85
   #FFFFFF on #022C50 14.20 - #B3DEF8 on #022C50 9.96 - #9FBCD4 on #022C50 7.18 */

* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: 'Poppins', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: var(--deep-ocean);
  background: var(--surface);
}

/* 1.4.10 Reflow. Carried forward verbatim from the three pages this file replaces - it is a
   measured finding, not a guess. An email address is one unbreakable token, and a list item
   content box is ~40px narrower than a paragraph one, so on a narrow viewport it cannot wrap even
   onto a line of its own. Measured on /terms at 256px: the token is 193px, the list content box
   168px. THIS DOES NOT RESCUE A 320px FAILURE, BECAUSE THERE IS NOT ONE - 320px, the criterion
   width, passes on every page. It buys headroom below the criterion, which is where a small phone
   with enlarged text lands. */
body { overflow-wrap: break-word; }

/* ---- header band ---- */

.site-head {
  background: var(--deep-ocean);
  padding: 20px 24px;
}
/* min-height is the 44px TARGET FLOOR (#W64), not a look: measured at 375px this link was
   96.6 x 27, which clears 2.5.8 AA's 24 and misses the house bar these pages share with the app.
   The mark and wordmark are centred in the taller box, so nothing moves visually except the band's
   own height. */
.site-head .brand {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  gap: 10px;
  text-decoration: none;
  color: var(--ice-blue);   /* the inlined mark is fill="currentColor" and inherits this */
}
.site-head .brand-mark { display: block; flex-shrink: 0; }
.site-head .wordmark {
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.2px;
  color: #FFFFFF;
}
.site-head .brand:hover .wordmark { text-decoration: underline; }

/* ---- content ---- */

main {
  max-width: 680px;
  margin: 0 auto;
  padding: 48px 24px 16px;
}
h1 {
  font-size: clamp(26px, 5vw, 34px);
  font-weight: 700;          /* NOT 800 - Poppins stops at 700 */
  letter-spacing: -0.5px;
  color: var(--deep-ocean);
  margin-bottom: 8px;
}

/* 🔴 18px, NOT the 19px the brief specified, AND THE PREMISE FOR 19 WAS BACKWARDS (#W40).
   The brief asked for 19 because "Poppins has a smaller x-height than Manrope at the same px, so
   18px would read smaller than it did". Measured in a headless browser at 200px, with the faces
   confirmed loaded via document.fonts.check() first:

       Poppins 600   x-height 0.560 of font-size   cap 0.705   x/cap 0.794
       Manrope 700   x-height 0.540 of font-size   cap 0.720   x/cap 0.750

   Poppins' x-height is 3.7% LARGER, and its x-height relative to its own cap height is larger too.
   So at 18px this heading already renders a 10.08px x-height against the old 9.72px - bigger than
   what it replaces - and 19px would overshoot by 9.5%.
   🟡 The first attempt at this measurement returned IDENTICAL numbers for both faces and identical
   string widths, because neither webfont had loaded and both fell back to the same default. The
   control that caught it is the fallback width: a face that has not loaded measures the same as
   `NoSuchFontXYZ`. Check document.fonts.check() before believing a font metric. */
h2 {
  font-size: 18px;
  font-weight: 600;
  color: var(--deep-ocean);
  margin-top: 36px;
  margin-bottom: 10px;
}
p, li {
  font-size: 16px;           /* NOT 15px - W37 retired that size */
  line-height: 1.7;
  color: var(--muted);
}
strong { color: var(--deep-ocean); font-weight: 600; }
h1 + p { margin-bottom: 32px; }
p + p, ul + p, ol + p, p + ul, p + ol { margin-top: 14px; }
ul, ol { padding-left: 22px; }
li { margin-bottom: 6px; }
.updated {
  color: var(--muted);
  font-size: 14px;
  margin-bottom: 40px;
}

a {
  color: var(--north-blue);
  font-weight: 500;
  text-decoration: none;
}
a:hover { text-decoration: underline; }

/* Focus. No gate covers focus states - W37 shipped an invisible focus ring because the section
   under it changed - so both grounds are stated and measured here. On the white page the ring is
   North Blue (8.85 on #FFFFFF). Inside the two Deep Ocean bands the ring must switch, because
   #0F4C81 on #022C50 is 1.55 and would vanish. */
a:focus-visible {
  outline: 2px solid var(--north-blue);
  outline-offset: 2px;
  border-radius: 2px;
}
.site-head a:focus-visible,
.site-foot a:focus-visible {
  outline-color: var(--ice-blue);   /* 9.96 on Deep Ocean */
}

/* ---- footer band ---- */

.site-foot {
  background: var(--deep-ocean);
  margin-top: 64px;
  padding: 32px 24px 40px;
}
.site-foot nav {
  max-width: 680px;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: 14px 28px;
}
/* 🔴 THE FOOTER LINKS WERE 25px TALL, AND THE ROW'S `gap` IS WHAT MAKES THE FIX FREE (#W64).
   They cleared 2.5.8 AA (24) by one pixel and missed the 44 the house asks for. Growing the LINK
   rather than the row keeps the 14px/28px gap doing the separating, so the visible rhythm is
   unchanged and only each target's hit box grows. */
.site-foot a {
  color: var(--ice-blue);
  font-size: 16px;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
.site-foot [aria-current="page"] {
  color: var(--muted-on-dark);
  font-weight: 400;
}

/* ============================================================================
   FORM PAGE - /reset-password                                     (added #W41)
   Document mode (header + main), but a form measure rather than a prose one, and no footer:
   this page is reached mid-flow from an email.
   ============================================================================ */

body.form-page main { max-width: 420px; padding: 40px 24px 64px; }
body.form-page h1 { font-size: clamp(24px, 5vw, 30px); margin-bottom: 10px; }
body.form-page p { margin-bottom: 20px; }

label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--deep-ocean);
  margin-bottom: 6px;
}
input[type="password"] {
  width: 100%;
  font-family: inherit;
  font-size: 16px;
  padding: 12px 14px;
  /* WCAG 1.4.11: an input's boundary is a UI component and wants 3:1 against what is behind it.
     This is now the TOKEN, not a copy of it - the page loads this file, so the duplicated hex that
     made #7B8288 a landmine for four sessions is gone. --border-strong on white measures 3.53.
     (The retired #7B8288 measured 3.90 on white; both clear the bar. The point is that there is now
     ONE copy of the value, not that 3.53 is better than 3.90.) */
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface);
  color: var(--deep-ocean);
  margin-bottom: 14px;
}
/* 🔴 A REAL RING, DRAWN - not `outline: none` plus a border tint, which is the weakest thing that
   can still be called a focus indicator on a form with two identical-looking password fields.
   WCAG 2.2 AA 2.4.7 wants focus visible; 1.4.11 wants the indicator itself at 3:1. The offset keeps
   the ring clear of the 1px border, so focus is a change of SHAPE and not only of colour. North
   Breeze measures 8.86 on white, up from the retired #286492's 6.31. The border colour still changes
   as a second, redundant signal.
   🔴 `:focus` AND `:focus-visible`, deliberately. A text input must show its ring to a MOUSE user
   too - `:focus-visible` alone is a heuristic, and on a two-password form the ring is the only thing
   saying which box you are typing into. The pair costs nothing and removes the question. */
input[type="password"]:focus,
input[type="password"]:focus-visible {
  outline: 2px solid var(--north-blue);
  outline-offset: 2px;
  border-color: var(--north-blue);
}
.rules-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--deep-ocean);
  margin: 0 0 6px;
}
ul.rules { list-style: none; margin: 0 0 20px; padding: 0; }
/* 🔴 NEUTRAL BEFORE ANYTHING IS TYPED (2026-08-17, #W19). This defaulted to a red cross, so a person
   arriving at the page - having done nothing - was shown THREE ERRORS. Three requirements rendering
   as three failures is a claim about them that is not true.
   Both apps solve it by rendering no rows at all until the field is non-empty (Android
   AuthScreens.kt:832, iOS SignUpView.swift:99). THE WEB DELIBERATELY DOES NOT COPY THAT: this list
   is the input's aria-describedby, so hiding it would strip the accessible description at the one
   moment it is most useful - before typing, which is when a description is read.
   So the rows stay and the MARKER carries the state. A bullet has no valence: it says
   "requirement". --muted on white measures 6.50 (the retired #5C6268 was 5.46 on Harbour). */
ul.rules li {
  font-size: 13px;
  line-height: 1.9;
  color: var(--muted);
  padding-left: 22px;
  position: relative;
}
ul.rules li::before {
  content: "\2022";
  position: absolute;
  left: 0;
  color: var(--muted);
  font-weight: 600;
}
/* Only once text exists is an unmet rule a FAILING rule. `typed` is toggled on the length of the
   CURRENT value, never on "has ever typed" - clearing the field returns you to having done nothing
   wrong, and the red has to go with it. #B3413B on white measures 5.62. */
ul.rules.typed li::before { content: "\2717"; color: #B3413B; }
/* 🔴 ORDERED AFTER .typed DELIBERATELY. These two selectors have EQUAL specificity, so source order
   is the only thing making met beat unmet. Moving this block above the one over it turns every
   satisfied rule red, with no warning from anything. #1F6F45 on white measures 6.14. */
ul.rules li.met { color: #1F6F45; }
ul.rules li.met::before { content: "\2713"; color: #1F6F45; }

button[type="submit"] {
  width: 100%;
  font-family: inherit;
  font-size: 16px;
  font-weight: 600;
  color: #FFFFFF;
  background: var(--north-blue);     /* matches .btn-primary on the homepage; label 8.86 */
  border: none;
  border-radius: 12px;
  padding: 14px;
  cursor: pointer;
}
button[type="submit"]:hover:not(:disabled) { background: #083A66; }   /* .btn-primary's hover, 11.62 */
button[type="submit"]:focus-visible {
  outline: 2px solid var(--north-blue);
  outline-offset: 2px;
}
/* 🔴 AN EXPLICIT DISABLED PAIR, NOT `opacity: 0.5`, AND THE OLD NUMBER WAS WORSE THAN IT LOOKED.
   Group opacity fades the FILL AND THE LABEL TOGETHER over the page, so the label-to-fill ratio
   collapses rather than merely dimming: measured, the shipped version rendered a #C7D8E5 label on a
   #90B2CC fill = 1.53 on the Harbour ground, and 1.47 once the ground went white. That was arguably
   exempt under 1.4.3's inactive-component rule - but this button's disabled state also carries the
   text "Resetting...", a PROGRESS message, which is not obviously inactive.
   --muted with a white label is 6.50 and needs no exemption to defend. */
button[type="submit"]:disabled {
  background: var(--muted);
  color: #FFFFFF;
  cursor: default;
}
.error { font-size: 14px; color: #B3413B; margin: -6px 0 14px; }    /* 5.62 on white */
/* 🟡 The spinner TRACK is decoration and is not held to 3:1: --card-border on white is 1.28 (the
   retired #C7CBCE was 1.44 on Harbour - the same class, not a regression). What conveys "loading"
   is the MOVING arc, which is North Blue at 8.86. Same exclusion the card borders on the homepage
   already carry: a surface is either a boundary, or a wash behind something that already passes. */
.spinner {
  width: 28px;
  height: 28px;
  border: 3px solid var(--card-border);
  border-top-color: var(--north-blue);
  border-radius: 50%;
  margin: 40px auto;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.center { text-align: center; }
.hidden { display: none; }

/* ============================================================================
   AUTH ACTION PAGE - /auth/action                                  (added #W68)
   The house handler for EVERY Firebase auth email link (reset, verify, change,
   recover). Document mode - header band, a form measure, the site's footer.

   🔴 EVERY RULE BELOW IS SCOPED TO body.auth-action, AND THAT IS LOAD-BEARING.
   Seven pages share this stylesheet, and the form rules above are UNSCOPED:
   `input[type="password"]` and `button[type="submit"]` already reach this page
   on their own. Restyling those selectors to suit this page would silently
   restyle /reset-password, which is a different page with its own measured
   decisions. So this block only ADDS, and overrides only under its own class.
   ============================================================================ */

/* 🔴 ONE PHONE-FIRST COMPOSITION (#W70, ruled after his verdict on #W69's shell at 390x844: content
   hugging the left edge, ending a third of the way down, a white gap, then the footer band).
   (a) The page FILLS THE VIEWPORT: a flex column, band -> main grows -> footer, so the footer's
   bottom is the viewport's bottom and a short page shows no white gap and no scroll. 100vh first as
   the fallback for a browser without dvh; 100dvh is the visible height on a phone whose URL bar
   collapses, which 100vh overshoots.
   (b) main is centred VERTICALLY in its region at every width, and its text is centred at <= 600px.
   Above 600 the block keeps the 460 measure, centred as a column, with its text at the start.
   `width: 100%` is load-bearing: auto side margins on a flex item override stretch, and without it
   main shrink-wraps its content. */
body.auth-action,
body.handoff {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
body.auth-action main,
body.handoff main {
  flex: 1 0 auto;
  width: 100%;
  max-width: 460px;
  margin: 0 auto;
  padding: 32px 24px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
body.auth-action h1,
body.handoff h1 { font-size: clamp(24px, 5vw, 30px); margin-bottom: 16px; }
/* Kept at 20 for the paragraphs INSIDE /auth/action's form (the rules label, the error), which this
   pass does not move; the panel-level rhythm below is 16/24. */
body.auth-action p,
body.handoff p { margin-bottom: 20px; }
body.auth-action .panel > p,
body.handoff main > p { margin-bottom: 24px; }
body.auth-action .panel > :last-child,
body.handoff main > :last-child { margin-bottom: 0; }
@media (max-width: 600px) {
  body.auth-action main,
  body.handoff main { text-align: center; }
}
/* 🟡 THE ONE EXCEPTION TO (b): /auth/action's password FORM stays start-aligned at every width.
   Centred labels over full-width inputs detach each label from its box, and the rules list draws
   its marker at `left: 0` (ul.rules li::before), so centred rule text would float away from its
   bullet. The heading and the sentence above the form centre like every other panel. */
body.auth-action form { text-align: left; }

/* The lockup is a raster, so its box is declared in both dimensions to keep the band's height
   stable before the image loads. 84x30 is the SVG viewBox's own ratio (#S13 derives the same pair);
   the artwork carries its clear space inside those pixels. */
body.auth-action .brand-lockup,
body.handoff .brand-lockup { display: block; height: 30px; width: 84px; }

/* 🔴 THE HOUSE WAIT: A BAR, NOT A BARE SPINNER. An indeterminate bar says "still working" without
   claiming progress it cannot know, and it carries a role for assistive tech - a spinner div is
   invisible to one.
   🔴 AND IT STOPS UNDER REDUCED MOTION, rather than being exempted by name. The a11y gate flags any
   element whose computed animation-name is not "none" with a duration above zero; .spinner is
   exempt because freezing it would remove the ONLY loading signal. This bar has a second signal -
   the sentence above it - so it does not need the exemption and does not take one. */
body.auth-action .wait-bar {
  height: 4px;
  border-radius: 999px;
  background: var(--card-border);
  overflow: hidden;
  margin: 28px 0 0;
}
body.auth-action .wait-bar::after {
  content: "";
  display: block;
  width: 40%;
  height: 100%;
  border-radius: 999px;
  background: var(--north-blue);
  animation: auth-wait 1.4s ease-in-out infinite;
}
@keyframes auth-wait {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
@media (prefers-reduced-motion: reduce) {
  body.auth-action .wait-bar::after { animation: none; width: 100%; opacity: 0.45; }
}

/* r12 on the fields, per the brief. The BORDER stays --border-strong and is not the brief's
   #CBD5DE: measured, that value is 1.49:1 on white and an input's boundary is a UI component that
   1.4.11 holds to 3.0. --border-strong is 3.53 and is the token this file already defends. */
body.auth-action input[type="password"] { border-radius: 12px; }

/* 48dp, r12, North Blue - the app's primary, on the web. The submit rules above already supply the
   fill, the radius and the label colour; only the height is new here. */
body.auth-action button[type="submit"] { min-height: 48px; }

/* The success state's primary is an ANCHOR (it leaves for the store), so it cannot inherit the
   submit button's rules and states its own. Label 8.86 on the fill. The focus ring is the document
   ring inherited from above: North Blue at a 2px OFFSET, so the colour adjacent to the ring is the
   white page (8.86) and not the button's own North Blue fill, which would measure 1.00 against the
   thing it rings - the defect this file records for the panel pages. */
body.auth-action .btn-primary,
body.handoff .btn-primary {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0 24px;
  border-radius: 12px;
  background: var(--north-blue);
  color: #FFFFFF;
  font-size: 16px;
  font-weight: 600;
  text-decoration: none;
}
body.auth-action .btn-primary:hover,
body.handoff .btn-primary:hover { background: #083A66; text-decoration: none; }

/* A text action, not a second button. min-height carries the 44px house target; it is a standalone
   control on its own line, so 2.5.8's inline-in-text exception does not cover it. */
body.auth-action .btn-text,
body.handoff .btn-text {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  margin-top: 8px;
  font-size: 15px;
  color: var(--north-blue);
}

/* The route out of a dead link, and the error's code class. Muted on white is 6.50. */
body.auth-action .muted-note,
body.handoff .muted-note { font-size: 14px; color: var(--muted); margin-bottom: 0; }

/* (d) THE SHELL'S FOOTER BAND, 72 HIGH ON A PHONE: 14 + the 44px link target (#W64, kept) + 14.
   Scoped: the DOCUMENT pages read the unscoped .site-foot rules above and do not move. The links are
   14/500 Ice Blue on Deep Ocean (9.96) in ONE row with 24 gaps, on the same 460 column as main; a
   row that does not fit wraps and stays centred at <= 600px. French uses the short labels
   (Accueil / Confidentialité / Conditions), measured: the full ones need 450px at 14/500 with the
   gutters, against 342 at 390. */
body.auth-action .site-foot,
body.handoff .site-foot { margin-top: 0; padding: 14px 24px; }
body.auth-action .site-foot nav,
body.handoff .site-foot nav { max-width: 412px; gap: 0 24px; }
/* min-width because at 14px "Home" is 42 wide (the a11y gate's house bar found it, #W70): the
   44x44 target is both dimensions, and the unscoped rule above only guarantees the height. */
body.auth-action .site-foot a,
body.handoff .site-foot a { font-size: 14px; font-weight: 500; min-width: 44px; justify-content: center; }
@media (max-width: 600px) {
  body.auth-action .site-foot nav,
  body.handoff .site-foot nav { justify-content: center; }
}

/* ============================================================================
   HANDOFF SHELL - /stripe-return (#W69), /stripe-refresh and /review (#W70)
   /auth/action's shell (the selectors above carry body.handoff beside
   body.auth-action) plus the two things only these pages have: the disc and the stars.
   ============================================================================ */

/* (c) The neutral disc on WHITE, 72 with a 36 arrow at stroke 2.2 - the app's empty-state disc,
   measured: NorthyEmptyState (ui/common/Components.kt) draws a 72dp circle with a 36dp icon. The
   brief said 32 "as the app's"; the app's is 36, and the source wins. The ruling it carries lives beside the markup in stripe-return.html
   ("NEUTRAL, NOT A GREEN TICK"). The arrow on the disc is Deep Ocean on Ice Blue, 9.96. The disc
   against white measures ~1.40: it is decoration (aria-hidden, it says nothing), so 1.4.11's 3.0
   does not apply to it. */
body.handoff .icon {
  flex-shrink: 0;
  width: 72px;
  height: 72px;
  background: var(--ice-blue);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 0 24px;
}
body.handoff .icon svg { width: 36px; height: 36px; }
@media (max-width: 600px) {
  body.handoff .icon { margin: 0 auto 24px; }
}
/* The note sits UNDER the primary: the primary has no bottom margin of its own. */
body.handoff .btn-primary + .muted-note { margin-top: 16px; }

/* /review's star row. Decorative (aria-hidden) but it is text, so the contrast gate measures it.
   🔴 NORTH BLUE, NOT THE PANEL'S GOLD (#W70): on white the old #D9A441 is ~2.1 and fails the gate,
   and the app already answers what a star is on a light ground - RatingStar = Cedar = #0F4C81
   (Theme.kt; RatingStarOnDark is the other ground's). 8.85 on white. line-height 1 so the row's box
   is the glyphs, not 1.7 lines of them. */
body.handoff .stars {
  font-size: 40px;
  line-height: 1;
  letter-spacing: 6px;
  color: var(--north-blue);
  margin-bottom: 24px;
}
