/* ==========================================================================
   JumpSteps — Header & Footer chrome
   Vendored from docs/header-footer-design/handoff/styles.css.

   DR-5 SCOPING RULE (enforced by test/chrome-css-scope.test.js):
   Every selector in this file must be one of:
     · :root                      — variable declarations only
     · body / body.js-lock        — the two permitted global exceptions
     · @keyframes                 — animation frames
     · a selector starting .js-   — chrome-owned classes
   Nothing here may select page content. Inner HTML passes through verbatim and
   is never parsed (DR-5), so the ONLY way to keep chrome styles out of page
   bodies is to make them unable to match anything outside the chrome.

   Diffs from the handoff: the global `*`, `body{background,font-family}`, `a`
   and `a:hover` rules are scoped to .js-header-zone/.js-footer; `.is-open >
   .js-arrow` is anchored to .js-nav-item; the demo-only block is removed.

   The bar color is intentionally always dark; header and footer do not
   theme-switch with page content.
   ========================================================================== */
:root {
  /* Surfaces */
  --js-page: #0A0E0F;                     /* vendored surface; retained, not consumed directly */
  --js-page-backdrop: #0A0E0F;            /* dark-theme page backdrop. FINAL — founder-confirmed
                                             on the live render 2026-07-26, chosen by eye over
                                             #141414 (live-SS parity) and #0D1517 (the bar colour).
                                             It equals --js-page; that is a coincidence of the
                                             ruling, NOT a reason to collapse the two. It stays a
                                             separate knob so a future re-ruling moves one line and
                                             cannot disturb the vendored surface. Sits DARKER than
                                             the hero (#1a1a1a) so content reads as raised panels
                                             against it. */
  --js-bar: #0D1517;                      /* header + footer background */
  --js-panel-bg: rgba(19, 27, 29, .88);   /* bar lightened +6, 88% opacity (glass) */
  --js-menu-bg: #111B1D;                  /* small dropdowns (account) */
  --js-scrim: rgba(10, 14, 15, .45);

  /* Brand */
  --js-brand: #4D8990;
  --js-brand-deep: #39666C;               /* filled CTA */
  --js-brand-lt: #7FB0B5;                 /* accent text / links */
  --js-brand-tint: rgba(77, 137, 144, .16);  /* hover / active fills */
  --js-brand-tint-soft: rgba(77, 137, 144, .10);

  /* Ink */
  --js-ink: #E9EFEF;
  --js-ink-strong: #F2F6F6;
  --js-ink-soft: #93A5A7;
  --js-ink-muted: #7E9497;
  --js-heading: #8FA6A8;                  /* footer group headings */
  --js-social: #D3DEDE;

  /* Lines */
  --js-border: #20302F;
  --js-border-soft: #2C4247;
  --js-card-border: #24393E;

  /* Motion */
  --js-ease-slide: cubic-bezier(.2, .7, .3, 1);
  --js-t-arrow: .55s ease;
  --js-t-reveal: .45s ease-out;
  --js-t-slide: .32s;
  --js-t-hover: .15s;

  /* Layout */
  --js-header-h: 86px;
  --js-max-w: 1400px;
  --js-gutter: 28px;

  /* Stacking band. Inner HTML is verbatim and unconstrainable (DR-5) — it can
     declare any z-index it likes. The acorns page alone uses 100 (sticky
     sub-nav), 200 (fixed floating CTA) and 9999 (fixed tooltip). Chrome
     therefore sits in a band above any plausible page content rather than
     trying to fit beneath it.
     Ordering: page content < scrim < header/panels. */
  --js-z-scrim: 99000;
  --js-z-chrome: 99100;
}

/* The permitted global rules. `margin: 0` removes the UA body gutter so the
   full-bleed bar reaches the viewport edge; `.js-lock` is the drawer's scroll
   lock, which by definition must apply to the document. An UNCONDITIONAL page
   background and font-family are still deliberately NOT set here — they belong
   to page content. */
body { margin: 0; }
body.js-lock { overflow: hidden; }

/* DARK-THEME PAGE BACKDROP (DR-5 review, founder-approved 2026-07-25).
   Third permitted global selector, added deliberately.

   Why it must live in the shell, not in page content: on Squarespace an
   ancestor painted the page backdrop, and the composition shell paints none, so
   dark-themed pages rendered their centred content column as islands on the
   browser's white default — measured at 190px of white down each side of the
   1060px homepage column, full page height. A centred, max-width page wrapper
   cannot paint the viewport behind itself without a viewport-width hack, and
   nothing else competes for this rule: no stylesheet sets a page background at
   all. (Contrast the comparison-class gutter, which HAD to live with the page
   because page CSS is emitted inside the shell and outranks the template.)

   Why it is scoped to a class rather than applied to every page: the classes are
   mixed. Measured at the left edge, homepage and partners are dark content on
   white and need this; comparison, compare-hub, reviews-hub, the jco/learning
   family and brand-review are LIGHT content that an unconditional dark backdrop
   would frame in dark. The class is emitted only for page types on the
   allow-list in lib/head.js. */
body.js-theme-dark { background: var(--js-page-backdrop); }

/* Scoped replacements for the handoff's global `*`, `body` and `a` rules. */
.js-header-zone, .js-footer { font-family: 'Roboto', sans-serif; }
.js-header-zone *, .js-footer * { box-sizing: border-box; }

/* The `hidden` attribute is enforced by the USER-AGENT sheet, which any author
   `display` declaration outranks — so `.js-acc-body { display: flex }` silently
   defeated `hidden` and the drawer accordions could never collapse (the arrow
   flipped, the content never moved). This rule restores hidden semantics for
   every chrome element, current and future.
   Specificity (0,2,0) beats a single-class rule (0,1,0), so no !important. */
.js-header-zone [hidden], .js-footer [hidden] { display: none; }
/* :where() contributes ZERO specificity, so these are fallbacks that every
   component rule below overrides — while still being unable to match anything
   outside the chrome (DR-5 containment is identical to a plain descendant
   selector; only the priority differs).

   Do NOT rewrite these as `.js-header-zone a`. That is (0,1,1) and silently
   outranks every single-class component rule in this file — it painted every
   chrome link teal and every hover #fff, overriding .js-cta--goal's white and
   .js-cta--claire:hover's teal. */
:where(.js-header-zone, .js-footer) a { color: var(--js-brand-lt); }
:where(.js-header-zone, .js-footer) a:hover { color: #fff; }

/* ==========================================================================
   Header bar
   ========================================================================== */
.js-header-zone { position: relative; z-index: var(--js-z-chrome); }
.js-header {
  display: flex; align-items: center; gap: 20px;
  height: var(--js-header-h); padding: 0 var(--js-gutter);
  background: var(--js-bar);
}
.js-logo { display: flex; align-items: center; flex: none; }
.js-logo img { height: 34px; width: auto; display: block; }

.js-nav { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
/* padding-inline 14 -> 12 reclaims (14-12) x 2 x 7 = 28px of nav width, which is what
   moves the desktop nav's measured minimum from 1364.9 to 1336.9 and lets the breakpoint
   below carry a real margin. The space comes from INSIDE the hover pill: inter-label
   separation goes 34px -> 30px while the pill only narrows. Derivation of record:
   tools/generator-conversion/nav-breakpoint.json. */
.js-nav-item {
  display: inline-flex; align-items: center; gap: 4px;
  background: none; border: none; cursor: pointer; font-family: inherit;
  font-size: 14.5px; font-weight: 400; line-height: 1; color: var(--js-ink);
  padding: 10px 12px; border-radius: 8px; white-space: nowrap;
  text-decoration: none; transition: background var(--js-t-hover), color var(--js-t-hover);
}
.js-nav-item:hover, .js-nav-item.is-open { background: var(--js-brand-tint); color: var(--js-ink-strong); }
.js-nav-item.is-open { font-weight: 500; }
.js-arrow { transition: transform var(--js-t-arrow); }
.js-nav-item.is-open > .js-arrow, .js-acc-btn.is-open .js-arrow { transform: rotate(180deg); }

/* Right-side actions */
.js-actions { display: flex; align-items: center; gap: 12px; flex: none; }
.js-cta {
  display: inline-flex; align-items: center; gap: 8px; height: 38px;
  font-size: 14px; font-weight: 700; line-height: 1; white-space: nowrap;
  text-decoration: none; padding: 0 16px; cursor: pointer; font-family: inherit;
}
.js-cta--goal { color: #fff; background: var(--js-brand-deep); border-radius: 9px; }
.js-cta--goal:hover { color: #fff; background: #417479; }
.js-cta--claire { color: var(--js-brand-lt); background: none; border: 1px solid var(--js-border-soft); border-radius: 99px; padding: 0 14px; }
.js-cta--claire:hover { color: var(--js-brand-lt); background: var(--js-brand-tint-soft); }
.js-ai-badge {
  display: inline-flex; align-items: center; height: 16px;
  font-size: 10px; font-weight: 900; letter-spacing: .03em; line-height: 1;
  border: 1.5px solid currentColor; border-radius: 5px; padding: 0 4px;
}
.js-ai-badge--sm { height: 15px; font-size: 9.5px; }

.js-account { position: relative; flex: none; }
.js-account-btn {
  display: flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border: 1px solid var(--js-border-soft);
  border-radius: 99px; background: none; cursor: pointer;
}
.js-account-btn:hover, .js-account-btn.is-open { background: var(--js-brand-tint); }
.js-account-menu {
  position: absolute; top: 48px; right: 0; width: 190px;
  background: var(--js-menu-bg); border: 1px solid var(--js-border);
  border-radius: 12px; box-shadow: 0 18px 38px -12px rgba(0, 0, 0, .65);
  padding: 8px; z-index: 40;
}
.js-account-link { display: block; text-decoration: none; font-size: 14px; font-weight: 500; color: var(--js-ink); padding: 10px 12px; border-radius: 8px; }
.js-account-link--primary { font-weight: 700; color: var(--js-brand-lt); }
.js-account-link:hover { background: var(--js-brand-tint); color: var(--js-ink-strong); }

/* ==========================================================================
   Mega panels
   Open on hover of a nav item; close on: pointer leaving the header zone,
   hovering the logo / CTAs / account, clicking the scrim, or Escape.
   Nav items are buttons and never navigate — clicking one TOGGLES its panel.
   Category landing pages are reached via each panel's lead link.
   ========================================================================== */
.js-panel-wrap { position: absolute; left: 0; right: 0; top: 100%; }
.js-panel {
  max-width: var(--js-max-w); margin: 0 auto;
  background: var(--js-panel-bg);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--js-border); border-top: none;
  border-radius: 0 0 18px 18px;
  box-shadow: 0 30px 50px -20px rgba(0, 0, 0, .7);
  padding: 30px var(--js-gutter) 34px; overflow: hidden;
}
/* Opening from fully closed: top-down clip reveal on the panel card */
.js-panel-wrap.is-reveal .js-panel { animation: jsPanelOpen var(--js-t-reveal); }
@keyframes jsPanelOpen {
  from { clip-path: inset(0 0 100% 0); opacity: .6; }
  to   { clip-path: inset(0 0 -40px 0); opacity: 1; }
}
/* Switching between open menus: content slides in the travel direction */
.js-panel-grid { display: grid; grid-template-columns: 320px 1fr 424px; gap: 36px; }
.js-panel-grid.slide-r { animation: jsSlideR var(--js-t-slide) var(--js-ease-slide); }
.js-panel-grid.slide-l { animation: jsSlideL var(--js-t-slide) var(--js-ease-slide); }
@keyframes jsSlideR { from { opacity: 0; transform: translateX(44px); } to { opacity: 1; transform: none; } }
@keyframes jsSlideL { from { opacity: 0; transform: translateX(-44px); } to { opacity: 1; transform: none; } }

.js-panel-lead {
  display: inline-block; text-decoration: none;
  font-size: 17px; font-weight: 700; color: var(--js-ink);
  padding: 8px 12px; margin: -8px -12px; border-radius: 8px;
}
.js-panel-desc { font-size: 13.5px; line-height: 1.55; color: var(--js-ink-soft); margin: 18px 0 0; }
/* Three explicit tracks; menus with two link groups leave the third empty. */
.js-panel-cols {
  display: grid; grid-template-columns: repeat(3, max-content);
  justify-content: space-between; gap: 0 24px; align-content: start; padding-right: 24px;
}
.js-panel-col { display: flex; flex-direction: column; gap: 2px; }
.js-panel-link {
  text-decoration: none; font-size: 15px; font-weight: 500; color: var(--js-ink);
  padding: 9px 12px; margin: 0 -12px; border-radius: 8px; white-space: nowrap;
}
.js-panel-lead:hover, .js-panel-link:hover { background: var(--js-brand-tint); color: var(--js-ink-strong); }

.js-claire-card {
  background: var(--js-brand-tint-soft); border: 1px solid var(--js-card-border);
  border-radius: 14px; padding: 22px 24px; align-self: start;
}
.js-claire-card__title { font-size: 16px; font-weight: 700; color: var(--js-ink); margin-top: 10px; }
.js-claire-card__body { font-size: 13.5px; line-height: 1.5; color: var(--js-ink-soft); margin-top: 5px; }
.js-claire-card__cta { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; font-size: 14px; font-weight: 700; color: var(--js-brand-lt); margin-top: 12px; }
.js-claire-card__cta:hover { color: #fff; }

.js-panel-secondary {
  grid-column: 1 / -1; display: flex; align-items: center; gap: 18px;
  border-top: 1px solid var(--js-border); margin-top: 22px; padding-top: 22px;
}
.js-panel-secondary__link {
  text-decoration: none; font-size: 14px; font-weight: 700; color: var(--js-brand-lt);
  white-space: nowrap; padding: 6px 10px; margin: -6px -10px; border-radius: 7px; flex: none;
}
.js-panel-secondary__link:hover { background: var(--js-brand-tint); color: var(--js-ink-strong); }
.js-panel-secondary__blurb { font-size: 13.5px; line-height: 1.5; color: var(--js-ink-soft); flex: 1; min-width: 0; }

/* Scrim over page content while a panel is open. Fixed, and a SIBLING of
   <main> rather than a child (the handoff nested it inside): the shell must
   never inject nodes into the content container — DR-5. Offset by the bar
   height so it dims content without dimming the header. */
.js-scrim { position: fixed; inset: var(--js-header-h) 0 0 0; background: var(--js-scrim); z-index: var(--js-z-scrim); }

/* Funnel shell (Rung 7 polish): a compact top bar — logo left, the colored
   match-score BANNER (gauge + tagline baked into the image) CENTERED in the bar
   as the whole lockup, NO nav (§11 no-nav-leak), a white compliance footer, and a
   CLS floor. .js-header.js-header--funnel is (0,2,0) so the height beats the base
   .js-header height regardless of source order (the source-order lesson from the
   footer bug). Bar height + banner height are FOUNDER-TUNABLE knobs. */
.js-header.js-header--funnel { position: relative; height: 56px; gap: 0; }
.js-header--funnel .js-logo img { height: 26px; }   /* scaled down for the compact bar */
/* The banner centers in the bar as the lockup (absolutely centered so the logo's
   width on the left doesn't offset it); logo stays left in flow. */
.js-funnel-strip {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
}
/* Colored match-score banner — a WIDE image (613x90), NOT a square icon: render
   at natural aspect, height-constrained. The banner carries its own gauge + text,
   so there is no separate tagline element. Height is a FOUNDER-TUNABLE knob. */
.js-funnel-strip-icon { height: 40px; width: auto; display: block; }

/* White, CENTER-ALIGNED compliance footer — reduces visual pull from the form.
   The address (nav.footer.address), disclosure + legal copy are UNCHANGED
   (verbatim, per the brand-language rules); only colours + alignment are restyled
   (the dark-theme greys/teal are unreadable on white). Funnel only — sitewide
   footer untouched.
   BUGFIX (shipped dark): the base .js-footer { background: var(--js-bar) } (line
   ~300) and this override are BOTH (0,1,0); the base comes LATER in source, so a
   single-class .js-footer--funnel lost and the footer shipped dark. The
   double-class .js-footer.js-footer--funnel is (0,2,0) — it wins by specificity,
   order-independent. The element carries both classes. (The .js-foot-* descendant
   overrides below are already (0,2,x) and always won — only the top-level bar did
   not.) */
.js-footer.js-footer--funnel { background: #fff; border-top: 1px solid #e2e6e6; padding-top: 32px; }
.js-footer--funnel .js-footer-inner { text-align: center; }
.js-footer--funnel .js-foot-address { color: #5b6b6e; margin-top: 0; }
/* max-width cap so centered multi-line disclosure keeps even line lengths. */
.js-footer--funnel .js-foot-disclosure { color: #5b6b6e; max-width: 720px; margin: 16px auto 0; }
.js-footer--funnel .js-foot-disclosure strong { color: #1a2426; }
.js-footer--funnel .js-foot-utility { flex-direction: column; align-items: center; justify-content: center; gap: 12px; border-top: none; padding-top: 0; }
.js-footer--funnel .js-foot-utility-links { justify-content: center; }
.js-footer--funnel .js-foot-link--sm { color: #39666C; }          /* brand-deep: readable teal on white */
.js-footer--funnel .js-foot-link--sm:hover { color: #1a2426; }
.js-footer--funnel .js-foot-copy { color: #6b7b7e; }

/* Funnel content region — .js-funnel-body is the SHELL-owned funnel <main>, NOT
   the page-content #leadgen-mount (chrome CSS must never select page content:
   DR-5, chrome-css-scope test), so funnel-forms.js's injected mount styles and its
   fixHeight (different element / properties) are untouched. Three concerns, all
   FOUNDER-TUNABLE:
   - PADDING keeps CliqForms (the progress bar included) off the region edges.
   - min-height = the MAX of a fixed CLS floor (guards very short viewports and
     first-paint layout shift — a typical step ≈ 600px) and a viewport-minus-header
     FILL, so the footer starts below the initial viewport on common heights. 100dvh
     with a 100vh fallback (browsers without dvh ignore the 2nd declaration). The
     subtracted value is the DESKTOP funnel bar height (56px — keep in sync with
     .js-header.js-header--funnel below).
   - box-sizing: border-box so the padding counts inside the min-height calc. */
.js-funnel-body {
  box-sizing: border-box;
  padding: 28px 40px;
  min-height: max(600px, calc(100vh - 56px));
  min-height: max(600px, calc(100dvh - 56px));
}

/* Mobile funnel treatment (≤640px): the desktop single row (logo left + absolutely-
   centered banner) clips/collides on narrow screens, so switch to a STACKED two-row
   header — row 1 the compact logo (via the <picture> mobile source), row 2 the match
   banner — both centered in simple flow (absolute centering dropped). Header height
   MATCHES the standard site's mobile bar via var(--js-header-h) (86px), so the funnel
   and primary surfaces line up. This is the FUNNEL-SURFACE header standard (also
   /forms-results, /refer, and paid landing pages), not a one-page fix. Desktop
   (>640px) untouched. All FOUNDER-TUNABLE. */
@media (max-width: 640px) {
  .js-header.js-header--funnel {
    height: auto; min-height: var(--js-header-h);    /* = standard 86px mobile bar */
    flex-direction: column; justify-content: center; align-items: center;
    gap: 6px; padding: 6px 0;
  }
  .js-header--funnel .js-logo img { height: 20px; }  /* compact logo, slim row 1 */
  .js-funnel-strip { position: static; transform: none; }  /* stacked flow, row 2 */
  /* content region: tighter side padding (keeps CliqForms off the device edges)
     and a floor keyed to the mobile (var(--js-header-h)) bar. */
  .js-funnel-body {
    padding: 24px 18px;
    min-height: max(440px, calc(100vh - var(--js-header-h)));
    min-height: max(440px, calc(100dvh - var(--js-header-h)));
  }
}

/* ==========================================================================
   Mobile drawer + hamburger (below 1200px)
   ========================================================================== */
.js-claire-mobile {
  display: none; align-items: center; justify-content: center;
  width: 40px; height: 40px; border: 1px solid var(--js-border-soft);
  border-radius: 99px; flex: none;
}
.js-claire-mobile:hover { background: var(--js-brand-tint-soft); }
.js-burger {
  display: none; flex-direction: column; justify-content: center; gap: 5px;
  width: 40px; height: 40px; padding: 0 9px;
  background: none; border: 1px solid var(--js-border-soft); border-radius: 99px; cursor: pointer;
}
.js-burger span { display: block; height: 2px; border-radius: 2px; background: var(--js-brand-lt); transition: transform .25s ease, opacity .25s ease; }
.js-burger.is-open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.js-burger.is-open span:nth-child(2) { opacity: 0; }
.js-burger.is-open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.js-drawer {
  position: fixed; top: var(--js-header-h); left: 0; right: 0; bottom: 0;
  background: var(--js-bar); border-top: 1px solid var(--js-border);
  overflow-y: auto; padding: 20px var(--js-gutter) 40px; z-index: 30;
}
.js-drawer-ctas { display: flex; flex-direction: column; gap: 10px; margin-bottom: 22px; }
.js-drawer-ctas .js-cta { justify-content: center; height: 46px; border-radius: 10px; }
.js-acc { border-bottom: 1px solid var(--js-border); }
.js-acc-btn {
  display: flex; align-items: center; justify-content: space-between; width: 100%;
  font-family: inherit; font-size: 16px; font-weight: 500; color: var(--js-ink);
  background: none; border: none; padding: 16px 2px; cursor: pointer;
}
.js-acc-body { display: flex; flex-direction: column; gap: 2px; padding: 2px 2px 16px; }
.js-drawer-link { display: block; text-decoration: none; font-size: 15px; color: var(--js-ink); padding: 10px 12px; margin: 0 -12px; border-radius: 8px; }
.js-drawer-link:hover { background: var(--js-brand-tint); color: var(--js-ink-strong); }
.js-drawer-link--lead { font-weight: 700; }
.js-drawer-link--accent { font-weight: 700; color: var(--js-brand-lt); }
.js-drawer-auth { display: flex; gap: 24px; padding: 20px 12px 0; }

/* ── HAMBURGER BREAKPOINT — DERIVED, NOT ADOPTED ──────────────────────────────
   1350 is not a round number that looked right; it is ceil(derivedMin + margin - 1).
   MEASURED on the served page: the desktop nav's true minimum viewport is 1364.9px
   (817.7 fixed skeleton + 547.2 of text, two instruments agreeing exactly). The old
   1199 left the band 1200-1364 rendering a desktop nav that CANNOT FIT — the defect
   this raises past. After the 28px nav compression the minimum is 1336.9, and a 14px
   margin puts desktop at V >= 1351 with 14.1px of slack.

   THE MARGIN HAS A FLOOR (10px) BECAUSE THE MEASUREMENT HAS A PLATFORM. It was taken
   on macOS Chrome and 40% of the width is text, so Windows metric variance lands on
   the text runs. A 1365 breakpoint with 1.1px of margin was founder-rejected as
   fragile for exactly that reason.

   ⚠ THE FOOTER'S @media (max-width: 1199px) BELOW IS A DIFFERENT BREAKPOINT AND MUST
   NOT FOLLOW THIS ONE. It governs .js-foot-grid's column count and shared this number
   only by coincidence; after this touch they differ ON PURPOSE. The gate pins their
   independence so the inconsistency is never 'tidied up'.
   Derivation of record: tools/generator-conversion/nav-breakpoint.json */
@media (max-width: 1350px) {
  .js-nav, .js-panel-wrap, .js-actions .js-cta, .js-account { display: none; }
  .js-actions { margin-left: auto; }
  .js-claire-mobile, .js-burger { display: flex; }
}

/* ==========================================================================
   Footer
   ========================================================================== */
.js-footer { background: var(--js-bar); border-top: 1px solid var(--js-border); padding: 56px var(--js-gutter) 32px; }
.js-footer-inner { max-width: var(--js-max-w); margin: 0 auto; }
.js-foot-grid { display: grid; grid-template-columns: 300px repeat(6, 1fr); gap: 40px; }
.js-foot-brand img { width: 190px; height: auto; display: block; }
.js-foot-tagline { font-size: 13.5px; line-height: 1.55; color: var(--js-ink-muted); margin: 14px 0 0; }
.js-foot-social { display: flex; gap: 10px; margin-top: 20px; }
.js-social-btn {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border: 1px solid var(--js-border-soft);
  border-radius: 99px; text-decoration: none; color: var(--js-social);
  transition: color var(--js-t-hover);
}
.js-social-btn:hover { color: var(--js-ink-strong); }
.js-foot-address { font-size: 13px; color: var(--js-ink-muted); margin: 18px 0 0; }
.js-foot-disclosure { font-size: 12.5px; line-height: 1.6; color: var(--js-ink-muted); margin: 44px 0 0; }
.js-foot-disclosure strong { color: var(--js-heading); font-weight: 700; }
.js-foot-col { display: flex; flex-direction: column; gap: 11px; }
.js-foot-heading {
  text-decoration: none; font-size: 11.5px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--js-heading); margin-bottom: 3px;
  transition: color var(--js-t-hover);
}
.js-foot-link { text-decoration: none; font-size: 14px; color: var(--js-brand-lt); transition: color var(--js-t-hover); }
.js-foot-link--sm { font-size: 13px; color: var(--js-ink-muted); }
.js-foot-heading:hover, .js-foot-link:hover { color: var(--js-ink-strong); }
.js-foot-utility {
  display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
  border-top: 1px solid var(--js-border); margin-top: 22px; padding-top: 22px;
}
.js-foot-utility-links { display: flex; gap: 24px; flex-wrap: wrap; align-items: center; }
.js-foot-copy { font-size: 13px; color: var(--js-ink-muted); }

/* FOOTER COLUMN COUNT — INDEPENDENT OF THE HAMBURGER BREAKPOINT ABOVE.
   Deliberately still 1199. These two shared a number by coincidence, never by meaning;
   raising this one with the header would change the footer's graded 3-column layout for
   a reason that has nothing to do with nav overflow. */
@media (max-width: 1199px) {
  .js-foot-grid { grid-template-columns: repeat(3, 1fr); }
  .js-foot-brand { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  .js-foot-grid { grid-template-columns: repeat(2, 1fr); gap: 32px 24px; }
}

/* Rung 8 6a chrome-cycle: jco-page breathing room before the dark full-bleed footer.
   SS fills this zone with inter-content sections; our chrome does not, so the CTA
   sits hard against the footer. Chrome-side override of the COMPOSITION WRAPPER's
   bottom padding only — NOT a jco.css edit (jco.css is a verbatim SS snapshot under
   a freeze-drift check). The wrapper carries a .js- chrome class (js-jco-page, added
   in lib/jco-wrapper.js) so this stays DR-5-scoped; the compound .js-jco-page.jco-page
   (0,2,0) outranks jco.css's .jco-page (0,1,0) despite chrome.css loading first.
   104px + the last element's margin (~16px on CTA pages) → ~120px effective. */
.js-jco-page.jco-page { padding-bottom: 104px; }

/* ══════════════════════════════════════════════════════════════════════════════
   PAGE-REGION RULES — keyed on the shell's own page-type hook, never on wrapper
   classes. `.jcp-wrap` is THREE different classes (commercial-pillar and
   learning-hub need the gutter, product-detail is graded FINE and must not
   change) and no wrapper selector can separate them. Until this touch the shell
   styled NOTHING on <main>, which is the root cause of both defects below.

   ⚠ COUPLED TO STORED page_type — AND THE COUPLING IS NOW ACTUALLY WIRED.
   CORRECTED 2026-07-29. This comment used to claim "the gate asserts every
   selector below matches a live page_type, so a migration turns it RED." It did
   not: every gate input was a committed file, so a registry write moved nothing
   and the gate would have stayed GREEN while a declaration was stranded. The
   missing producer is tools/generator-conversion/page-type-map-build.js, which
   measures live counts into page-type-map.json; gutter-touch-gate.js section 8
   then asserts every page_type-keyed declaration here is backed by >=1 LIVE
   record. THAT assertion names no class, so it survives any renaming.

   THE MIGRATION IS DEFERRED (founder, 2026-07-29 — nomenclature does not gate
   the flip). Its complete design, all 37 records, is committed in
   tools/generator-conversion/pagetype-migration-snapshot.json. The count "29"
   was wrong; so was the risk direction. UNDER THE FLOOR MODEL BELOW, LOSING A
   page_type DOES NOT LOSE A GUTTER — the floor is unconditional. What breaks is
   a SELF-CONTAINED declaration going unbacked, which DOUBLES the gutter: the 19
   live product pages would take .jcp-wrap 18px PLUS the floor. If the migration
   runs, rename the commercial-sub-pillar-hub declaration in STATE 2 to
   product-detail IN THE SAME ACT, then re-run the producer and the gate.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── GUTTER FLOOR — ONE UNCONDITIONAL RULE, SCALED, NO BREAKPOINT ─────────────
   SUPERSEDES the two breakpoint-keyed blocks that stood here (eight classes at
   16px ≤680, partners at 10px ≤680). Both fixed only the ZERO region and left a
   sub-legible ramp above it.

   ⭐ THE LAW THIS ENCODES, TWO-SIDED: geometric nonzero is not a gutter, and a
   gutter beyond minimum-legible is a content-cost defect. Every gutter pixel is
   taken from card width, and a needlessly narrow card also runs taller. So the
   target is the MINIMUM that reads as intentional at each width — never a
   comfortable value. Founder render convicts the endpoints; the curve carries
   them as variables so a re-grade is a one-character change, not a new act.

   MEASURED, and it is why there is no breakpoint: every wrapper is capped and
   centred, so above cap+2×gutter this rule is a NO-OP — offset = (V − cap)/2
   whether the padding is there or not. It floors below and vanishes above, with
   no boundary to dip at. A single extended @media could not do this: brand-review
   caps at 1100 while everything else caps at 1060, so any breakpoint below 1132
   dips for brand-review — the outlier that ruled out breakpoint-coupled shapes.

   PROVISIONAL CURVE (all three numbers are founder-convictable):
     390 → 14px   ·   800 → 20px   ·   1080 → 24px   ·   ≥1108 → no-op
   14 is proposed BELOW the proven-acceptable 16 because 16 was proven acceptable,
   not proven minimal. 24 is proposed below the ~25 measured as clearly-reading at
   1080, on the same argument. */
:root {
  --js-gutter-min: 14px;
  --js-gutter-max: 24px;
  --js-gutter-floor: clamp(var(--js-gutter-min), 8px + 1.5vw, var(--js-gutter-max));
}
.js-page { padding-inline: var(--js-gutter-floor); }

/* ── POSTURE CONTRACT — THREE POSTURES, EVERY ONE OF THEM DECLARED ────────────
   A page is in exactly one of three states, and none of them is silence:

     1. TAKES THE SHELL FLOOR  — the default, above. Nothing to write.
     2. SELF-CONTAINED         — it provides its own gutter, at or above the
                                 floor, so the shell must not add a second one.
     3. TRUE EDGE-TO-EDGE      — it must touch the viewport, by design.

   Only state 3 is an exception. STATE 2 IS NOT AN OPT-OUT AND NOT AN ABSENCE —
   these classes were MEASURED to carry their own gutter and are declared here as
   what they are. Reading a self-contained page as a silent one is the mistake
   that produced the original flush-partners call, where inner card padding was
   read as an outer gutter; the distinction is load-bearing and is why states 2
   and 3 are written in separate blocks even though both resolve to zero.

   STATE 2 — SELF-CONTAINED. Measured per class, at the width where it binds. */
.js-page.js-page--comparison,               /* .jsc-page      20 / 16 ≤680  */
.js-page.js-page--commercial-sub-pillar-hub,/* .jcp-wrap      18   ≤1100    */
.js-page.js-page--compare-hub,              /* .jcmp-wrap     20 / 16 ≤680  */
.js-page.js-page--reviews-hub,              /* .jsw           20 / 16 ≤680  */
.js-page:has(> .jco-page) {                 /* .jco-page      20 / 16 ≤680  */
  padding-inline: 0;
}
/* ⭐ THE jco SET CANNOT BE KEYED ON page_type AND THIS IS MEASURED, NOT ASSUMED.
   The wrapper is applied by CONTENT SIGNAL (lib/jco-wrapper.js), so the classes
   are MIXED: learning-sub-pillar-hub is 19 jco-wrapped and 2 not. A page_type
   list would either double 19 pages or strand 2. `:has(> .jco-page)` matches the
   structure the wrapper actually produces — verified on served bytes as a DIRECT
   child of <main>. KNOWN LIMIT, STATED: no gate we run resolves :has (there is no
   DOM library in this repo), so its MATCH is founder-eye only; the gate asserts
   the structural precondition instead. Failure mode is doubling to ~30–36px on
   93 pages — visible, not broken. */

/* STATE 3 — TRUE EDGE-TO-EDGE. The only state that is an exception. */
.js-page.js-page--why-jumpsteps-bot { padding-inline: 0; }  /* full-bleed white, ledger-ruled */

/* ── BOTTOM CLEARANCE — the two graded TOO TIGHT ──────────────────────────────
   Measured: .jcp-wrap on /banking and .jcl-wrap on /meet-claire declare NO
   padding at all, so content meets the footer. 60px matches product-detail, the
   class graded FINE on the same instrument. (The ledger's spacing convention
   listed only /, /partners and /bot at zero — it predates these two classes.) */
.js-page--commercial-pillar-hub,
.js-page--why-jumpsteps-claire,
/* ⭐ ADDED 2026-07-30 with /why-jumpsteps, and MEASURED not assumed: served /banking renders a 60px
   gap between its final CTA band and the footer, /why-jumpsteps rendered 0 — its final band sat
   flush against the page end. Same cause as the original two: the class declares no bottom padding
   of its own, so content meets the footer. This is the page-end half of the same rhythm the gutter
   floor governs horizontally. */
/* ⭐ ADDED 2026-07-31 with /members, and MEASURED not assumed: P1’s one REAL diff was
   main.padding-bottom 60px (/banking) vs 0px (/members) — its payment-method icons met the
   footer. Same cause as the three above: the class declares no bottom padding of its own.
   JOINING THIS LIST is using the named mechanism; a private 60px in members.css would have been
   a fourth copy of one number. */
.js-page--members-landing,
.js-page--why-jumpsteps-hub { padding-bottom: 60px; }

/* ── HEADER CONFINEMENT — FULL-BLEED BAR, CONSTRAINED CONTENTS ────────────────
   CORRECTED (founder re-grade). The first fix put max-width on .js-header, which
   constrained the BAR: the dark band stopped at 1400px and the page background
   showed at the edges on a wide monitor. A bar is not its contents.

   This is the FOOTER PATTERN, and the file already contained it twice:
     .js-footer        padding: 56px var(--js-gutter) 32px    ← outer paints + pads
     .js-footer-inner  max-width: var(--js-max-w); margin: 0 auto
     .js-panel-wrap    left:0; right:0                        ← outer spans
     .js-panel         max-width: var(--js-max-w); margin: 0 auto
   The header is the third instance. Padding stays on the OUTER element and the
   inner carries ONLY max-width + auto margins, which is what makes header
   contents land on the same left edge as footer contents: both are a 1400px box
   centred in the viewport. Putting the padding on the inner instead would inset
   header contents a further 28px and they would NOT align with the footer —
   measured, and it is why the padding did not move.

   CONTENTS DO NOT MOVE BELOW 1456px (1400 + 2×28): the inner is under its cap,
   so every graded mobile width renders exactly as before. On a 3440 monitor the
   contents shift 28px outward, which is the correction.

   NOT applied to .js-header--funnel, which has no inner wrapper: .js-header
   remains the flex container so that variant's `gap: 0` still binds. Reverting
   the max-width also un-does an ungraded change — the funnel bar shares
   .js-header and was constrained by the first fix without ever being graded. */
.js-header-inner {
  display: flex; align-items: center; gap: 20px;
  flex: 1; min-width: 0;
  max-width: var(--js-max-w); margin: 0 auto;
}

/* ── PARTNERS — SUPERSEDED BY THE FLOOR, and the reasoning is worth keeping ────
   Touch #2 gave partners a bespoke 10px outer gutter on the argument that the
   card pads itself inside so the outer one could be smaller than standard. The
   legibility law retired that: 10px is geometrically nonzero and does not READ,
   and the card's 14px is INNER padding that contributes nothing to the gap the
   eye sees. Partners now takes the same floor as every other page — it is
   deliberately ABSENT from the opt-out list above, and the gate asserts that
   absence so a future reader cannot mistake it for an oversight. The card's
   internal 14px remains untouched. */


/* ── PAGE DISCLOSURE ─────────────────────────────────────────────────────────
   Rendered by the shell from the registry when a page carries additional
   disclosure text. Styled here rather than in a page-scoped sheet because it is
   SHELL furniture: it can appear on any page type, and a page-scoped home would
   mean one copy per type -- the private-duplicate defect this week was about.

   Deliberately quiet: regulatory text is required to be present and legible, not
   prominent. Matches the footer disclosure treatment it sits above. */
.js-page-disclosure {
  /* ⛔ DECLARES ITS OWN FONT, LIKE EVERY OTHER SHELL COMPONENT. chrome.css deliberately sets NO
     body/html font-family — its own note two hundred lines up says so: "background and
     font-family are still deliberately NOT set here — they belong to page content." So
     .js-header-zone and .js-footer each declare Roboto, and anything else outside <main> that
     does not will render in the browser serif default. This block did not, and did. Not a gap in
     the system: the block broke a pattern the system already had. */
  font-family: 'Roboto', sans-serif;
  max-width: var(--js-page, 1060px);
  margin: 0 auto;
  /* Bottom clearance matches the 60px the chrome bottom-clearance list gives page types that
     declare none of their own. Needed HERE because brand-review is not on that list, so <main>
     contributes 0 and the block would meet the footer flush — measured at 0px before this. */
  padding: 28px var(--js-gutter-floor, 20px) 60px;
}
.js-page-disclosure-inner {
  border-top: 1px solid #e2e2e2;
  padding-top: 16px;
  font-size: 12px;
  line-height: 1.6;
  color: #6e6e6e;
  white-space: pre-line;   /* the field is Long text; author paragraphs survive */
}