/* =========================================================================
   Joseph Ruiz, Mortgage Loan Officer at The Lending Pros.
   The whole design system. Hand-written, no build step.

   Brand values come from PART 1 of Brett's build spec. The aesthetic is the
   Erica Iriarte system the client named as the reference: warm white and
   cream, serif display type, generous space, restrained motion. The one
   departure the client asked for is a blue accent.

   Every rule tagged [CHECKLIST n] is an acceptance criterion from PART 2 of
   that spec. Each one is a defect a previous Appomark build shipped. Do not
   "simplify" them away.
   ========================================================================= */

/* [CHECKLIST 4] FIRST rule in the sheet, deliberately. Without it iOS Safari
   paints button text in system blue while desktop renders black, so the bug is
   invisible in desktop testing. */
button {
  color: inherit;
  font: inherit;
}

/* ------------------------------------------------------------------ tokens */
:root {
  --paper: #FFFFFF;
  --cream: #FBF9F7;                  /* page background */
  --sand: #F0EDE8;                   /* alternating sections */
  --line: #E2DED6;
  --ink: #101418;
  --ink-soft: rgba(16, 20, 24, .66);
  --ink-faint: rgba(16, 20, 24, .44);

  /* THE TWO SET-BACK INKS THE ROUND 3 COMPONENTS NEED, and the reason they are
     not the flat grey the reference file used. Both the loan counter and the
     Learn list keep everything except the focused item set back, so on any
     given screen most of the words are in this colour. The reference sets it
     to #C9C6BC, which measures 1.5:1 on cream: that is not muted, it is
     illegible, and this site publishes an accessibility statement claiming AA.
     So the same hierarchy is drawn with ink at reduced opacity, measured on
     BOTH grounds the components sit on:
       --ink-set   4.76:1 on cream, 4.59:1 on sand  (body copy, needs 4.5:1)
       --ink-back  3.25:1 on cream, 3.17:1 on sand  (32px and up, needs 3:1)
     The focused item is full ink at 15:1, so the difference still reads at a
     glance. Do not lighten either one without re-measuring. */
  --ink-set: rgba(16, 20, 24, .60);
  --ink-back: rgba(16, 20, 24, .48);

  /* THE ACCENT IS SAMPLED FROM HIS OWN LOGO, not invented.
     The spec shipped a placeholder navy (#1F5FA8) pending the client's hex
     codes. His logo arrived afterwards and carries the real brand blue in its
     ring: #47A5CD, hue 198 degrees. That raw colour is far too light for text
     (2.65:1 on cream, 2.79:1 for white on it, both well under the 4.5:1 floor),
     so the tokens below hold HIS HUE AND SATURATION darkened to a usable
     lightness. Measured, not guessed:
       --blue on --cream  6.81:1     --blue on --sand  6.12:1
       white on --blue    7.15:1
     If he sends exact hexes, swap these three and RE-CHECK those three numbers
     before shipping. Say so if his supplied blue fails; do not ship it quietly. */
  --blue: #215E78;
  --blue-deep: #1A4A5E;
  --blue-wash: #EDF6FA;              /* tinted panel, calculators and callouts */

  /* THE ACCENT AT 70%, AS A COLOR RATHER THAN AS `opacity`. The award
     separator used to reach `opacity: .7` and stop there, which is the same
     pixels but a different CLAIM: `responsive-check`'s blanks pass reads a
     settled reveal as opaque, so an element resting under 0.99 is exactly
     what an unfired reveal looks like, and it reported the separator blank at
     thirteen rest positions on /about/. Carrying the alpha in the color lets
     the settled state be a true `opacity: 1`, so the check keeps its whole
     grip on that element: if the reveal ever really fails, opacity stays 0 and
     it is still caught. No contrast figure is quoted because the separator is
     a decorative diamond carrying `aria-hidden="true"`. */
  --blue-soft: rgba(33, 94, 120, .7);

  /* The editorial upgrade adds a DARK end to the same hue family, so the page
     can close on a strong dark moment without inventing a second brand colour.
     --teal-deep is the estimate card, the closing band and the footer; it is
     hue 193, four degrees off his logo ring, so the two read as one family.
     Measured against it, because dark grounds are where contrast quietly fails:
       cream on --teal-deep        11.3:1
       cream at .80 on --teal-deep  7.7:1   (the compliance block floor)
       --blue-light on --teal-deep  6.5:1
     --accent-teal is an ALIAS, not a new colour: it points at his blue so the
     new row and wizard rules use one token and a future hex swap stays a
     three-line edit at the top of this file.

     THERE IS NO GOLD IN THIS PALETTE. A --gold token carried three small
     labels and was removed on Brett's round 2 note: the palette is white,
     cream, sand, ink and the one blue accent, and nothing else.

     --blue-light is that same accent for DARK grounds, not a second colour.
     --blue itself measures 1.8:1 on --teal-deep, which is invisible, so the
     estimate card's label needs the hue carried up in lightness rather than
     down. Hue 198 degrees, the same as his logo ring and the same as --blue.
     Use it ONLY on the deep teal; on cream it fails, and --blue is correct
     there. */
  --accent-teal: var(--blue);
  --blue-light: #7FC4E2;
  --teal-deep: #0D3844;
  --teal-glow: rgba(23, 90, 108, .85);
  --cream-dark: rgba(247, 244, 238, .92);

  /* [CHECKLIST] Neutral near black RGB triplet for photo overlays. NEVER a
     brand colour: a tinted scrim colours every photograph on the site and costs
     a full revision round to trace back. */
  --scrim: 10, 9, 8;

  /* THREE FACES, AND THE SPLIT IS "READ" AGAINST "OPERATE", ROUND 7 PART 4.
     --display is the headline cut, --text is the TEXT cut of the same family,
     and --body is the interface face.

     Every paragraph a visitor READS is set in --text: article bodies, the About
     bio, the loan page intro, every lede, every description under a title, the
     legal pages and the questionnaire's helper lines. Everything a visitor
     OPERATES stays in --body: the nav, eyebrows, labels, buttons, chips, form
     fields and their hints, the marquee, the fit checklists, the facts row and
     every figure the calculators print. The site read like form text because
     one sans was doing both jobs.

     'Noto Serif' and 'Noto Serif Display' are two cuts of ONE family, so the
     pairing needs no checking: the display cut is drawn for 32px and up and
     goes thin and tight at paragraph sizes, which is exactly why the text cut
     exists. Do not set body copy in --display. */
  --display: 'Noto Serif Display', 'Times New Roman', serif;
  --text: 'Noto Serif', Georgia, 'Times New Roman', serif;
  --body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;

  --nav-h: 88px;
  --wrap: 1240px;
  --ease: cubic-bezier(.22, .61, .36, 1);

  /* The vertical rhythm is ONE scale, here and nowhere else. Every section pays
     the gap twice, once as its own bottom padding and once as the next
     section's top, so 56px a side is the 112px desktop budget the responsive
     check enforces. Never hard-code a fat literal padding on a section; if a
     section needs more air, raise this and re-run the check. */
  --gap-section: 56px;
  --gap-tight: 40px;
  --gap-head: 24px;
}

@media (max-width: 860px) {
  :root {
    --nav-h: 72px;
    --gap-section: 36px;
    --gap-tight: 28px;
  }
}

/* -------------------------------------------------------------------- base */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

/* Any anchored target lands with its heading clear of the fixed nav. */
:target,
section[id] {
  scroll-margin-top: 104px;
}

body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img,
svg,
video {
  max-width: 100%;
  display: block;
}

/* `height: auto` is NOT cosmetic here, it is what makes `aspect-ratio` work.
   An <img> carrying width and height ATTRIBUTES (which it should, so the
   browser can reserve the space and avoid a layout shift) gets a presentational
   height, and a height that is not `auto` makes the browser ignore
   `aspect-ratio` entirely. The About portrait shipped 562px wide and 1750px
   tall because of exactly that, which then pushed the bio beside it a thousand
   pixels down the page and out of the first screen. Nothing in the responsive
   check can see this: it causes no horizontal overflow, no blank section and no
   rhythm breach. It was found by looking at the page. */
img[width][height] {
  height: auto;
}

a {
  color: var(--blue);
  text-decoration: none;
}

a:hover {
  color: var(--blue-deep);
}

hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: var(--gap-tight) 0;
}

.container {
  width: min(100% - 40px, var(--wrap));
  margin-inline: auto;
}

@media (max-width: 560px) {
  .container {
    width: calc(100% - 32px);
  }
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--ink);
  color: #fff;
  padding: 12px 18px;
  z-index: 200;
}

.skip-link:focus {
  left: 12px;
  top: 12px;
}

:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 3px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------- type kit */
h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 300;
  line-height: 1.14;
  margin: 0;
  letter-spacing: -.01em;
}

/* [CHECKLIST 10] No trailing heading periods anywhere on this site. */

.eyebrow {
  position: relative;
  display: block;
  font-family: var(--body);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--blue);
  margin-bottom: 14px;
}

/* The drawing rule is the signature of the motion system: on a revealed
   section head this 30px dash grows from zero before the heading rises. It is
   `currentColor`, so it inherits the cream override on a dark header rather
   than needing a second rule per context. */
.eyebrow::before {
  content: '';
  display: inline-block;
  width: 30px;
  height: 1px;
  margin-right: 12px;
  vertical-align: middle;
  background: currentColor;
  opacity: .85;
}

.h-display {
  font-size: clamp(38px, 6.4vw, 78px);
  text-transform: uppercase;
  letter-spacing: .04em;
}

.h-section {
  font-size: clamp(32px, 4.2vw, 56px);
}

.h-card {
  font-family: var(--display);
  font-size: 21px;
  font-weight: 300;
}

/* ROUND 7 PART 4.3.1: THE LEDE IS THE ON RAMP TO A SECTION, so it is set in the
   serif italic at 20px. This needed no new markup anywhere on the site, because
   the paragraph under a page header or a section heading is ALWAYS this class.
   That is also the boundary of the device: the paragraph under an `h2` INSIDE
   an article is not a lede, it is the article, and setting those italic too
   would put half of every article in italic. */
.lede {
  font-family: var(--text);
  font-style: italic;
  font-size: clamp(18px, 1.6vw, 20px);
  line-height: 1.6;
  color: var(--ink-soft);
  max-width: 62ch;
}

.muted {
  color: var(--ink-soft);
}

.small {
  font-size: 13px;
  line-height: 1.6;
}

/* A TEXT column capped for reading length is deliberate typography, not dead
   space. The responsive check's FILL pass skips these on purpose. */
.section-head {
  max-width: 760px;
  margin-bottom: var(--gap-head);
}

.section-head.center {
  margin-inline: auto;
  text-align: center;
}

/* ROUND 7 PART 4.1: THE READING COLUMN. 18px in the serif text cut at 1.75, in
   a 62ch measure, with 1.4em between paragraphs. The family sits on `.prose`
   itself rather than on `.prose p`, because `ch` is measured in the element's
   OWN font: put the family on the children and the 62ch cap is still being
   measured in Inter and the column comes out the wrong width. */
.prose {
  font-family: var(--text);
  font-size: 18px;
  line-height: 1.75;
  max-width: 62ch;
}

.prose p,
.prose ul,
.prose ol {
  color: var(--ink-soft);
  margin: 0 0 1.4em;
}

.prose h2 {
  font-size: clamp(24px, 2.6vw, 32px);
  margin: 36px 0 12px;
}

.prose h3 {
  font-size: 20px;
  margin: 28px 0 8px;
}

.prose li {
  margin-bottom: 8px;
}

/* ROUND 7 PART 4.3.3: THE PULL LINE. One sentence LIFTED out of the copy it
   came from, not a new one written and not a copy of one left in place: the
   page has to carry the same words afterwards. Roughly one every four
   paragraphs, and never two on one screen.

   THE SELECTOR NAMES BOTH HOSTS ON PURPOSE. `.prose p` and `.about-bio p` are
   both one class and one element, so a bare `.pull` written here, above them,
   loses the tie on margin and colour and the line comes out grey and spaced
   like an ordinary paragraph. Naming the parent wins it outright and says which
   two places carry the device. */
.prose .pull,
.about-bio .pull {
  font-family: var(--text);
  font-style: italic;
  font-size: 24px;
  line-height: 1.45;
  color: var(--blue);
  max-width: 32ch;
  margin: 24px 0;
}

@media (max-width: 560px) {
  .prose .pull,
  .about-bio .pull {
    font-size: 21px;
    max-width: none;
  }
}

/* ROUND 7 PART 4.2.1: EMPHASIS IS A BRAND MOMENT, NOT A HEAVIER GREY. Bold
   inside body copy renders in the accent, which is 6.81:1 on cream and 6.12:1
   on sand.
   TWO GROUNDS ARE EXCLUDED BELOW AND BOTH HAVE TO BE. `--blue` measures 1.8:1
   on the deep teal, so on the closing band and inside the calculator's results
   panel the same rule would be invisible ink. The calculator's panel prints
   every one of its figures in `<b>`, which is why it is named rather than left
   to chance. */
p strong,
p b,
li strong,
li b {
  color: var(--blue);
  font-weight: 600;
}

.calc-results strong,
.calc-results b {
  color: inherit;
}

.cta-band p strong,
.cta-band p b,
.section-ink p strong,
.page-header.has-media .lede strong {
  color: var(--blue-light);
}

/* ROUND 7 PART 4.2.3: an inline link inside body copy is underlined, offset
   clear of the descenders, and the rule thickens rather than the colour
   changing. Scoped to reading contexts, so the nav, the buttons and the
   compliance links in the footer are untouched. */
.prose p a,
.prose li a,
.lede a,
.loan-intro a,
.about-bio a {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-thickness .15s var(--ease), color .15s var(--ease);
}

.prose p a:hover,
.prose li a:hover,
.lede a:hover,
.loan-intro a:hover,
.about-bio a:hover {
  text-decoration-thickness: 2px;
}

/* ----------------------------------------------------------------- buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  /* [CHECKLIST 1] inline-flex + nowrap, so a CTA never wraps to two lines and
     shoves the nav bar taller. */
  white-space: nowrap;
  padding: 14px 26px;
  border: 1px solid transparent;
  border-radius: 2px;
  font-family: var(--body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease), transform .25s var(--ease);
}

.btn-solid {
  background: var(--blue);
  color: #fff;
  border-color: var(--blue);
}

.btn-solid:hover {
  background: var(--blue-deep);
  border-color: var(--blue-deep);
  color: #fff;
}

.btn-outline {
  background: transparent;
  color: var(--ink);
  border-color: var(--ink);
}

.btn-outline:hover {
  background: var(--ink);
  color: var(--cream);
}

.btn-ghost {
  background: transparent;
  color: #fff;
  border-color: rgba(255, 255, 255, .6);
}

.btn-ghost:hover {
  background: #fff;
  color: var(--ink);
  border-color: #fff;
}

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: .45;
  cursor: not-allowed;
}

/* ROUND 4: THE PILL. Scoped, like every other piece of soft geometry added in
   round 4: the wizard's CONTINUE, the loan page's single CTA, and nothing
   else. A site whose buttons are all rounded loses the one place the rounding
   was meant to signal "this is the thing to press". */
.btn-pill {
  border-radius: 999px;
  padding: 16px 34px;
  font-size: 12px;
}

/* The magnetic hover. site.js writes --mx / --my from the pointer's offset
   inside the button and clears them on leave, so the resting state is a plain
   `translate(0,0)` and nothing is left displaced if the pointer exits during
   a transition. The transform is SHORTER than the button's colour transitions
   on purpose: at .25s the button lags visibly behind the cursor. */
.btn-pill[data-magnetic] {
  transition:
    background .25s var(--ease),
    color .25s var(--ease),
    border-color .25s var(--ease),
    transform .18s var(--ease);
  transform: translate(var(--mx, 0px), var(--my, 0px));
}

/* Reduce Motion gets a button that does not chase the pointer. */
@media (prefers-reduced-motion: reduce) {
  .btn-pill[data-magnetic] {
    transform: none !important;
  }
}

.link-more {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--blue);
}

.link-more::after {
  content: '';
  width: 22px;
  height: 1px;
  background: currentColor;
  transition: width .25s var(--ease);
}

.link-more:hover::after {
  width: 34px;
}

/* -------------------------------------------------------------- navigation */
.nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: 100;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  background: transparent;
  transition: background .3s var(--ease), border-color .3s var(--ease), box-shadow .3s var(--ease);
  border-bottom: 1px solid transparent;
}

/* [CHECKLIST 2] Solid cream with a hairline past 80px of scroll. Transition
   only, never a layout change, or the page jumps under the reader. */
.nav.is-solid {
  background: var(--cream);
  border-bottom-color: var(--line);
}

/* Interior pages have a light header behind the bar, so the bar is solid from
   the first frame. Only the home hero is dark enough for transparent chrome. */
.nav.is-static {
  background: var(--cream);
  border-bottom-color: var(--line);
}

/* THE THREE ZONE BAR: logo hard left, links dead centre, button hard right.
   `1fr auto 1fr` puts the links in the middle on its own, but each outer cell
   is a full third of the bar and a grid item stretches to fill it, so the
   wordmark's clickable box used to run 400px into the middle of the bar. The
   two `justify-self` declarations below shrink each end to its own content and
   pin it to its own edge. */
.nav-inner {
  width: min(100% - 40px, var(--wrap));
  margin-inline: auto;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 20px;
}

/* Past 1500px the 1240px content wrap leaves 340px of empty page outside the
   bar at either end, and the whole lockup reads as one centred cluster rather
   than as furniture pinned to the sides. The bar, and only the bar, is allowed
   wider than the wrap so the logo and the button reach the edges at 1920. */
@media (min-width: 1500px) {
  .nav-inner {
    width: min(100% - 96px, 1720px);
  }
}

/* 1.3 The wordmark. No logo was supplied at spec time; one arrived later, so
   the mark is an image with the typographic lockup beside it. One class, so a
   replacement logo is a single swap. */
.nav-wordmark {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  color: inherit;
  justify-self: start;
}

.nav-wordmark img {
  width: 40px;
  height: 40px;
  flex: none;
}

/* ROUND 7: THE MARK ALONE, ON EVERY PAGE. Round 6 made this the interior
   variant and round 7 made it the only one, so partials.js now emits it for the
   home page too. It emits no `.wordmark-text` at all rather than hiding one, so
   there is nothing left in the bar for a stray rule to reveal. The anchor keeps
   its `aria-label`, so the accessible name is still the full one.

   THE BAR DOES NOT CHANGE HEIGHT AND NOTHING ELSE MOVES. --nav-h owns the
   height, and the three zone grid sizes its middle column to the links, so the
   two outer cells split what is left evenly whatever the mark measures: the
   links stay centred on the bar and the button stays on its right edge at 40px
   and at 52px alike. Measured at 1204 and 1440 after the change. */
.nav-wordmark.is-mark-only img {
  width: 52px;
  height: 52px;
}

.wordmark-text {
  display: block;
}

.wordmark-name {
  display: block;
  font-family: var(--display);
  font-size: 17px;
  font-weight: 300;
  letter-spacing: .14em;
  text-transform: uppercase;
  line-height: 1.1;
  white-space: nowrap;
}

.wordmark-role {
  display: block;
  font-size: 9px;
  font-weight: 500;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-top: 5px;
  white-space: nowrap;
}

/* The transparent bar used to lift `.wordmark-name` and `.wordmark-role` to
   white over the hero and the header photographs. Round 7 took the printed name
   out of the bar on every page, so those two rules had no element left to
   colour and are gone with it. The classes themselves stay: the footer lockup
   still uses them, and it is never transparent. */

.nav-links {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 30px;
}

/* ROUND 5 PART 1, AND THE TARGET IS NOW A MEASURED REFERENCE RATHER THAN A
   JUDGEMENT. Round 4 lifted the link from 11px to 16px to rescue a 10px button
   label, and 16px uppercase overshot: against a 17px wordmark the links read as
   a second headline. The reference build measures 13px at 1.2px of tracking at
   weight 500, so that is what this bar carries. The tracking and the weight are
   unchanged from round 4; only the size comes down.
   Checked for wrap at 1204 and 1160, the two widths above the 1150px burger
   breakpoint where the bar is tightest, and a smaller label can only widen the
   margin there. */
.nav-link {
  position: relative;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--ink);
  /* [CHECKLIST] A desktop nav item holds ONE line, always. Wrap adds height,
     not width, so a sweep cannot see it. */
  white-space: nowrap;
}

.nav.is-transparent .nav-link {
  color: #fff;
}

.nav-link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -6px;
  width: 0;
  height: 1px;
  background: currentColor;
  transition: width .28s var(--ease);
}

.nav-link:hover::after,
.nav-link[aria-current="page"]::after {
  width: 100%;
}

.nav-cta {
  display: flex;
  justify-content: flex-end;
  justify-self: end;
}

/* ROUND 5 PART 1, the same correction on the button. Round 4 took the label
   from 10px to 13.5px inside 15px / 28px of padding, which drew a 54px tall
   button in an 88px bar. The reference measures 13px inside 13px / 22px, so
   the button sits at 45px and stops competing with the wordmark beside it.
   The tracking stays at a flat 1.5px. The bar height is --nav-h and does not
   move with the button, so nothing else in the chrome shifts. */
.nav-cta .btn {
  padding: 13px 22px;
  font-size: 13px;
  letter-spacing: 1.5px;
}

.nav.is-transparent .nav-cta .btn-solid {
  background: #fff;
  color: var(--ink);
  border-color: #fff;
}

.nav.is-transparent .nav-cta .btn-solid:hover {
  background: var(--blue);
  border-color: var(--blue);
  color: #fff;
}

/* ROUND 8 PART 6.2, THE EN | ES CONTROL. It sits in the right hand cell with
   the button, so it lands right of the links and left of the CTA exactly as the
   note asks, and it inherits that cell's `justify-self: end`. The active
   language is full ink and the other is set back; on a photographic header the
   whole control lifts to white with the rest of the bar. */
.lang-toggle {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-right: 18px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  white-space: nowrap;
}

.lang-toggle .is-current {
  color: var(--ink);
}

.lang-toggle a {
  color: var(--ink-set);
  transition: color .25s var(--ease);
}

.lang-toggle a:hover {
  color: var(--blue);
}

.lang-toggle span[aria-hidden] {
  color: var(--ink-faint);
}

.nav.is-transparent .lang-toggle .is-current {
  color: #fff;
}

.nav.is-transparent .lang-toggle a,
.nav.is-transparent .lang-toggle span[aria-hidden] {
  color: rgba(255, 255, 255, .7);
}

.nav.is-transparent .lang-toggle a:hover {
  color: #fff;
}

/* In the panel it sits above the CTA button, on its own row, centred with the
   rest of the panel's furniture rather than pinned to an edge that is not
   there. The margin is the panel's own rhythm, not the bar's. */
.lang-toggle-mobile {
  margin: 8px 0 6px;
  justify-content: flex-start;
  font-size: 14px;
}

.nav-toggle {
  display: none;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  /* The burger belongs hard right. See the mobile grid redeclaration below,
     which is what actually keeps it there. */
  justify-self: end;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 5px;
}

.nav-toggle span {
  display: block;
  width: 22px;
  height: 1.5px;
  background: var(--ink);
  transition: transform .3s var(--ease), opacity .2s var(--ease);
}

.nav.is-transparent .nav-toggle span {
  background: #fff;
}

.nav-toggle[aria-expanded="true"] span {
  background: var(--ink);
}

.nav-toggle[aria-expanded="true"] span:nth-child(1) {
  transform: translateY(6.5px) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] span:nth-child(2) {
  opacity: 0;
}

.nav-toggle[aria-expanded="true"] span:nth-child(3) {
  transform: translateY(-6.5px) rotate(-45deg);
}

/* ------------------------------------------------------------ mobile panel */
/* [CHECKLIST 1] The burger breakpoint is 1150px. THE GRID IS REDECLARED HERE,
   and that is the whole point of this block. Below the breakpoint the links and
   the CTA go display:none, which takes them OUT of the parent grid, so with a
   `1fr auto 1fr` template the two survivors fall into columns one and two and
   the burger lands DEAD CENTRE. `margin-left:auto` cannot rescue it, because an
   `auto` column is shrink-to-fit and offers no free space to push into. This
   shipped centred on a previous build at every width from 320 to 1150 and
   survived a 91 screenshot contact sheet, because nobody looked at that corner. */
@media (max-width: 1150px) {
  .nav-inner {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .nav-links,
  .nav-cta {
    display: none;
  }

  .nav-toggle {
    display: flex;
    justify-self: end;
  }
}

.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 99;
  /* [CHECKLIST 3] Solid background while open, and the top padding reserves the
     bar height so the first item is never under the burger. */
  background: var(--cream);
  padding: calc(var(--nav-h) + 24px) 24px 40px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  transform: translateY(-100%);
  visibility: hidden;
  transition: transform .4s var(--ease), visibility .4s;
  overflow-y: auto;
}

.mobile-menu.is-open {
  transform: translateY(0);
  visibility: visible;
}

/* [CHECKLIST 3] No scroll behind the open panel. */
body.menu-open {
  overflow: hidden;
}

.mobile-menu a:not(.btn) {
  font-family: var(--display);
  font-size: 26px;
  font-weight: 300;
  color: var(--ink);
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}

.mobile-menu .btn {
  margin-top: 22px;
  align-self: flex-start;
}

/* THE LANGUAGE LINKS ARE NOT NAV ITEMS AND MUST NOT BE DRESSED AS ONE.
   `.mobile-menu a:not(.btn)` above sets every anchor in the panel to a 26px
   display serif with a rule under it, which would turn EN and ES into two more
   menu entries. This block sits after it and outranks it, so the control reads
   as the small piece of chrome it is in the bar. */
.mobile-menu .lang-toggle a,
.mobile-menu .lang-toggle .is-current {
  font-family: var(--body);
  font-size: 14px;
  font-weight: 500;
  padding: 0;
  border-bottom: 0;
}

.mobile-menu .lang-toggle a {
  color: var(--ink-set);
}

.mobile-contact {
  margin-top: auto;
  padding-top: 28px;
  font-size: 13px;
  color: var(--ink-soft);
}

.mobile-contact a {
  display: block;
  color: var(--ink);
  padding: 3px 0;
  font-family: var(--body);
  font-size: 13px;
  border: 0;
}

/* ------------------------------------------------------------------- hero */
/* ROUND 4 PART 2: THE LOCKUP IS CENTRED, HORIZONTALLY AND VERTICALLY. It used
   to sit on the floor of the frame at the left, which read as unanchored
   because nothing else on the page shares that composition. `center` on both
   axes plus a centred measure on the copy is the whole change; the padding
   still reserves the nav height so the name can never slide under the bar. */
.hero {
  position: relative;
  /* [CHECKLIST 5] 100dvh, so mobile browser chrome does not crop the buttons. */
  min-height: 100dvh;
  display: flex;
  align-items: center;
  /* The bottom padding is part of the RHYTHM BUDGET, not just breathing room:
     it is paid once here and again as the next section's top padding, so a
     viewport-relative value that looks harmless at 1440 blows the phone budget
     on a tall screen. The first build ran clamp(48px, 8vh, 96px) and the check
     caught 108px against a 96px mobile ceiling. Keep the ceiling low enough
     that this plus --gap-section stays inside the budget at every height. */
  padding: calc(var(--nav-h) + 40px) 0 clamp(44px, 6vh, 78px);
  overflow: hidden;
}

@media (max-width: 860px) {
  .hero {
    padding-bottom: 44px;
  }
}

.hero-media {
  position: absolute;
  inset: 0;
  /* [CHECKLIST 8] No z-index:-1 decorations. The media is a sibling behind the
     content by document order and a positive stack, never a negative one, which
     on some Safari versions drops it behind the page background entirely. */
  z-index: 0;
}

/* THE PARALLAX LAYER, and the oversize is the load-bearing half of it.
   The layer translates as the page scrolls, so it must be TALLER than the
   frame it sits in or the frame's own background shows through as a bar along
   one edge. The room here and the speed in site.js are one number apart on
   purpose: travel is `scrollY * 0.12` and the hero stops being visible once
   scrollY passes its own height, so the most it ever moves is 12% of that
   height against 16% of room. site.js clamps to the room as well, so a change
   to either number cannot open a gap. */
.hero-media-layer {
  position: absolute;
  left: 0;
  right: 0;
  top: -16%;
  height: 132%;
  will-change: transform;
}

.hero-media img,
.hero-media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The bias keeps the porch and door in frame on a tall phone viewport, where
     a centred crop pushes the house out of the top. */
  object-position: 50% 62%;
}

/* The ambient loop is an ENHANCEMENT over the still, never a replacement for
   it. It carries no src until site.js decides the visitor should have it
   (desktop, motion allowed, data saver off), it fades in only once the browser
   reports it is actually playing, and site.js removes it on an error. So a
   phone, a blocked request and a file that is not there all land on the same
   place: the photograph. */
.hero-media video {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 1.2s var(--ease);
}

.hero-media video.is-playing {
  opacity: 1;
}

/* THE SCRIM FOLLOWS THE TEXT, and round 4 moved the text. It used to be a
   left-weighted horizontal pass, because the lockup sat in the left third over
   a bright cream house and a purely vertical wash left "MORTGAGE LOAN OFFICER,
   THE LENDING PROS" in 11px white over white siding. The lockup is centred
   now, so the protection is a centred pool instead: a soft ellipse over the
   middle of the frame where every line of the copy actually is, plus the
   standing vertical wash that keeps the top and the bottom edges honest.
   A HEAVIER FLAT SCRIM IS NOT THE ANSWER. It would dim the photograph
   uniformly, and the picture is the reason the hero exists. */
.hero-scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 78% 62% at 50% 48%, rgba(var(--scrim), .66) 0%, rgba(var(--scrim), .52) 42%, rgba(var(--scrim), .22) 74%, rgba(var(--scrim), 0) 100%),
    linear-gradient(to top, rgba(var(--scrim), .62) 0%, rgba(var(--scrim), .26) 42%, rgba(var(--scrim), .16) 70%, rgba(var(--scrim), .40) 100%);
}

/* On a phone the copy is wider relative to the frame and the picture is
   cropped tighter, so the pool spreads and the vertical wash carries more. */
@media (max-width: 1150px) {
  .hero-scrim {
    background:
      radial-gradient(ellipse 95% 58% at 50% 48%, rgba(var(--scrim), .68) 0%, rgba(var(--scrim), .54) 46%, rgba(var(--scrim), .26) 78%, rgba(var(--scrim), 0) 100%),
      linear-gradient(to top, rgba(var(--scrim), .70) 0%, rgba(var(--scrim), .40) 46%, rgba(var(--scrim), .30) 74%, rgba(var(--scrim), .44) 100%);
  }
}

.hero .container {
  position: relative;
  z-index: 1;
  color: #fff;
  text-align: center;
}

.hero-lockup {
  margin-bottom: 26px;
}

/* ROUND 6 PART 1, THE SIZE BETWEEN THE TWO THIS SITE HAS ALREADY SHIPPED.
   The name ran 103.5px before round 5 and 72.2px after it, and the client asked
   for the midpoint, so the desktop half of the rule is 7.3vw capped at 92px:
   87.9px at 1204 and the full 92px from 1260px up. One line at every width, so
   `white-space: nowrap` states it rather than leaving it to whatever the
   measure allows.

   ROUND 8 PART 2 CHANGED THE WORDS, SO THE FLOOR HAD TO MOVE WITH THEM. The
   hero now reads JOSEPH ALLAN RUIZ. Measured in the browser at the real face:
   the old name drew 6.65px of width for every 1px of size and the new one draws
   10.55px, which is 59 percent wider at any size. The note asks for a 34px
   floor and says it fits at 390px. It does not, by a whisker: 34px is 358.8px
   of unbreakable headline against 358px of measure at 390px, so it would drag
   the page sideways by less than a pixel, which is still sideways.

   THE FLOOR IS THEREFORE A `min()`, exactly as it was for the short name, and
   8.3vw is the arithmetic rather than a guess. At 320px the measure is 288px
   and 8.3vw is 26.6px, which draws 280px and leaves the same 7px of clearance
   the round 6 build was measured at. It reaches the full 34px at 410px and the
   desktop rule takes over at 466px. At 390px the name is 32.4px, which is 341px
   inside 358px of measure and one line, which is what the note actually wanted.
   RAISING 8.3vw BREAKS 320px. Raising the 34px floor changes nothing there. */
.hero-name {
  font-family: var(--display);
  font-size: clamp(min(34px, 8.3vw), 7.3vw, 92px);
  font-weight: 300;
  text-transform: uppercase;
  letter-spacing: .06em;
  line-height: 1.05;
  color: #fff;
  white-space: nowrap;
}

/* THE NAME MUST CLEAR THIS LINE. On a 1.05 line height the headline's box stops
   almost on its own baseline, so 14px of margin read as nothing and the two
   lines touched. 18px is the gap Brett asked for in round 4 and it is measured
   from the headline's box, not from the letterforms. Round 5 changed the size of
   the name above and nothing here, exactly as the note says.
   The size went from 11px to 17px in that same round 4 note. The tracking is
   unchanged at .34em, and the clamp only steps the line down on a phone, where
   17px at that tracking runs to three lines inside the hero.

   THIS LINE LIVES INSIDE THE HEADLINE, so it inherits the `nowrap` above.
   Left inherited, "MORTGAGE LOAN OFFICER, THE LENDING PROS" at .34em of
   tracking is about 640px of unbreakable text and it takes the document out
   past the screen on every phone. It wraps, and it must. */
.hero-role {
  display: block;
  white-space: normal;
  font-size: clamp(13px, 1.5vw, 17px);
  font-weight: 500;
  letter-spacing: .34em;
  text-transform: uppercase;
  /* .92, not .82: with .34em tracking this line has very little ink, so it
     loses legibility over a photograph long before the headline does. */
  color: rgba(255, 255, 255, .92);
  margin-top: 18px;
}

.hero-sub {
  font-family: var(--text);
  font-size: clamp(16px, 1.6vw, 19px);
  line-height: 1.7;
  color: rgba(255, 255, 255, .9);
  max-width: 56ch;
  margin: 0 auto 30px;
}

/* ROUND 8 PART 3. The line holds ONE ROW on a desktop, and it does it by being
   a shorter sentence rather than smaller type; the size is the round 4 one,
   untouched. Below 1024px it wraps and it should: a phone cannot hold it.

   THE CAP COMES OFF WITH THE WRAP. `max-width: 56ch` is a reading measure and
   the sentence is wider than it, so leaving both in place would push the text
   out of its own box rather than keep it in one line. Measured at 1024, the
   narrowest width this rule runs at: the sentence draws 604px inside 984px of
   container, and 701px inside 1164px at 1204. The Spanish twin is about a fifth
   longer and still clears, which is the reason the rule starts at 1024 rather
   than at the 860px the layout changes at. */
@media (min-width: 1024px) {
  .hero-sub {
    max-width: none;
    white-space: nowrap;
  }
}

/* The one place the accent cannot carry the emphasis. `--blue` on a night
   photograph is unreadable, so the hero's emphasis takes the light end of the
   same hue, which is what every other dark ground on this site uses. */
.hero-sub strong {
  color: var(--blue-light);
  font-weight: 600;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: center;
}

/* The phone override is GONE, and deliberately. It set the name to 13vw, which
   is 50.7px at 390px and wider than the screen once the name may not wrap. The
   clamp above now carries every width by itself, so there is one rule for the
   name and no breakpoint that can disagree with it. */

/* THE CREDENTIALS STRIP REPLACED THIS. The block is kept, unused by the
   generator, because the marquee is the optional half of the upgrade and this
   is what it reverts to. Printing both would say NAHREP twice on one screen. */
.hero-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 26px;
  margin-top: 34px;
  padding-top: 22px;
  border-top: 1px solid rgba(255, 255, 255, .26);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .82);
}

/* ------------------------------------------------- credentials strip */
/* A slow marquee of the credentials, in a thin dark band directly under the
   hero. `overflow: hidden` on the band is what keeps a track wider than the
   page off the sweep: the track is `max-content` and must never be allowed to
   widen the document.

   FOUR IDENTICAL SETS, NOT TWO, AND THAT IS THE ROUND 4 FIX. The rule for a
   seamless marquee is not "duplicate the set", it is "the sets you are NOT
   translating through must still cover the widest screen". Two sets translating
   to -50% only works while ONE set is wider than the viewport. This set
   measures about 940px, so at 1920 the band ran the second set across the left
   half of the screen and showed nothing at all across the right half until the
   loop came round. That is the blank band Brett saw.
   Four sets translating to -25% leave THREE sets, about 2,820px, covering the
   screen at every moment, which clears 2560 with room. If the credential list
   is ever shortened, re-check that (sets - 1) x set width still beats the
   widest viewport before dropping one. */
.credstrip {
  background: var(--teal-deep);
  color: var(--cream-dark);
  overflow: hidden;
  padding: 13px 0;
}

/* THE MASK BELONGS ON THIS WRAPPER, NOT ON THE BAND, and that is the whole
   reason the wrapper exists. A mask applies to the element's own background as
   well as to its content, so masking `.credstrip` faded the deep teal itself
   out at both ends and let the cream page show through. What the reader saw was
   a white fade at each edge of a dark band. Masking an inner wrapper leaves the
   band's colour solid edge to edge and dissolves only the words, over the
   band's own ground, with no colour in the fade at all.

   It also cannot go on `.credstrip-track`: the track translates, so a mask on
   it would travel across the page with the animation instead of staying at the
   two edges. */
.credstrip-mask {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
}

.credstrip-track {
  display: flex;
  width: max-content;
  /* Still 34s. The track doubled in length but the translate halved with it,
     so the distance travelled is one set either way and the reading pace on
     screen is unchanged. Change one of the two numbers and change the other. */
  animation: credscroll 34s linear infinite;
}

.credstrip:hover .credstrip-track {
  animation-play-state: paused;
}

.credstrip-set {
  display: flex;
  flex: none;
  align-items: center;
  gap: 34px;
  padding-right: 34px;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: .22em;
  text-transform: uppercase;
  white-space: nowrap;
}

.credstrip-set span {
  display: inline-flex;
  align-items: center;
  gap: 34px;
}

.credstrip-set span::after {
  content: '\25C6';
  font-size: 7px;
  letter-spacing: 0;
  color: rgba(255, 255, 255, .7);
}

/* -25%, matching FOUR sets in the track. One quarter of four sets is exactly
   one set, so the frame the loop restarts on is pixel-identical to the frame
   it started on and there is no seam. */
@keyframes credscroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-25%); }
}

/* ------------------------------------------------------------ page headers */
/* [CHECKLIST 5] Interior headers clear the bar height.

   ROUND 5 PART 4, AND IT IS ONE RULE FOR EVERY INTERIOR HEADER ON THE SITE.
   The reader reported the About header text sitting lower than the rest, and
   the measurement said why: the text was anchored to the BOTTOM of the frame
   with `align-items: flex-end`, and the frames were not the same height, so
   where a heading landed depended on how many lines of lede that page happened
   to carry. A page's own content was moving its own title.

   Both halves of that are fixed here and nowhere else:
     1. The text is anchored to the TOP, one shared padding above it, so the
        eyebrow starts at the same y on every page.
     2. One shared min-height, 44svh with a 380px floor, so no page's header is
        taller than another's.
   THERE ARE NO PER PAGE AND NO PER VARIANT OVERRIDES OF EITHER. The moment one
   page gets its own padding or its own height this defect is back, so a header
   that needs more room raises the numbers here for all of them. */
.page-header {
  position: relative;
  display: flex;
  align-items: flex-start;
  min-height: max(44vh, 380px);
  min-height: max(44svh, 380px);
  padding: calc(var(--nav-h) + 48px) 0 48px;
  overflow: hidden;
  background: var(--sand);
}

/* Same oversize contract as the hero layer: 18% of room against a 14% travel,
   clamped again in site.js. */
.page-header-media {
  position: absolute;
  left: 0;
  right: 0;
  top: -18%;
  height: 136%;
  z-index: 0;
  will-change: transform;
}

.page-header-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* THE SCRIM IS ON THE SECTION, NOT ON THE MOVING LAYER. If it rides the
   parallax it slides off the title it exists to protect. Absolutely
   positioned, so it is not a flex item of the header.

   ROUND 5: THE SCRIM FOLLOWED THE TEXT, exactly as the hero's did in round 4.
   A plain vertical wash is at its weakest in the middle of the frame, which was
   safe while the text was anchored to the bottom. Part 4 moved the text to the
   TOP of a shorter frame, and it landed in that weak middle: GET IN TOUCH in
   thin white serif over a pale sky was the result, and it was measurably worse
   than the build it replaced.
   So the protection is a soft pool where the words actually are, over on the
   left where the container puts them, plus the standing vertical wash. The wash
   still opens to .28 in the middle so the RIGHT of every photograph is as clear
   as it was before; only the left, under the words, is carried. A HEAVIER FLAT
   SCRIM IS NOT THE ANSWER, for the same reason it was not the answer on the
   hero: it dims the picture uniformly, and the picture is why the header is a
   photograph rather than a band of sand. */
.page-header.has-media::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background:
    radial-gradient(ellipse 78% 88% at 32% 46%, rgba(var(--scrim), .70) 0%, rgba(var(--scrim), .54) 46%, rgba(var(--scrim), .18) 78%, rgba(var(--scrim), 0) 100%),
    linear-gradient(to bottom, rgba(var(--scrim), .60) 0%, rgba(var(--scrim), .28) 48%, rgba(var(--scrim), .30) 76%, rgba(var(--scrim), .50) 100%);
}

/* On a phone the heading and the lede run the full width of the frame, so the
   pool spreads to match rather than sitting in the left third. */
@media (max-width: 860px) {
  .page-header.has-media::after {
    background:
      radial-gradient(ellipse 130% 86% at 50% 46%, rgba(var(--scrim), .72) 0%, rgba(var(--scrim), .58) 48%, rgba(var(--scrim), .24) 80%, rgba(var(--scrim), 0) 100%),
      linear-gradient(to bottom, rgba(var(--scrim), .62) 0%, rgba(var(--scrim), .32) 48%, rgba(var(--scrim), .34) 78%, rgba(var(--scrim), .50) 100%);
  }
}

.page-header .container {
  position: relative;
  z-index: 1;
}

.page-header.has-media .container {
  color: #fff;
}

.page-header.has-media .eyebrow {
  color: rgba(255, 255, 255, .84);
}

.page-header.has-media .lede {
  color: rgba(255, 255, 255, .9);
}

/* ROUND 5 PART 2.2, AND THE TWO RULES ARE NOW ONE. A header with a photograph
   ran up to 92px while a plain one ran to 60px, so GET IN TOUCH and ABOUT
   JOSEPH were set larger than any heading in the page below them. Both carry
   the reference build's interior rule, clamp(36px, 4vw, 54px), which lands on
   48px at 1204 and reaches 54px at 1350 and up. One rule, so a header cannot
   drift away from its own page again. */
.page-header-title {
  font-size: clamp(36px, 4vw, 54px);
  line-height: 1.05;
  text-transform: uppercase;
  letter-spacing: .04em;
}

.page-header .lede {
  margin: 16px 0 0;
}

/* A header with no photo, used where doubling an image would break the
   one-image-per-slot rule: the six articles and the three legal pages.
   IT CARRIES NO HEIGHT AND NO PADDING OF ITS OWN, since round 5. It used to
   set `min-height: auto`, which is exactly the kind of per variant override
   that made the About header sit lower than Contact's. It takes the shared
   frame above and adds only its own ground and its own hairline. */
.page-header.plain {
  background: var(--sand);
  border-bottom: 1px solid var(--line);
}

/* ---------------------------------------------------------------- sections */
.section {
  padding: var(--gap-section) 0;
}

.section-sand {
  background: var(--sand);
}

.section-paper {
  background: var(--paper);
}

.section-ink {
  background: var(--ink);
  color: rgba(255, 255, 255, .84);
}

.section-ink h2,
.section-ink h3 {
  color: #fff;
}

.section-ink .eyebrow {
  color: rgba(255, 255, 255, .68);
}

/* --------------------------------------------------------- split, 2 column */
.split {
  display: grid;
  grid-template-columns: 1.05fr .95fr;
  gap: clamp(28px, 4vw, 64px);
  align-items: center;
}

.split-media {
  position: relative;
}

.split-media img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.split-media.portrait img {
  aspect-ratio: 4 / 5;
}

/* NO DECORATIVE RECTANGLE ON A PHOTOGRAPH. `.split-media::after` used to stamp
   a 1px accent square into the bottom right corner of every picture that runs
   through this component, which is the Start Here photograph, the calculators
   photograph, the home portrait and the About headshot. It read as a box drawn
   onto the image rather than as a frame, so it is gone. Do not reintroduce it:
   a photograph on this site carries nothing on top of it. */

@media (max-width: 860px) {
  .split {
    grid-template-columns: 1fr;
  }

  /* [CHECKLIST 7] Explicit order on mobile: the words come first, the picture
     second. Source order alone leaves half the splits upside down. */
  .split-text {
    order: 1;
  }

  .split-media {
    order: 2;
  }
}

/* -------------------------------------------------------- index rows */
/* THESE WERE BOXES. A grid of bordered squares is the shape every template
   site arrives in, and it wastes the one thing this layout has, which is
   width. The same markup is now a run of full-bleed hairline rows: a number, a
   title, the sentence, and an arrow that only appears under the cursor.

   [CHECKLIST] A component FILLS the column it is given, and a row list fills it
   by construction. The class name stays `card-grid` because nine generated
   templates and the responsive check's FILL list both name it. */
.card-grid {
  display: block;
  margin-top: 44px;
  border-top: 1px solid var(--line);
}

.card {
  display: grid;
  grid-template-columns: 64px minmax(0, 1.05fr) minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(18px, 3vw, 40px);
  padding: 30px 0;
  border-bottom: 1px solid var(--line);
  background: none;
  border-radius: 0;
  box-shadow: none;
  transition: background .4s var(--ease);
}

a.card {
  color: inherit;
}

/* The index. 01 to 09, in the accent, so the eye can count the list. */
.card-num {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .24em;
  color: var(--accent-teal);
  margin: 0;
}

.card .h-card {
  margin: 0;
  font-size: 22px;
  transition: transform .4s var(--ease), color .4s var(--ease);
}

.card p {
  color: var(--ink-soft);
  font-size: 16px;
  margin: 0;
}

/* The arrow rests hidden and slides in from the left on hover, which is what
   makes a row feel like an index entry rather than a table cell. */
.card .link-more {
  justify-self: end;
  margin: 0;
  opacity: 0;
  transform: translateX(-8px);
  transition: opacity .25s var(--ease), transform .25s var(--ease);
}

/* The whole row is the link, so the whole row takes the hover ground. The
   negative inline margin lets the sand run a little past the text on both
   sides without moving anything; it is smaller than the container's own
   gutter at every width, so it cannot reach the edge of the page. */
a.card {
  padding-inline: 14px;
  margin-inline: -14px;
  transition: background .25s var(--ease);
}

a.card:hover,
a.card:focus-visible {
  background: var(--sand);
}

a.card:hover .h-card {
  transform: translateX(6px);
  color: var(--accent-teal);
}

a.card:hover .link-more,
/* Keyboard parity. A row reached by Tab must show the same arrow a mouse gets,
   or the affordance exists only for people using a pointer. */
a.card:focus-visible .link-more {
  opacity: 1;
  transform: translateX(0);
}

/* Below 1024 a four column row has nowhere left to go: the number keeps a
   narrow gutter, the title and the sentence stack in the remaining column, and
   the arrow is dropped rather than shrunk. */
@media (max-width: 1024px) {
  .card {
    grid-template-columns: 64px minmax(0, 1fr);
    align-items: start;
    gap: 8px 20px;
    padding: 26px 0;
  }

  .card .h-card,
  .card p {
    grid-column: 2;
  }

  .card-num {
    grid-row: 1 / span 2;
  }

  .card .link-more {
    display: none;
  }

  a.card:hover .h-card {
    transform: none;
  }
}

@media (max-width: 560px) {
  .card {
    grid-template-columns: 44px minmax(0, 1fr);
    gap: 6px 14px;
  }
}

/* ============================== the sticky counter, round 3 parts 2 and 3 */
/* THE LOAN LIST IS NOT A LEDGER OF ROWS ANY MORE. The left column pins while
   the loans scroll past it on the right, a serif counter ticks to whichever
   loan is nearest the middle of the screen, and only that loan renders in full
   ink. The home page runs it at four and the Loan Options page at nine; there
   is one component and the content is the only difference.

   [CHECKLIST] The component FILLS its column: the right track is 1.5fr of a
   two track grid and the blocks are `display: block` inside it. */
.loanproc {
  display: grid;
  grid-template-columns: 1fr 1.5fr;
  gap: clamp(32px, 5vw, 64px);
  align-items: start;
  margin-top: 8px;
}

/* THE PIN CARRIES NO TRANSFORM AND NO FILTER OF ITS OWN. The reveal goes on
   the `.section-head` inside it rather than on this element, because a
   `[data-reveal]` here would put a blur and a translate on the sticky box
   itself and the two do not want to be on one element. */
.loanproc-left {
  position: sticky;
  top: calc(var(--nav-h) + 32px);
}

.loanproc-counter {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(48px, 4.5vw, 64px);
  line-height: 1;
  margin: 4px 0 0;
  color: var(--ink);
}

/* The number leaves in the direction of travel and the next one arrives behind
   it, so the track has to clip and hold its own width. `min-width` is in `em`
   so it tracks the clamped font size instead of needing a second breakpoint. */
.lp-track {
  position: relative;
  display: inline-block;
  height: 1em;
  min-width: 1.35em;
  overflow: hidden;
}

.lp-num {
  display: inline-block;
  transition: transform .2s var(--ease), opacity .2s var(--ease);
}

.lp-num.exit-up,
.lp-num.exit-down {
  position: absolute;
  left: 0;
  top: 0;
  opacity: 0;
}

.lp-num.exit-up { transform: translateY(-110%); }
.lp-num.exit-down { transform: translateY(110%); }
.lp-num.enter-below { transform: translateY(110%); opacity: 0; }
.lp-num.enter-above { transform: translateY(-110%); opacity: 0; }
.lp-num.settled { transform: translateY(0); opacity: 1; }

.lp-total {
  font-size: .42em;
  color: var(--ink-faint);
}

.loanproc-lede {
  font-family: var(--display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(18px, 1.6vw, 21px);
  line-height: 1.5;
  color: var(--ink-soft);
  max-width: 26ch;
  margin: 16px 0 0;
}

.loanproc-items {
  display: flex;
  flex-direction: column;
  gap: clamp(40px, 4.4vw, 64px);
}

.loan-item {
  display: block;
  color: inherit;
  text-decoration: none;
}

/* Desktop counts with the pinned counter, so the per-block number is a phone
   affordance and nothing else. */
.loan-item-num {
  display: none;
}

.loan-item h3 {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(26px, 2.6vw, 32px);
  line-height: 1.2;
  margin: 0 0 8px;
  transition: color .25s var(--ease);
}

.loan-item p {
  font-family: var(--text);
  font-size: 18px;
  line-height: 1.75;
  color: var(--ink-soft);
  max-width: 54ch;
  margin: 0;
  transition: color .25s var(--ease);
}

.loan-item .go {
  display: inline-block;
  margin-top: 12px;
  font-family: var(--body);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--blue);
  transition: opacity .25s var(--ease), transform .25s var(--ease);
}

.loan-item .go::after {
  content: '\2192';
  margin-left: 9px;
}

.loan-item:hover h3,
.loan-item:focus-visible h3 {
  color: var(--blue);
}

/* THE SETTING BACK IS DESKTOP ONLY, and that is the whole of the mobile
   answer. Below 1024px there is no pinned column and no counter, so a phone
   shows one block at a time and muting the other three would just be hiding
   the content. The base state above is therefore full ink with the details
   link showing, which is also what a visitor with JavaScript off gets. */
@media (min-width: 1024px) {
  .js .loan-item h3,
  .js .loan-item p {
    color: var(--ink-set);
  }

  .js .loan-item .go {
    opacity: 0;
    transform: translateX(-8px);
  }

  .js .loan-item.is-active h3 {
    color: var(--ink);
  }

  .js .loan-item.is-active p {
    color: var(--ink-soft);
  }

  .js .loan-item.is-active .go,
  .js .loan-item:focus-visible .go {
    opacity: 1;
    transform: translateX(0);
  }
}

@media (max-width: 1023px) {
  .loanproc {
    grid-template-columns: minmax(0, 1fr);
    gap: 30px;
  }

  .loanproc-left {
    position: static;
    top: auto;
  }

  .loanproc-counter {
    display: none;
  }

  .loanproc-lede {
    max-width: 40ch;
  }

  .loanproc-items {
    gap: 34px;
  }

  .loan-item-num {
    display: block;
    font-family: var(--body);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .24em;
    color: var(--blue);
    margin-bottom: 6px;
  }
}

/* ===================================== the learn ink fill, round 3 part 4 */
/* AN ARTICLE IS ITS TITLE. No rules, no boxes, no read times and no tiles.
   Each title arrives blurred and letterspaced wide and settles sharp, and
   whichever title is nearest the middle of the screen fills with ink from the
   left while the rest sit back. ALL OF IT RUNS ON A PHONE.

   The list is a centred 1020px measure inside the 1240px wrap. That is
   typography rather than dead space: at 56px a title running the full 1240 is
   a headline nobody can scan. It is centred, so there is no band of nothing
   down one side either. */
.learn-list {
  display: flex;
  flex-direction: column;
  gap: clamp(34px, 3.6vw, 46px);
  max-width: 1020px;
  margin-inline: auto;
}

.learn-item {
  display: block;
  color: inherit;
  text-decoration: none;
}

.learn-topic {
  display: block;
  font-family: var(--body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--blue);
  margin-bottom: 10px;
}

.learn-title {
  display: block;
  width: fit-content;
  max-width: 100%;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(32px, 4.2vw, 56px);
  line-height: 1.15;
  letter-spacing: .005em;
  margin: 0;
}

/* EVERY RESTING STATE ON THIS COMPONENT IS THE READABLE ONE, and the set-back
   state is added by `.js`. With scripting off nothing ever gets the focus
   class, so a title parked in the set-back colour would stay there forever and
   the topic label and the read link would never appear at all. */
.learn-title {
  color: var(--ink);
}

.js .learn-title {
  color: var(--ink-back);
}

.js .learn-item.is-active .learn-title,
.js .learn-item:hover .learn-title,
.js .learn-item:focus-visible .learn-title {
  color: var(--ink);
}

/* THE INK FILL. A two stop gradient at 205% of the box, clipped to the glyphs:
   parked at 100% the set-back half sits over the words, and sliding it to 0
   wipes ink across from the left. It is inside `@supports` because the
   fallback has to be a title that is simply readable, not an invisible one:
   `color: transparent` in a browser that cannot clip a background to text
   paints nothing at all. The colour rules above are restated here so the
   transparent value wins at every one of their specificities. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .learn-title {
    background-image: linear-gradient(90deg, var(--ink) 50%, var(--ink-back) 50%);
    background-repeat: no-repeat;
    background-size: 205% 100%;
    background-position: 0 0;
    -webkit-background-clip: text;
    background-clip: text;
    transition: background-position .6s var(--ease);
  }

  .learn-title,
  .js .learn-title,
  .js .learn-item.is-active .learn-title,
  .js .learn-item:hover .learn-title,
  .js .learn-item:focus-visible .learn-title {
    color: transparent;
  }

  .js .learn-title {
    background-position: 100% 0;
  }

  .js .learn-item.is-active .learn-title,
  .js .learn-item:hover .learn-title,
  .js .learn-item:focus-visible .learn-title {
    background-position: 0 0;
  }
}

/* Forced colours throw the gradient away, so the title has to be painted with
   a system colour or it disappears on a high contrast desktop. */
@media (forced-colors: active) {
  .learn-title,
  .js .learn-title {
    background-image: none;
    -webkit-text-fill-color: currentColor;
    color: CanvasText;
  }
}

/* ROUND 6 PART 2: THE ONE LINE UNDER THE TITLE, BACK BUT NOT AS A ROW.
   Round 3 took the list down to titles alone and the client finds that thin.
   The line is a teaser at a reading measure, and on a desktop it BELONGS TO
   THE FOCUSED ITEM: it arrives with the read link as the title inks in, so the
   list still reads as one title at a time rather than as six paragraphs.
   Nothing is drawn around it and no rule was added anywhere.

   ON A PHONE EVERY LINE IS SIMPLY SHOWN. The focus is only ever one item on a
   screen that holds one item, so hiding the other five would be hiding the
   content rather than focusing it, which is the same reasoning the loan
   counter's mobile rule already carries. The rest state below is the shown
   one, and the desktop query is what takes it away. */
.learn-excerpt {
  display: block;
  margin-top: 12px;
  font-family: var(--text);
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink-soft);
  max-width: 60ch;
}

@media (min-width: 1024px) {
  .js .learn-item[data-reveal] .learn-excerpt {
    opacity: 0;
    transition: opacity .4s var(--ease);
  }

  .js .learn-item[data-reveal].is-active .learn-excerpt,
  .js .learn-item[data-reveal]:hover .learn-excerpt,
  .js .learn-item[data-reveal]:focus-visible .learn-excerpt {
    opacity: 1;
  }
}

.learn-go {
  display: inline-flex;
  align-items: center;
  margin-top: 12px;
  font-family: var(--body);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--blue);
}

/* The line extends ahead of the words rather than under them, which is what
   stops this reading as one more underlined link. */
.learn-go::before {
  content: '';
  display: inline-block;
  width: 34px;
  height: 1px;
  margin-right: 10px;
  background: currentColor;
  transition: width .35s var(--ease), margin-right .35s var(--ease);
}

.js .learn-go::before {
  width: 0;
  margin-right: 0;
}

.js .learn-item.is-active .learn-go::before,
.js .learn-item:hover .learn-go::before,
.js .learn-item:focus-visible .learn-go::before {
  width: 34px;
  margin-right: 10px;
}

@media (max-width: 1023px) {
  .learn-title {
    font-size: clamp(28px, 8vw, 40px);
  }
}

/* ============================== the learn center, round 6 parts 3 and 4 */
/* THE READING LIST AND THE QUESTIONNAIRE ARE ONE ROW ON A DESKTOP, and the
   form column PINS so it rides alongside the list the whole way down. Round 5
   put the form directly under the header and the client rejected it there.

   THE MARKUP ORDER IS THE PHONE ORDER. Below 1024px this collapses to one
   column and the form follows the last card, because it is second in the
   document. It is never at the top of the page at any width, which is the
   thing the note is actually ruling out. */
.learn-layout {
  display: grid;
  gap: 48px;
}

.learn-main {
  min-width: 0;
}

.learn-aside {
  min-width: 0;
  scroll-margin-top: 104px;
}

@media (min-width: 1024px) {
  .learn-layout {
    grid-template-columns: 1.6fr 1fr;
    align-items: start;
  }

  /* `align-items: start` above is what makes this stick: a stretched grid item
     is already as tall as the row, so it has nothing to travel through.

     The offset is the nav height plus the site's standing 24px, not a flat
     120px, so the one place the bar's height is written stays the one place. */
  .learn-aside {
    position: sticky;
    top: calc(var(--nav-h) + 24px);
  }
}

/* The wizard is the round 4 component, unchanged. These three rules only size
   it to a column narrower than the one it was drawn for, at every width; the
   round 7 block under them takes it down again on a desktop, where the card
   also has to fit the height of the screen it is pinned to. */
.learn-aside .h-section {
  font-size: clamp(26px, 2.2vw, 32px);
}

.learn-aside .wizard-q {
  font-size: clamp(24px, 1.9vw, 28px);
}

/* THE BAR SPANS THE COLUMN. In the full width card the count sits beside the
   bar; in a 380px column that leaves the bar too short to read as progress, so
   the head stacks and the bar takes the whole width under the count. */
.learn-aside .wizard-head {
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
}

.learn-aside .wizard-count {
  order: -1;
}

/* ROUND 7: A PINNED CARD HAS TO FIT THE SCREEN IT IS PINNED TO, and on a laptop
   this one did not. The deck was drawn for a full width band, so in the sidebar
   it stood 731px tall on its tallest step and sat 120px down the screen. That is
   851px of card against about 690px of viewport on a 1280 by 800 laptop and
   about 780px on a 1440 by 900 one, so CONTINUE was under the fold on both and
   the reader had to scroll the page to answer a question that is supposed to
   ride beside the reading list.

   So the sidebar takes a compact scale of the same component. Nothing is
   removed, no step is rewritten and no protection is touched: only the type, the
   padding and the gaps come down. Measured after the change on all four steps,
   the tallest step of each: 580px at 1024, 545px at 1280, 555px at 1440 and
   565px at 1920. With the 112px offset above, that is 692px at its worst, so the
   whole card is on the screen at every laptop height.

   THIS BLOCK MUST STAY BELOW THE THREE RULES ABOVE. They are not in a media
   query, so at equal specificity the later block is the one that wins; moved up,
   the 32px heading and the 28px question would quietly come back.

   IT IS SCOPED TO THE TWO COLUMN LAYOUT ON PURPOSE. Below 1024px the form is
   full width under the last card with the whole page to scroll, and the same
   deck at the foot of every article is not in `.learn-aside` at all, so both
   keep the round 4 sizes. */
@media (min-width: 1024px) {
  .learn-aside .section-head {
    margin-bottom: 16px;
  }

  .learn-aside .h-section {
    font-size: clamp(23px, 1.8vw, 27px);
  }

  .learn-aside .wizard {
    padding: clamp(20px, 1.6vw, 26px);
  }

  .learn-aside .wizard-head {
    margin-bottom: 16px;
    gap: 8px;
  }

  .learn-aside .wizard-q {
    font-size: clamp(20px, 1.5vw, 24px);
  }

  .learn-aside .wizard-help {
    font-size: 13px;
    margin-bottom: 14px;
  }

  /* ONE ANSWER PER ROW, AND THE PADDING IS WHY IT HAD TO BE WRITTEN DOWN. At
     38px a side the answers could not make the 190px track twice and stacked by
     themselves; taking the padding down handed them the room for two columns,
     which put a one word answer beside a two line one and left an empty cell
     under them. The height one column costs is paid back by the padding. */
  .learn-aside .choice-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 8px;
  }

  .learn-aside .choice {
    padding: 11px 14px;
    font-size: 14px;
  }

  .learn-aside .choice small {
    margin-top: 2px;
    font-size: 12px;
  }

  .learn-aside .form {
    gap: 10px;
  }

  .learn-aside .field {
    gap: 5px;
  }

  /* 15px is safe here and only here. The 16px floor on an input exists so a
     phone does not zoom the page when the field takes focus, and this block
     never runs below 1024px. */
  .learn-aside .field input {
    padding: 9px 12px;
    font-size: 15px;
  }

  .learn-aside .consent label {
    font-size: 12px;
  }

  .learn-aside .wizard-actions {
    margin-top: 14px;
    padding-top: 14px;
  }
}

/* ------------------------------------------------------- the topic filter */
/* A chip row over the grid. It is not on the reference, which carries no
   filter at all; it is Brett's own fallback spec and it is worth having on a
   page whose whole job is finding the article that answers your question.
   The chips are BUILT FROM THE ARTICLES, so a new topic appears by itself.

   ROUND 7: THE ROW IS AN UNDERLINE BAR AND THE UNDERLINE SLIDES. It shipped as
   six filled pills, so the chosen topic SWITCHED from one pill to another with
   nothing travelling between them. One rule under the whole row, moved to the
   chip that was pressed, is the state the note asks for and it is also the only
   version that reads as one control rather than six.
   The rule is positioned against `.topic-filter`, so that box has to stay
   `position: relative` and the chips have to stay in one row box for
   `offsetLeft` to mean what site.js thinks it means. */
.topic-filter {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: clamp(16px, 2vw, 26px);
  border-bottom: 1px solid var(--line);
  margin-bottom: 30px;
}

.topic-chip {
  padding: 12px 0;
  border: 0;
  background: none;
  font-family: var(--body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-soft);
  cursor: pointer;
  transition: color .2s var(--ease);
}

.topic-chip:hover,
.topic-chip[aria-pressed="true"] {
  color: var(--ink);
}

/* THE RULE IS DECORATION AND `aria-pressed` IS THE STATE. A screen reader is
   told which chip is chosen by the attribute, so this carries `aria-hidden` in
   the markup and nothing here has to announce anything. */
/* `top` IS WRITTEN BY site.js AS WELL AS `left`, and on a phone that is what
   keeps it honest. The row wraps to two lines below about 560px, so a rule
   pinned to the bottom of the whole box would sit under the SECOND row while a
   chip on the first row was the chosen one. Taking the offset from the chip
   puts it under whichever chip was pressed, on either row. */
.chip-line {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 2px;
  background: var(--blue);
  transition: top .3s var(--ease), left .3s var(--ease), width .3s var(--ease);
}

/* --------------------------------------------------------- the topic cards */
/* THE ROUNDING HERE IS AN EXCEPTION THE NOTE ASKED FOR BY NAME. Round 4 scoped
   soft geometry to the wizard, the calculator results card and the one pill,
   and everything else on this site is square. Round 6 puts the wizard beside
   these cards in one row, so 12px on the card is what stops the two halves of
   the row reading as two different sites. It is scoped to this page's grid and
   it stays scoped. */
/* ROUND 7 PART 2.3: THE GRID'S HEIGHT IS ANIMATED, WHICH IS WHY THE PAGE STOPS
   JUMPING. Measured on the live page before the fix: 2006px before the CREDIT
   chip was pressed and 462px one frame after it, because `display: none`
   landed in a single frame and the whole document lurched up under the reader.
   site.js pins the old height, reads the new one, then eases between the two
   and releases the property back to `auto`.

   `overflow` IS ONLY HIDDEN WHILE IT MOVES. Growing from a short grid to a tall
   one means the incoming cards are outside the pinned box for those 350ms, and
   without the clip they paint over the section below. It goes back the moment
   the height is released, so a card's hover lift and shadow are never clipped
   at rest. It is safe here and only here: `.topic-grid` is not an ancestor of
   the pinned questionnaire beside it, so nothing about the sticky column is
   touched. Put a clip on `.learn-layout` or `.learn-main` and the pin dies. */
.topic-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 22px;
  transition: height .35s var(--ease);
}

.topic-grid.is-sizing {
  overflow: hidden;
}

@media (max-width: 1023px) {
  .topic-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  }
}

/* THE ANCHOR IS THE CELL, THE INNER SPAN IS THE CARD, and the split is what
   makes the hover possible at all. The reveal system owns `transform` on the
   element carrying `data-reveal`, and its settled `transform: none` sits at a
   higher specificity than any hover rule on the same element, so a lift
   written on the anchor never runs. Keeping the two jobs on two elements also
   means the arrival and the hover can never fight over one property. */
.topic-card {
  display: block;
  height: 100%;
  min-width: 0;
  color: inherit;
}

.topic-card-inner {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--paper);
  box-shadow: 0 2px 6px rgba(16, 20, 24, .04);
  transition: transform .4s var(--ease), box-shadow .4s var(--ease);
}

.topic-card:hover,
.topic-card:focus-visible {
  color: inherit;
}

.topic-card:hover .topic-card-inner,
.topic-card:focus-visible .topic-card-inner {
  transform: translateY(-6px);
  box-shadow: 0 18px 38px rgba(16, 20, 24, .12), 0 4px 10px rgba(16, 20, 24, .06);
}

/* THE FRAME CLIPS AND THE PICTURE SCALES INSIDE IT. `overflow: hidden` here as
   well as on the card, because the card's own radius is what the picture must
   stay inside while it grows. */
.topic-card-media {
  display: block;
  overflow: hidden;
  background: var(--sand);
}

.topic-card-media img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  transition: transform .4s var(--ease);
}

.topic-card:hover .topic-card-media img,
.topic-card:focus-visible .topic-card-media img {
  transform: scale(1.05);
}

.topic-card-body {
  display: block;
  padding: 24px 24px 26px;
  flex: 1 1 auto;
}

.topic-card-label {
  display: block;
  font-family: var(--body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--blue);
  margin-bottom: 10px;
}

/* The underline draws under the title rather than under the whole card, so the
   card still has one thing in it that answers a pointer. It is the only hover
   state that survives Reduce Motion. */
.topic-card-title {
  display: inline;
  font-family: var(--display);
  font-size: 22px;
  font-weight: 300;
  line-height: 1.25;
  margin: 0;
  background-image: linear-gradient(var(--blue), var(--blue));
  background-repeat: no-repeat;
  background-size: 0 2px;
  background-position: 0 100%;
  /* `clone`, so a title that runs to two lines gets an underline the width of
     each line rather than one gradient stretched across the whole inline box. */
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  transition: background-size .35s var(--ease);
}

.topic-card:hover .topic-card-title,
.topic-card:focus-visible .topic-card-title {
  background-size: 100% 2px;
}

.topic-card-excerpt {
  display: block;
  margin-top: 12px;
  font-family: var(--text);
  font-size: 15px;
  line-height: 1.65;
  color: var(--ink-soft);
}

/* A TEXT CARD IN A ROW WITH A PICTURE CARD SETS ITS WORDS AT THE FOOT. The
   three article pictures landed on 2026-09-10 and the other three articles
   still have none, so one row pairs the two kinds. The row stretches the text
   card to the picture card's height, and with its words at the top it ended in
   about 230px of empty white. At the foot, its label, title and line sit level
   with the neighbour's. site.js sets the class per row; a row of two text cards
   never carries it. Remove this when every article has its own picture. */
.topic-card.is-beside-media .topic-card-body {
  flex: 0 0 auto;
  margin-top: auto;
}

/* ============ THE ARRIVAL AND THE FILTER ARE TWO SYSTEMS, NOT ONE, ROUND 7 ===
   This is the whole of part 1 of the round 7 note, and every symptom in it came
   from the two sharing one set of transitions.

   Round 6 tried to make them share and it does not work. `.js [data-reveal]`
   owns `opacity` at 800ms and carries the stagger's own `transition-delay`, up
   to 550ms on the sixth card, so the filter was borrowing an 800ms fade and
   fighting a `translateY(30px)` hidden state at the same time. Worse, a card
   that had never revealed — anything below the fold in a 2006px grid — had no
   visible state of its own, so a survivor measured `opacity: 0` a full 900ms
   after the chip was pressed and simply was not there.

   SO A REVEALED CARD IS HANDED OVER PERMANENTLY. site.js adds `is-settled` once
   a card's arrival has finished, and from that moment this rule owns the card:
   opacity, transform and blur are all pinned to their arrived values, the 800ms
   transition is replaced by the filter's own short pair, and the stagger delay
   is dropped. THE FILTER CAN NEVER UN-REVEAL A CARD, because nothing the filter
   does can put back a hidden state this rule has already overwritten.

   The three classes below are the filter's own states and they are the file's:
   180ms to fade an outgoing card while it is still in the layout, then 350ms
   for the survivors to glide to their new cells and the newcomers to rise into
   theirs. `is-settled` beats `.js [data-reveal]` on specificity, and the
   survivors' FLIP transforms beat everything because site.js writes them
   inline. */
.js .topic-card[data-reveal].is-settled {
  opacity: 1;
  transform: none;
  filter: none;
  transition: opacity .18s ease, transform .35s var(--ease);
  transition-delay: 0ms;
}

.js .topic-card[data-reveal].is-settled.is-out {
  opacity: 0;
  transform: scale(.96);
  pointer-events: none;
}

.js .topic-card[data-reveal].is-settled.is-new {
  opacity: 0;
  transform: translateY(14px);
}

.topic-card[hidden] {
  display: none;
}

/* The topic label at the head of an article, where the read time used to be.
   It sits inside `.prose`, so it needs to clear `.prose p`'s soft ink. */
p.article-topic {
  font-family: var(--body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--blue);
  margin: 0 0 18px;
}

/* The text link that closes a ledger: "See all nine loan options". */
.ledger-foot {
  margin: 28px 0 0;
}

/* ------------------------------------------------- situation shortcuts */
/* Loan Options opens on three routes by situation, then the full nine row
   ledger underneath. Two ways in: a shortcut for somebody who knows what they
   are trying to do, and a complete scannable list for somebody who does not.
   Deliberately a different shape from the ledger, so the reader can tell at a
   glance that they are two different things and not one list twice. */
.route-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 18px;
  margin-top: 36px;
}

@media (max-width: 860px) {
  .route-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

.route {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
  padding: 26px 24px;
  border: 1px solid var(--line);
  background: var(--paper);
  color: inherit;
  transition: border-color .25s var(--ease), box-shadow .25s var(--ease);
}

.route:hover,
.route:focus-visible {
  border-color: var(--blue);
  box-shadow: 0 10px 26px rgba(16, 20, 24, .06);
  color: inherit;
}

.route h3 {
  font-family: var(--display);
  font-size: 24px;
  font-weight: 300;
  margin: 0;
}

.route p {
  margin: 0;
  font-family: var(--text);
  font-size: 15px;
  line-height: 1.65;
  color: var(--ink-soft);
}

.route .link-more {
  margin-top: auto;
  padding-top: 6px;
}

/* ------------------------------------------------- the loan detail page */
/* ROUND 4 PART 9. Five elements: the header, this paragraph, the checklist,
   the facts row and one CTA. Round 7 sets it in the serif text cut at 18px in a
   62ch measure, which is a reading measure and therefore a deliberate cap
   rather than dead space. */
.loan-intro {
  font-family: var(--text);
  max-width: 62ch;
  margin: 0;
  font-size: 18px;
  line-height: 1.75;
  color: var(--ink-soft);
}

.loan-fit {
  margin-top: var(--gap-section);
}

.loan-fit + .facts {
  margin-top: var(--gap-section);
}

/* ------------------------------------------- the six Non-QM types, round 10 */
/* PART 3 of the round 10 notes: one page, six labelled sections, each with its
   own anchor. It is the site's own row idiom, hairline between rows and nothing
   drawn around anything, riding in the `sticky-split` so the six fill the
   column instead of trailing a third of the page down their right.

   THE `scroll-margin-top` IS THE POINT OF THE ANCHORS. The bar is fixed and 88
   pixels tall, so a landing on /loan-options/non-qm-loans/#dscr would otherwise
   put the heading it was aimed at underneath the bar. It is set on the section
   rather than globally, because these six are the only addressable fragments on
   the site. */
.loan-types {
  margin-top: var(--gap-section);
}

.loan-types + .facts {
  margin-top: var(--gap-section);
}

.loan-type {
  padding: 26px 0;
  border-top: 1px solid var(--line);
  scroll-margin-top: calc(var(--nav-h) + 24px);
}

.loan-type:first-child {
  border-top: 0;
  padding-top: 0;
}

.loan-type h3 {
  font-family: var(--display);
  font-size: 22px;
  font-weight: 400;
  margin: 0 0 8px;
}

.loan-type p {
  font-family: var(--text);
  font-size: 17px;
  line-height: 1.7;
  color: var(--ink-soft);
  margin: 0;
  max-width: 62ch;
}

/* A labelled value is interface, so it takes Inter and the eyebrow's tracking
   while the paragraph above it stays in the reading serif. Same split as the
   fit lists and the facts row. */
.loan-type .loan-type-who {
  font-family: var(--body);
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-set);
  margin-top: 10px;
}

.loan-type .loan-type-who span {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--blue);
  margin-right: 10px;
}

/* ------------------------------------------------------- quick facts row */
/* Three facts per loan. Program guidelines only: a minimum down payment, who
   qualifies, how long it takes. NO RATES, NO APRs AND NO "AS LOW AS" — see the
   compliance note at the top of _build/loans.mjs. */
.facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(22px, 3vw, 48px);
}

@media (max-width: 860px) {
  .facts {
    grid-template-columns: minmax(0, 1fr);
  }
}

.fact {
  min-width: 0;
  border-top: 1px solid var(--line);
  padding-top: 16px;
}

.fact-label {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--blue);
  margin-bottom: 8px;
}

.fact-value {
  margin: 0;
  font-size: 17px;
  line-height: 1.55;
  color: var(--ink);
}

/* --------------------------------------------------------- fit checklist */
/* "This loan fits you if", four or five lines, each check drawn in rather than
   simply appearing. The wipe runs along the glyph's own diagonal because the
   box is rotated 45 degrees first, so an inset from the right reads as a pen
   stroke. The stagger is 120ms and it lives in CSS rather than in site.js,
   because the list is one revealed element and the steps are inside it. */
.fit-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 14px;
}

.fit-list li {
  position: relative;
  padding-left: 30px;
  font-size: 17px;
  color: var(--ink-soft);
}

.fit-list li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 9px;
  width: 10px;
  height: 10px;
  border-right: 2px solid var(--blue);
  border-bottom: 2px solid var(--blue);
  transform: rotate(45deg);
}

.js .fit-list[data-reveal] li::before {
  clip-path: inset(0 100% 0 0);
  transition: clip-path .45s var(--ease);
  transition-delay: var(--cd, 0ms);
}

.js .fit-list[data-reveal].is-in li::before {
  clip-path: inset(0 0 0 0);
}

.fit-list li:nth-child(1) { --cd: 200ms; }
.fit-list li:nth-child(2) { --cd: 320ms; }
.fit-list li:nth-child(3) { --cd: 440ms; }
.fit-list li:nth-child(4) { --cd: 560ms; }
.fit-list li:nth-child(5) { --cd: 680ms; }
.fit-list li:nth-child(6) { --cd: 800ms; }

/* ============================ recognition, engraved credits, round 3 part 6 */
/* NO BOXES, NO ROWS, NO RULES AND NO TAGS. Two centred credits: a small accent
   year, the award in large serif, one line under it, and the marquee's own
   diamond turning upright between them. Each name arrives blurred and
   letterspaced wide and settles sharp, which is the same settle the Learn
   titles use, so the site's two editorial moments rhyme. */
.recog {
  max-width: 900px;
  margin-inline: auto;
  text-align: center;
}

.recog .eyebrow {
  margin-bottom: 34px;
}

.award {
  padding: 26px 0;
}

.award-year {
  font-family: var(--body);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .25em;
  color: var(--blue);
  margin: 0;
}

.award-name {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(30px, 3.2vw, 46px);
  line-height: 1.25;
  letter-spacing: .01em;
  margin: 10px 0 0;
  color: var(--ink);
}

.award-desc {
  font-size: 16px;
  line-height: 1.7;
  color: var(--ink-soft);
  max-width: 52ch;
  margin: 12px auto 0;
}

.award-sep {
  margin: 6px 0;
  font-size: 10px;
  line-height: 1;
  color: var(--blue-soft);
}

/* THE HIDDEN STATES ARE ALL GATED ON `.js`. With scripting off nothing ever
   gets `is-in`, so an unguarded `opacity: 0` here would leave the whole
   section blank rather than merely unanimated. */
.js .award[data-reveal] {
  opacity: 1;
  transform: none;
  filter: none;
  transition: none;
}

.js .award[data-reveal] .award-year {
  opacity: 0;
  transition: opacity .5s var(--ease);
  transition-delay: var(--d, 0ms);
}

.js .award[data-reveal] .award-name {
  opacity: 0;
  letter-spacing: .22em;
  filter: blur(8px);
  transition:
    opacity .9s var(--ease),
    letter-spacing .9s var(--ease),
    filter .9s var(--ease);
  transition-delay: var(--d, 0ms);
}

/* The description arrives a beat behind the name, which is what makes the
   engraving read as settling rather than as one block fading up. */
.js .award[data-reveal] .award-desc {
  opacity: 0;
  transition: opacity .5s var(--ease);
  transition-delay: calc(var(--d, 0ms) + 400ms);
}

.js .award[data-reveal].is-in .award-year,
.js .award[data-reveal].is-in .award-desc {
  opacity: 1;
}

.js .award[data-reveal].is-in .award-name {
  opacity: 1;
  letter-spacing: .01em;
  filter: blur(0);
}

/* The separator turns upright as it arrives. Its own transform has to be
   restated here, because the generic `[data-reveal]` hidden state would
   otherwise replace it with the standard 30px rise. */
.js .award-sep[data-reveal] {
  opacity: 0;
  transform: rotate(-90deg);
  filter: none;
  transition: opacity .6s var(--ease), transform .6s var(--ease);
  transition-delay: calc(var(--d, 0ms) + 200ms);
}

.js .award-sep[data-reveal].is-in {
  opacity: 1;
  transform: rotate(0);
}

/* ------------------------------------------------------------ about bio */
/* 18px at 1.7, capped at 62ch. It ran at the 16px body size inside a 68ch
   `.prose`, which left it small and floating in the column beside a tall
   portrait. The last paragraph loses its bottom margin so the block sits
   centred against the picture rather than hanging above its own gap. */
.about-bio {
  font-family: var(--text);
  max-width: 62ch;
}

.about-bio p {
  font-size: 18px;
  line-height: 1.75;
  margin-bottom: 1.4em;
}

/* The bio opens on the same on ramp every section on the site opens on. It has
   no `.lede` of its own, because the heading above it is the page's second
   heading rather than a section head with a standing lede under it. */
.about-bio p:first-of-type {
  font-style: italic;
  font-size: 20px;
  line-height: 1.6;
}

.about-bio p:last-child {
  margin-bottom: 0;
}

/* THE PICTURE TAKES ITS HEIGHT FROM THE WORDS, not the other way round, and
   that is what actually answered "the bio floats in whitespace". Raising the
   type to 18px made the text block taller but the 4:5 portrait beside it was
   706px against 420px, so vertical centring left 143px of empty column above
   the first line and 143px below the last. Top-aligning only moves the hole to
   the bottom.
   Absolute positioning is the load-bearing part: a grid item that keeps its
   intrinsic aspect ratio SETS the row height, so the picture has to stop
   contributing one before the text can decide it. The frame then takes the
   text's height and the photograph fills it.
   Desktop only. Below 860px the split is a single column and a picture as
   short as the paragraph above it would be a letterbox, so the portrait keeps
   its own 4:5 there. */
@media (min-width: 861px) {
  .split.split-fill {
    align-items: stretch;
  }

  .split.split-fill .split-media {
    position: relative;
    min-height: 340px;
  }

  .split.split-fill .split-media img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    aspect-ratio: auto;
    object-fit: cover;
    /* 5 PERCENT, NOT 20. At 20 the crop started far enough into the file that
       it grazed the top of his head in the 530 by 450 frame this becomes at
       desktop width. Taking it to 5 shows the photograph's own headroom above
       his hair and still lands the bottom edge mid chest. Measured at 1204 and
       1440, which are the two widths the frame is squarest at. */
    object-position: 50% 5%;
  }
}

/* ------------------------------------------------------------ steps / list */
.step-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 14px;
}

.step-list li {
  display: grid;
  grid-template-columns: 34px 1fr;
  gap: 14px;
  align-items: start;
  color: var(--ink-soft);
}

.step-list li::before {
  content: counter(step);
  counter-increment: step;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: 1px solid var(--blue);
  border-radius: 50%;
  color: var(--blue);
  font-size: 12px;
  font-weight: 600;
}

.step-list {
  counter-reset: step;
}

.tick-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
}

.tick-list li {
  position: relative;
  padding-left: 26px;
  color: var(--ink-soft);
}

.tick-list li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 9px;
  width: 9px;
  height: 9px;
  border-right: 1.5px solid var(--blue);
  border-bottom: 1.5px solid var(--blue);
  transform: rotate(45deg);
}

/* ------------------------------------------------------------------- FAQ */
.faq {
  display: grid;
  gap: 0;
  border-top: 1px solid var(--line);
}

.faq-item {
  border-bottom: 1px solid var(--line);
}

.faq-q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 20px 0;
  background: none;
  border: 0;
  text-align: left;
  font-family: var(--display);
  font-size: 18px;
  font-weight: 300;
  cursor: pointer;
}

.faq-q::after {
  content: '';
  flex: none;
  width: 11px;
  height: 11px;
  border-right: 1.5px solid var(--blue);
  border-bottom: 1.5px solid var(--blue);
  transform: rotate(45deg) translate(-3px, -3px);
  transition: transform .3s var(--ease);
}

.faq-q[aria-expanded="true"]::after {
  transform: rotate(225deg) translate(-3px, -3px);
}

.faq-a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .32s var(--ease);
}

.faq-q[aria-expanded="true"] + .faq-a {
  grid-template-rows: 1fr;
}

.faq-a > div {
  overflow: hidden;
}

.faq-a p {
  color: var(--ink-soft);
  margin: 0 0 20px;
  max-width: 74ch;
}

/* --------------------------------------------------------- sticky split */
/* Heading holds the left column, the component fills the right. This is what
   stops a list capped narrow inside a wide wrap from trailing dead space. */
.sticky-split {
  display: grid;
  grid-template-columns: 360px 1fr;
  gap: clamp(28px, 4vw, 72px);
  align-items: start;
}

.sticky-split > .section-head {
  position: sticky;
  top: 120px;
  margin-bottom: 0;
}

@media (max-width: 1024px) {
  .sticky-split {
    grid-template-columns: 1fr;
  }

  .sticky-split > .section-head {
    position: static;
    margin-bottom: var(--gap-head);
  }
}

/* ------------------------------------------------------------------ forms */
/* [CHECKLIST 6] Every field gets min-width:0, every input is width:100%, the
   button lives in its own cell, 2 columns at 860 and 1 at 560, and there is
   zero horizontal scroll at 390px. THE CALCULATOR SUITE IS THE HIGHEST RISK
   COMPONENT FOR THIS, so it is built under exactly these rules below. */
.form {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 7px;
  min-width: 0;
}

.field.full {
  grid-column: 1 / -1;
}

.field label,
.field > .label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.field input,
.field select,
.field textarea {
  width: 100%;
  min-width: 0;
  padding: 13px 14px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: 16px;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}

.field textarea {
  min-height: 132px;
  resize: vertical;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--blue);
  box-shadow: 0 0 0 3px rgba(33, 94, 120, .16);
}

.field .hint {
  font-size: 12px;
  color: var(--ink-faint);
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
}

.field-error {
  color: #A3242B;
  font-size: 12px;
}

.consent {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 20px 1fr;
  gap: 12px;
  align-items: start;
}

.consent input {
  width: 18px;
  height: 18px;
  margin-top: 3px;
  accent-color: var(--blue);
}

.consent label {
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink-soft);
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
}

/* The submit sits in its OWN cell, never sharing a grid track with a field. */
.form-actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
}

.form-note {
  grid-column: 1 / -1;
  font-size: 12px;
  color: var(--ink-faint);
}

.form-status {
  grid-column: 1 / -1;
  padding: 14px 16px;
  border: 1px solid var(--line);
  background: var(--blue-wash);
  font-size: 14px;
  display: none;
}

.form-status.is-shown {
  display: block;
}

.form-status.is-error {
  background: #FBF0F0;
  border-color: #E4C8C8;
  color: #8C1F26;
}

/* Honeypot. Off-screen rather than display:none, which some bots skip. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

@media (max-width: 860px) {
  .form {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 560px) {
  .form {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ------------------------------------------------------- calculator suite */
.calc-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 28px;
  margin-bottom: 28px;
  border-bottom: 1px solid var(--line);
  padding-bottom: 0;
}

.calc-tab {
  padding: 12px 2px;
  /* Sits the 2px underline ON the container's hairline. Only -1px, so a bar
     that wraps to two rows overlaps by a hairline instead of by its padding. */
  margin-bottom: -1px;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  background: none;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-soft);
  cursor: pointer;
  white-space: nowrap;
  transition: color .2s var(--ease), border-color .2s var(--ease);
}

.calc-tab:hover {
  color: var(--accent-teal);
  border-bottom-color: var(--accent-teal);
}

/* An underline tab bar, not four filled chips: the chips competed with the
   programme pills directly beneath them and the reader could not tell which
   row was the section and which was the setting. */
.calc-tab[aria-selected="true"] {
  background: none;
  color: var(--ink);
  border-bottom-color: var(--accent-teal);
}

.calc-panel[hidden] {
  display: none;
}

/* The calculator is a FORM and obeys every form rule above. */
.calc {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  gap: clamp(24px, 3vw, 44px);
  align-items: start;
}

@media (max-width: 1024px) {
  .calc {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* 18px between rows, so each label has real space above it rather than sitting
   on the field before it. The column gap stays 16px: widening that squeezes
   the inputs instead. */
.calc-inputs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px 16px;
}

@media (max-width: 560px) {
  .calc-inputs {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* THE ESTIMATE CARD. It was a flat pale blue square and it read as a disabled
   panel, which is the wrong signal for the one part of the page that answers
   the reader's question. Deep teal, a soft glow off the top corner, and the
   big number breathes on every keystroke. calculators.js is untouched: this is
   entirely the styling of the classes it already emits. */
.calc-results {
  position: sticky;
  top: calc(var(--nav-h) + 24px);
  background: var(--teal-deep);
  color: var(--cream-dark);
  border: 0;
  /* ROUND 4 PART 10: a raised card at the same 16px as the wizard, so the two
     interactive components on the site read as one family. */
  border-radius: 16px;
  box-shadow: 0 26px 60px rgba(13, 56, 68, .26), 0 3px 10px rgba(16, 20, 24, .10);
  padding: clamp(28px, 3vw, 44px);
  overflow: hidden;
}

.calc-results::before {
  content: '';
  position: absolute;
  inset: -30%;
  background: radial-gradient(ellipse at 85% -10%, var(--teal-glow), transparent 55%);
  pointer-events: none;
}

/* The glow is a stacking sibling, so everything real is lifted above it. */
.calc-results > * {
  position: relative;
}

/* RELATIVE, NOT STATIC, and `top` reset with it.

   The panel stops being sticky here, but it must stay POSITIONED, because the
   glow above is `position: absolute; inset: -30%` and an absolutely positioned
   box climbs to its nearest positioned ancestor. Nothing between this and the
   root is positioned, so `static` handed the glow the initial containing block:
   -30% then meant -30% of the VIEWPORT, the glow spread 117px past each edge of
   a 390px phone, and the document went 507px wide. `overflow: hidden` on this
   element does not save it either, because an element only clips boxes whose
   containing block it is part of.

   `top: auto` is the other half. `position: relative` keeps `top` live, and
   the sticky offset above would then push the whole panel 112px down the page
   as a relative shift, leaving a gap above it and its own tail hanging past
   the section. */
@media (max-width: 1024px) {
  .calc-results {
    position: relative;
    top: auto;
  }
}

/* TABULAR FIGURES ARE NOT A DETAIL HERE. The headline tweens digit by digit on
   every keystroke, and in proportional figures a 1 is narrower than an 8, so
   the whole number shuffles sideways while it counts. Tabular pins every glyph
   to the same advance and the figure counts in place. */
.calc-headline {
  font-family: var(--display);
  font-size: clamp(36px, 3.6vw, 52px);
  font-weight: 300;
  color: #fff;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

/* The accent, carried up in lightness for the deep teal card. See --blue-light
   at the top of this file: --blue itself measures 1.8:1 here. */
.calc-headline-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--blue-light);
  margin-bottom: 6px;
}

/* THE EMPTY STATE IS TEXT, NOT A RESULT, and it is the first thing every
   visitor to this page reads: "enter the interest rate you want to test".
   calculators.js writes it as `<p class="muted">`, and `.muted` is dark ink for
   a cream page, so on the teal card it went to near-black on near-black and
   disappeared. Every calculator opens in that state, because no rate is ever
   prefilled. Anything the panel can print must be given a colour that works on
   this ground. */
.calc-results .muted {
  color: rgba(247, 244, 238, .78);
}

.calc-rows {
  list-style: none;
  margin: 22px 0 0;
  padding: 0;
  display: grid;
}

.calc-rows li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 11px 0;
  border-bottom: 1px solid rgba(247, 244, 238, .14);
  font-size: 14.5px;
  color: var(--cream-dark);
}

.calc-rows li b {
  color: #fff;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.calc-rows li.is-total {
  border-bottom: 0;
  border-top: 1px solid rgba(247, 244, 238, .35);
  font-size: 16px;
  color: #fff;
}

.calc-flag {
  margin-top: 18px;
  padding: 12px 14px;
  border-radius: 2px;
  font-size: 13px;
  line-height: 1.5;
}

/* Measured on the deep teal, because a flag that has gone quiet is a result
   the reader cannot read: ok 6.4:1, warn 6.8:1, bad 7.6:1. */
.calc-flag.ok {
  background: rgba(106, 168, 116, .25);
  color: #CBE5CF;
}

.calc-flag.warn {
  background: rgba(185, 169, 127, .22);
  color: #EFE3C2;
}

.calc-flag.bad {
  background: rgba(163, 67, 40, .3);
  color: #F3CDC0;
}

.calc-flag b {
  color: #fff;
}

/* The recompute pulse. site.js re-arms this class whenever the panel's markup
   is replaced, which is on every keystroke. */
.calc-pop {
  animation: calcPop .45s var(--ease) both;
}

@keyframes calcPop {
  from { opacity: .35; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* A pair of range + number that stay in step. */
.calc-range {
  display: grid;
  gap: 8px;
}

/* ================================================ ROUND 4 PART 10: SLIDERS */
/* THE FILL IS DRAWN, NOT INHERITED. `accent-color` paints the filled half of a
   range in Chrome and Firefox and does nothing of the sort in Safari, so a
   slider that reads as a blue level on two browsers reads as a grey bar on the
   third. The track is a two stop gradient instead, cut at `--fill`, which the
   JS writes on every input. It looks identical everywhere and it also lets the
   unset rate slider sit at a flat grey with no blue on it at all.
   The percentage is written by calculators.js (the four calculators) and by
   quote.js (the wizard's two sliders); both call the same setFill helper. */
.calc-range input[type="range"],
.field-range input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 6px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: linear-gradient(to right, var(--blue) 0 var(--fill, 0%), var(--line) var(--fill, 0%) 100%);
  cursor: pointer;
}

.calc-range input[type="range"]::-webkit-slider-thumb,
.field-range input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px;
  height: 20px;
  border-radius: 999px;
  background: var(--blue);
  border: 3px solid #fff;
  box-shadow: 0 2px 6px rgba(16, 20, 24, .28);
  cursor: pointer;
}

.calc-range input[type="range"]::-moz-range-thumb,
.field-range input[type="range"]::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 999px;
  background: var(--blue);
  border: 3px solid #fff;
  box-shadow: 0 2px 6px rgba(16, 20, 24, .28);
  cursor: pointer;
}

.calc-range input[type="range"]::-moz-range-track,
.field-range input[type="range"]::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: transparent;
}

.calc-range input[type="range"]:focus-visible,
.field-range input[type="range"]:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 4px;
}

/* The slider that sits under a number field, injected by calculators.js. */
.field-range {
  margin-top: 12px;
}

/* THE RATE SLIDER OPENS UNSET, AND IT HAS TO LOOK IT. The site prints no rate
   anywhere, so the rate field ships empty; a slider parked at its floor with a
   blue fill behind it would read as "the rate is zero", which is a figure. No
   fill and a hollow thumb until the reader moves it or types one. */
.field-range input[type="range"][data-unset] {
  background: var(--line);
}

.field-range input[type="range"][data-unset]::-webkit-slider-thumb {
  background: var(--paper);
  border-color: var(--ink-faint);
}

.field-range input[type="range"][data-unset]::-moz-range-thumb {
  background: var(--paper);
  border-color: var(--ink-faint);
}

/* The programme pills sit directly above the first input label and they own
   the whole gap, so this is the full 28px rather than a share of it. THE PILLS
   ARE A SIBLING OF `.calc-inputs`, NOT A CHILD, so the grid's 18px row gap
   never applies between them and the first field, and `grid-column` below is
   inert. Measured on the page: with 10px here the gap was 10px, not 28px. */
.calc-sub {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 28px;
}

.calc-sub button {
  padding: 8px 16px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  cursor: pointer;
  transition: background .2s var(--ease), color .2s var(--ease), border-color .2s var(--ease);
}

.calc-sub button:hover {
  border-color: var(--accent-teal);
  color: var(--accent-teal);
}

.calc-sub button[aria-selected="true"] {
  background: var(--accent-teal);
  border-color: var(--accent-teal);
  color: #fff;
}

/* PART 6: required beneath EVERY calculator. */
.calc-disclaimer {
  margin-top: var(--gap-head);
  padding-top: 18px;
  border-top: 1px solid var(--line);
  font-size: 12px;
  line-height: 1.6;
  color: var(--ink-faint);
  max-width: 88ch;
}

.calc-cta {
  margin-top: 20px;
}

/* ------------------------------------------------------- the quote wizard */
/* ROUND 4 PART 6, AND THE ROUNDING IS SCOPED. This component, the calculator
   results card and the GET PREQUALIFIED pill are the only soft geometry on the
   site; everything else stays square. Keep it that way, or the one place that
   is meant to feel like a product stops standing out from the editorial. */
.wizard {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: clamp(22px, 3vw, 38px);
  /* Raised off the band rather than sitting flat in it. Two shadows: a wide
     soft one for the lift and a tight one so the card still has an edge on
     sand, where a single diffuse shadow disappears. */
  box-shadow: 0 22px 48px rgba(16, 20, 24, .10), 0 2px 6px rgba(16, 20, 24, .05);
}

/* THE BAR AND THE COUNT SIT ON ONE LINE, and the bar is a REAL one: quote.js
   builds this head once and only ever writes a new width into it, so the fill
   animates from the step you were on to the step you are going to. It used to
   be rebuilt with the rest of the markup on every advance, which meant a fresh
   element painted straight at its final width and nothing ever moved. */
.wizard-head {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 24px;
}

.wizard-progress {
  flex: 1 1 auto;
  min-width: 0;
  height: 6px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}

.wizard-bar {
  height: 100%;
  width: 10%;
  border-radius: 999px;
  background: var(--accent-teal);
  transition: width .35s var(--ease);
}

.wizard-count {
  flex: none;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--ink-faint);
  white-space: nowrap;
}

/* THE DECK HOLDS ONE STEP AT A TIME, and briefly two while they cross. The
   outgoing step is taken out of flow so the incoming one sets the height
   immediately; without that the card collapses to nothing for a frame between
   a tall step and a short one. */
.wizard-deck {
  position: relative;
}

.wizard-step {
  transition: transform .3s var(--ease), opacity .3s var(--ease);
}

.wizard-step.is-entering {
  opacity: 0;
  transform: translateX(24px);
}

.wizard-step.is-leaving {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateX(-24px);
  pointer-events: none;
}

/* Back reverses it, so the direction of travel always matches the direction
   the reader asked for. */
.wizard-step.is-entering.back {
  transform: translateX(-24px);
}

.wizard-step.is-leaving.back {
  transform: translateX(24px);
}

.wizard-step[hidden] {
  display: none;
}

.wizard-q {
  font-family: var(--display);
  font-size: clamp(26px, 2.6vw, 36px);
  font-weight: 300;
  margin: 0 0 6px;
}

/* The helper line under a question is the one thing in the wizard a reader
   READS rather than operates, so it takes the serif with the rest of the body
   copy. The question above it, the answers, the fields and the button stay in
   the interface face. */
.wizard-help {
  font-family: var(--text);
  color: var(--ink-soft);
  font-size: 14px;
  line-height: 1.65;
  margin: 0 0 20px;
}

.choice-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
  gap: 12px;
}

.choice {
  padding: 18px 18px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--paper);
  text-align: left;
  font-family: var(--body);
  font-size: 15px;
  color: var(--ink);
  cursor: pointer;
  min-width: 0;
  transition: border-color .2s var(--ease), background .2s var(--ease), color .2s var(--ease), transform .2s var(--ease), box-shadow .2s var(--ease);
}

.choice:hover {
  border-color: var(--accent-teal);
  transform: translateY(-2px);
  box-shadow: 0 10px 24px rgba(16, 20, 24, .07);
}

/* A PICKED CHOICE FILLS. It used to take a pale wash and an inset ring, which
   was quiet enough that on a grid of four a reader could not tell at a glance
   which one they had chosen. Solid blue with white text is unmistakable, and
   white on --blue measures 6.4:1, so the label is still comfortably readable
   rather than merely visible. */
.choice[aria-pressed="true"] {
  border-color: var(--blue);
  background: var(--blue);
  color: #fff;
  box-shadow: 0 8px 20px rgba(33, 94, 120, .28);
}

.choice[aria-pressed="true"]:hover {
  border-color: var(--blue-deep);
  background: var(--blue-deep);
}

.choice small {
  display: block;
  margin-top: 4px;
  color: var(--ink-faint);
  font-size: 12.5px;
}

/* The sub-line has to invert with its card, or it goes to 1.6:1 on the fill. */
.choice[aria-pressed="true"] small {
  color: rgba(255, 255, 255, .85);
}

.wizard-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}

.wizard-actions .spacer {
  flex: 1 1 auto;
}

.btn-quiet {
  background: none;
  border: 0;
  padding: 12px 4px;
  color: var(--ink-soft);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  cursor: pointer;
}

.btn-quiet:hover {
  color: var(--ink);
}

.wizard-note {
  margin-top: 14px;
  padding: 12px 16px;
  border-left: 2px solid var(--accent-teal);
  background: var(--blue-wash);
  font-size: 13px;
  color: var(--ink-soft);
}

/* The thank-you keeps the card, so the page does not lose a component the
   moment somebody finishes with it. `padding-block`, not `padding`, or it
   would drop the card's own horizontal padding on the floor. */
.wizard-done {
  text-align: center;
  padding-block: clamp(28px, 4vw, 48px);
}

.wizard-done h3 {
  font-family: var(--display);
  font-size: clamp(28px, 3vw, 40px);
  font-weight: 300;
  margin-bottom: 10px;
}

/* ROUND 10 PART 5.4. The line that hands a finished wizard on to the real
   application. It is a label rather than a sentence, so it takes the interface
   face and the eyebrow's tracking, and it carries the whole gap above the
   button so the two read as one closing step. */
.wizard-done .wizard-next {
  font-family: var(--body);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-set);
  margin: 26px 0 12px;
}

/* The slider steps read as a setting, so the live figure is the loud half of
   the label and sits at the end of the line where the thumb finishes. The
   track itself is styled once, in the sliders block above. */
.wizard .calc-range label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

.wizard .calc-range label b,
.wizard .calc-range label span {
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* -------------------------------------------- Instagram strip, round 8 P4 */
/* The band above the closing CTA on the home page. site.js REMOVES the whole
   section when there is no handle or no widget id, so these rules describe a
   band that is either complete or absent, never an empty heading.

   THE WHITE GROUND, THE SQUARE CROPS, THE ABSENCE OF BORDERS AND OF THE
   WIDGET'S OWN BRAND COLORS ARE SETTINGS IN ITS OWN DASHBOARD, as the note
   asks. This feed app draws into the ordinary DOM rather than a closed shadow
   root (unlike the reviews and the chat apps), but its class names belong to
   the vendor, so nothing here styles the tiles. What this owns is the band
   around it and the heading over it. */
.insta-strip {
  background: var(--paper);
}

/* FINAL PUNCH LIST PART 1.3. The strip waits COLLAPSED until the feed has
   drawn a post, and site.js removes it if none arrives. Collapsed rather than
   `display: none`, because the feed lays its tiles out from the slot's width
   and a hidden parent gives it zero. The heading IS taken out of the layout,
   so a reveal inside a clipped box is never measured as a blank on screen. */
.insta-strip.is-waiting {
  height: 0;
  padding-block: 0;
  overflow: hidden;
  visibility: hidden;
}

.insta-strip.is-waiting .section-head {
  display: none;
}

.insta-strip .h-section {
  margin-bottom: 0;
}

/* The heading IS the link to the profile, so it is underlined on approach
   rather than coloured, which would put a second accent on a heading. */
.insta-link {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color .25s var(--ease);
}

.insta-link:hover {
  border-bottom-color: var(--blue);
}

/* No min-height. It used to hold room for a feed that was slow to paint; the
   strip now only opens once the posts are drawn, so reserved room would only
   ever be empty space under a shorter feed. */
.insta-strip [data-widget-slot] {
  margin-top: var(--gap-head);
}

/* ----------------------------------------------- reviews strip, round 12 P3 */
/* The Zillow reviews on the home page, under the Learn list, and on About,
   under the recognition credits. Sand, so it never shares a ground with either
   neighbour: cream above it on both pages, the white Instagram band or the dark
   closing band below it.

   THE CARDS, THE TEXT, THE STARS AND THE ABSENCE OF BLUE ARE SETTINGS IN THE
   WIDGET'S OWN DASHBOARD. It draws inside its own shadow root, so no rule here
   can reach a card. What this owns is the band, the heading and the source line.

   It waits exactly as the Instagram strip does, and for the same two reasons:
   collapsed rather than `display: none`, because the widget lays its cards out
   from the slot's width, and the heading out of the layout while it waits, so a
   reveal in a clipped box is never counted as a blank on screen. site.js opens
   it once a review has drawn and removes it when none has. */
.reviews-strip.is-waiting {
  height: 0;
  padding-block: 0;
  overflow: hidden;
  visibility: hidden;
}

.reviews-strip.is-waiting .section-head {
  display: none;
}

.reviews-strip .h-section {
  margin-bottom: 0;
}

/* The attribution is a label, not a sentence, so it is set in the interface
   face. Soft ink measures well over 4.5:1 on sand. */
.reviews-source {
  font-family: var(--body);
  font-size: 13px;
  letter-spacing: .04em;
  color: var(--ink-soft);
  margin: 10px 0 0;
}

/* -------------------------------------------------------------- CTA band */
/* Every page now closes on a dark moment, and the footer under it is the same
   teal, so the band and the footer read as one piece rather than two stripes.
   The gap-section padding is UNCHANGED: the band got darker, not fatter, and
   the rhythm budget is measured between sections either way. */
.cta-band {
  position: relative;
  background: var(--teal-deep);
  color: var(--cream-dark);
  padding: var(--gap-section) 0;
  text-align: center;
  overflow: hidden;
}

.cta-band::before {
  content: '';
  position: absolute;
  inset: -40% -10%;
  background: radial-gradient(ellipse at 50% -10%, var(--teal-glow), transparent 60%);
  pointer-events: none;
}

/* ROUND 8 PART 5.1. On a phone the chat button is 16px from the bottom right
   corner, which is exactly where the closing band's own button sits. The extra
   room is scoped to `body.has-assistant`, a class site.js sets ONLY when it has
   really mounted the launcher, so no page carries 80px of empty band waiting
   for a button that is not there. It is the one place on this site allowed to
   exceed the section rhythm, and it is for a control rather than for air. */
@media (max-width: 700px) {
  body.has-assistant .cta-band {
    padding-bottom: calc(var(--gap-section) + 80px);
  }
}

.cta-band .container {
  position: relative;
}

.cta-band .section-head {
  margin-inline: auto;
}

.cta-band h2 {
  color: #fff;
}

.cta-band .eyebrow {
  color: #fff;
}

.cta-band .lede {
  color: var(--cream-dark);
}

/* Inverted, because a solid teal button on a teal band is a rectangle nobody
   can see. */
.cta-band .btn-solid {
  margin-top: 6px;
  background: var(--cream);
  border-color: var(--cream);
  color: var(--teal-deep);
}

.cta-band .btn-solid:hover {
  background: #fff;
  border-color: #fff;
  color: var(--teal-deep);
}

.cta-band :focus-visible {
  outline-color: var(--cream);
}

/* ------------------------------------------------------- contact details */
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 340px;
  gap: clamp(28px, 4vw, 64px);
  align-items: start;
}

@media (max-width: 1024px) {
  .contact-grid {
    grid-template-columns: 1fr;
  }
}

.contact-card {
  background: var(--paper);
  border: 1px solid var(--line);
  padding: 26px 24px;
}

.contact-card h3 {
  font-size: 19px;
  margin-bottom: 16px;
}

.contact-row {
  display: grid;
  gap: 2px;
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}

.contact-row:last-of-type {
  border-bottom: 0;
}

.contact-row .label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.contact-row a,
.contact-row span.v {
  color: var(--ink);
  font-size: 15px;
}

.contact-row a:hover {
  color: var(--blue);
}

/* FINAL PUNCH LIST PART 1.1: THE INSTAGRAM ROW IS ONE LINK, EDGE TO EDGE.
   partials.js writes the label and the handle inside a single anchor, so the
   tap target is the whole row (the card's width, about 60px tall) rather than
   one 22px line of text. The row gives its padding to the anchor, so it sits
   on exactly the same rhythm as the rows around it. */
.contact-row[data-instagram-row] {
  padding: 0;
}

.contact-row-link {
  display: grid;
  gap: 2px;
  min-height: 44px;
  padding: 12px 0;
}

.contact-row-link:hover .v {
  color: var(--blue);
}

/* ----------------------------------------------------------------- footer */
.footer {
  background: var(--teal-deep);
  color: rgba(247, 244, 238, .78);
  padding: var(--gap-section) 0 0;
  font-size: 14px;
}

.footer a {
  color: rgba(247, 244, 238, .78);
}

.footer a:hover {
  color: #fff;
}

/* Three columns, not four. The Contact column's address, phone and email moved
   into the identity block in column one, where the rest of who he is already
   was. */
.footer-grid {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr;
  gap: clamp(24px, 3vw, 48px);
}

@media (max-width: 1024px) {
  .footer-grid {
    grid-template-columns: 1fr 1fr;
  }
}

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

.footer h4 {
  font-family: var(--body);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .5);
  margin-bottom: 14px;
}

.footer-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px;
}

.footer-brand .nav-wordmark {
  color: #fff;
  margin-bottom: 14px;
}

.footer-brand .wordmark-name {
  color: #fff;
}

.footer-brand .wordmark-role {
  color: rgba(255, 255, 255, .55);
}

.footer-brand p {
  margin: 0;
  max-width: 40ch;
  color: rgba(255, 255, 255, .6);
}

.footer-social {
  display: flex;
  gap: 12px;
  margin-top: 16px;
}

.footer-social a {
  position: relative;
  width: 34px;
  height: 34px;
  border: 1px solid rgba(255, 255, 255, .22);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
}

/* FINAL PUNCH LIST PART 1.1: A 44px TAP TARGET, THE DRAWN CIRCLE UNCHANGED.
   The hit area reaches 5px past the 34px ring on every side. With the 12px gap
   between icons that leaves 2px between two targets, so they never overlap.
   THE INSET IS 6px, NOT 5px, and the first publish proved why: an absolute
   box is placed against the PADDING box, which sits 1px inside the ring's
   border. At -5px the target measured 42px, with elementFromPoint missing 4px
   past the right and bottom edges. At -6px it is 44 by 44. */
.footer-social a::before {
  content: '';
  position: absolute;
  inset: -6px;
}

.footer-social a:hover {
  border-color: #fff;
  background: rgba(255, 255, 255, .08);
}

.footer-social svg {
  width: 15px;
  height: 15px;
  fill: currentColor;
}

/* PART 10. The identity line and the disclaimers are required, not styling. */
.footer-legal {
  margin-top: var(--gap-tight);
  border-top: 1px solid rgba(255, 255, 255, .13);
  padding: 26px 0 30px;
}

/* THE COMPLIANCE BLOCK GOT LOUDER, NOT QUIETER, on the way to the darker
   ground. The disclaimer ran at .45 white on near black, which measures 4.4:1
   and is under the floor: text a regulator requires must not be the least
   readable text on the page. Every line here is now 80% cream or more, which
   is 7.8:1 on the teal. The words themselves are untouched.

   THE OVERRIDE ON `.footer-brand p` IS LOAD BEARING. The identity stack now
   sits inside `.footer-brand`, whose paragraph rule paints at 60% white. That
   would quietly drop the NMLS and DRE numbers below the readability floor the
   paragraph above exists to hold. */
.footer-identity {
  margin: 14px 0 0;
}

.footer-brand .footer-identity p {
  color: rgba(247, 244, 238, .92);
  font-size: 13.5px;
  line-height: 1.65;
  max-width: none;
  margin: 0;
}

.footer-brand .footer-identity a {
  color: rgba(247, 244, 238, .92);
}

.footer-brand .footer-identity a:hover {
  color: #fff;
}

/* The separator needs its own air. Without it the line renders as
   "The Lending Pros|Company NMLS #2320101", with the rule jammed against both
   words. Invisible while the block sat at 60% on near black; obvious once the
   compliance lines were raised to 90% cream. */
.footer-identity .sep {
  color: rgba(247, 244, 238, .4);
  padding: 0 8px;
}

.footer-disclaimer {
  color: rgba(247, 244, 238, .8);
  font-size: 12px;
  line-height: 1.65;
  max-width: 96ch;
  margin: 0 0 16px;
}

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  gap: 16px 28px;
  align-items: center;
  justify-content: space-between;
  border-top: 1px solid rgba(255, 255, 255, .1);
  padding-top: 20px;
}

.footer-bottom-links {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 20px;
  font-size: 12.5px;
}

.footer-marks {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* The Equal Housing mark is required furniture, not a decoration, so it holds
   the same 80% cream floor as the rest of the compliance block. */
.eho {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  color: rgba(247, 244, 238, .85);
}

.eho svg {
  width: 26px;
  height: 26px;
  fill: currentColor;
  flex: none;
}

.eho span {
  font-size: 9.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  line-height: 1.25;
}

.footer-copy {
  font-size: 12.5px;
  color: rgba(247, 244, 238, .6);
}

/* ROUND 14, THE APPOMARK CREDIT. Three things in it are not what the note
   literally says, and each one is the note's own rule winning over its
   picture of the footer.
   - IT SITS UNDER THE EQUAL HOUSING MARK, NOT BESIDE THE COPYRIGHT. The note
     asks for the far right of the copyright's line, and also that the credit
     never sit above or between a compliance line. The far right of that line
     IS the mark, so the credit takes the far right of the bar's LAST line,
     level with the legal links, and the mark rides up 10px to sit level with
     the copyright. `padding-top` is that 10px: the copyright's 12.5px top
     margin, less half the gap between its 20.6px line and the 26px mark.
   - IT IS 60% CREAM, NOT `--ink-soft`. That token is dark ink for the cream
     pages and measures 1.3:1 on --teal-deep. 60% cream is the copyright's own
     soft tone, 5.2:1, and still the quietest line in the bar.
   - IT IS 12px, the disclaimer's size, which is the smallest SENTENCE in the
     footer. The 9.5px and 10px in use are tracked capitals.
   THE SWITCH IS A WIDTH PER LANGUAGE, NOT A FLEX WRAP. The two lines inside
   `.footer-end` go right when it sits beside the links and left when it drops
   under them, and a wrap cannot tell CSS which of those happened. So below
   the width where the links, the 28px gap and the credit fit on one line
   (measured: 444 + 28 + 129px in English, 502 + 28 + 157px in Spanish) the
   group always takes its own line, as the mark alone did, and above it the
   bar never wraps. A narrower real container, a scrollbar say, only folds
   the links onto two lines.
   THE BAR GETS ROOM FOR THE CHAT BUTTON WHILE THE CREDIT IS ON THE RIGHT.
   The page ends at the footer, and the button is a 64px circle 24px off the
   bottom right corner, so at the foot of every page it sat on "Appomark" and
   on the right half of the Equal Housing caption, which it was already
   covering on 2026-09-14. 104px under the bar is the button's 88px plus 16px of air. It is
   scoped to `body.has-assistant`, as the closing band's own room is, and not
   given on a phone, where both lines sit on the left. */
.footer-end {
  display: flex;
  flex: 0 0 100%;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

.footer-credit {
  margin: 0;
  font-size: 12px;
  line-height: 1.65;
  color: rgba(247, 244, 238, .6);
  white-space: nowrap;
}

/* Underlined because it is a link inside a sentence; the footer's white
   hover is the site's standard one there. */
.footer .footer-credit a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: rgba(247, 244, 238, .35);
}

.footer .footer-credit a:hover {
  color: #fff;
  text-decoration-color: currentColor;
}

@media (min-width: 641px) {
  html[lang="en"] .footer-bottom {
    flex-wrap: nowrap;
  }

  html[lang="en"] .footer-end {
    flex-basis: auto;
    align-self: stretch;
    align-items: flex-end;
    justify-content: space-between;
    padding-top: 10px;
  }

  html[lang="en"] body.has-assistant .footer-legal {
    padding-bottom: 104px;
  }
}

@media (min-width: 728px) {
  .footer-bottom {
    flex-wrap: nowrap;
  }

  .footer-end {
    flex-basis: auto;
    align-self: stretch;
    align-items: flex-end;
    justify-content: space-between;
    padding-top: 10px;
  }

  body.has-assistant .footer-legal {
    padding-bottom: 104px;
  }
}

/* ------------------------------------------------------------------ reveal */
/* [CHECKLIST 13] Soft focus editorial motion. Same trigger machinery as
   before, richer states: elements rise out of a slight blur, photographs
   develop through a wipe and settle out of a small zoom, split sections arrive
   from their own side, and a section head draws its rule before the heading
   moves. The reduced-motion guard at the end of this block turns all of it
   off, and that guard is the reason the rest of it is allowed to exist. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(30px);
  filter: blur(6px);
  transition:
    opacity .8s var(--ease),
    transform .8s var(--ease),
    filter .8s var(--ease);
  transition-delay: var(--d, 0ms);
}

.js [data-reveal].is-in {
  opacity: 1;
  /* `none`, not translateY(0): a reveal on an element that carries its own
     transform would otherwise have it cancelled here. */
  transform: none;
  filter: blur(0);
}

/* Directional variants. The markup names the side, so a split's words and its
   photograph close on each other instead of both rising. These sit AFTER the
   base rule on purpose: same specificity, so order is what decides.

   THE OFFSET CAN NEVER EXCEED THE SIDE PADDING, and a flat 36px did. A block
   waiting to be revealed sits that far outside its own column, and the
   container's gutter at 320px is only 16px, so the DOCUMENT was 20px wider
   than the screen for as long as that block had not arrived. That is a real
   sideways scroll, not a paint artifact: a visitor at 320px could drag the
   page sideways until the block came in. It reads as an intermittent fault,
   which is why it survived a round: settle every reveal first and the
   measurement comes back clean, because settling is what removes the offset.
   Measured on /, /about/ and /loan-options/{slug}/, which are the pages
   carrying a `right` split.

   SO THE TRAVEL IS TIED TO THE GUTTER ITSELF, not to a fraction of the
   viewport. `(100vw - var(--wrap)) / 2` IS the gutter above 1280px; 4px comes
   off it for rounding, 14px is the floor (the same 14px `a.card` already
   proved safe against the 16px gutter a phone has), and 36px is the cap.
   Measured, 320 to 2560 in 8px steps, on all eleven page shapes: clean.

   `min(36px, 4vw)` was tried first and is NOT enough. It fixes 320px, but the
   container's gutter stops growing at 20px once the viewport passes 560px
   while 4vw keeps climbing to 36px at 900px, so the overflow comes back
   across the whole tablet band and peaks at 16px just under 900px. Measured
   on the same sweep.

   The cost is honest and it is small: the sideways travel is 14px rather than
   36px below about 1270px, and the full 36px from 1320px up, which is where
   nearly every desktop sits. The rise and the blur are untouched, and they
   are the larger half of the arrival.

   DO NOT SOLVE THIS WITH `overflow: hidden` ON A WRAPPER. It hides the
   symptom, and on any ancestor of the loan component it also kills the
   sticky column's pin. */
.js [data-reveal="left"] {
  transform: translateX(calc(min(36px, max(14px, (100vw - var(--wrap)) / 2 - 4px)) * -1));
}

.js [data-reveal="right"] {
  transform: translateX(min(36px, max(14px, (100vw - var(--wrap)) / 2 - 4px)));
}

/* AND THE HIDDEN OFFSET MUST SNAP TO THE VIEWPORT, NOT ANIMATE TOWARDS IT.
   Tying the travel to the gutter above fixed the value at every width but not
   the JOURNEY between two widths, and the base rule transitions `transform` on
   the hidden state, so the offset LAGS a resize by 800ms. Shrink the window
   from a desktop width and the block keeps the desktop's 36px while the gutter
   is already the phone's 16px: the document is 20px wider than the screen for
   most of a second, which is exactly the 20px the sweep reports at 320px. A
   fresh load at 320px was always clean, so this hides from any check that
   measures one width and reloads; `responsive-check` resizes one page across
   281 widths, which is why it sees it and a person reporting the bug would not.

   So `transform` comes OFF the transition list while the block waits, and goes
   back ON when it arrives. The snap costs nothing that anyone can see, because
   a waiting block is at `opacity: 0`. The 36px desktop travel is kept whole.

   Durations still come from the base rule's shorthand, which is .8s for all
   three, so only the PROPERTY LIST changes here. */
.js [data-reveal="left"],
.js [data-reveal="right"] {
  transition-property: opacity, filter;
}

.js [data-reveal="left"].is-in,
.js [data-reveal="right"].is-in {
  transform: none;
  transition-property: opacity, filter, transform;
}

/* Photographs develop: a wipe from the left, and the picture settles out of
   1.07. No blur on a photo frame — a blurred photograph reads as a failed
   image load rather than as an arrival. */
.js .reveal-photo {
  overflow: hidden;
}

/* A photograph in a split carries `data-reveal="right"`, so it holds the
   directional offset too — and this rule's own shorthand would put `transform`
   back on the transition list and bring the lag above straight back. It gets
   the same split for the same reason. */
.js .reveal-photo[data-reveal] {
  clip-path: inset(0 14% 0 0);
  filter: none;
  transition:
    opacity .8s var(--ease),
    clip-path 1s var(--ease);
}

.js .reveal-photo[data-reveal].is-in {
  clip-path: inset(0 0 0 0);
  transition:
    opacity .8s var(--ease),
    transform .8s var(--ease),
    clip-path 1s var(--ease);
}

.js .reveal-photo img {
  transform: scale(1.07);
  transition: transform 1.15s var(--ease);
  transition-delay: var(--d, 0ms);
}

.js .reveal-photo.is-in img {
  transform: scale(1);
}

/* THE ENGRAVING. A Learn title is not carried in on the standard rise: it
   arrives blurred and letterspaced wide and settles sharp over 800ms, which is
   the same settle the recognition credits use. So the item's own reveal is
   cancelled and the title, the topic label and the read link carry it. The
   stagger's `--d` still applies, because `data-stagger` sets it on the item and
   it inherits down. */
.js .learn-item[data-reveal] {
  opacity: 1;
  transform: none;
  filter: none;
  transition: none;
}

.js .learn-item[data-reveal] .learn-title {
  opacity: 0;
  letter-spacing: .1em;
  filter: blur(8px);
  transition:
    opacity .8s var(--ease),
    letter-spacing .8s var(--ease),
    filter .8s var(--ease),
    background-position .6s var(--ease),
    color .6s var(--ease);
  transition-delay: var(--d, 0ms);
}

.js .learn-item[data-reveal].is-in .learn-title {
  opacity: 1;
  letter-spacing: .005em;
  filter: blur(0);
}

/* The label and the read link belong to the focused item only, so they rest at
   zero and the focus class is what lifts them. The label tightens as it
   arrives rather than simply appearing. */
.js .learn-item[data-reveal] .learn-topic {
  opacity: 0;
  letter-spacing: .35em;
  transition: opacity .4s var(--ease), letter-spacing .6s var(--ease);
}

.js .learn-item[data-reveal] .learn-go {
  opacity: 0;
  transition: opacity .3s var(--ease);
}

.js .learn-item[data-reveal].is-active .learn-topic,
.js .learn-item[data-reveal]:hover .learn-topic,
.js .learn-item[data-reveal]:focus-visible .learn-topic {
  opacity: 1;
  letter-spacing: .22em;
}

.js .learn-item[data-reveal].is-active .learn-go,
.js .learn-item[data-reveal]:hover .learn-go,
.js .learn-item[data-reveal]:focus-visible .learn-go {
  opacity: 1;
}

/* The eyebrow's rule draws itself, then the heading rises behind it. */
.js .section-head[data-reveal] .eyebrow::before {
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .7s var(--ease) .15s;
}

.js .section-head[data-reveal].is-in .eyebrow::before {
  transform: scaleX(1);
}

/* Hero load choreography. The home hero is above the fold, so it cannot wait
   for a scroll trigger; site.js puts `is-loaded` on the body one frame after
   DOMContentLoaded and these three arrive in sequence behind it. */
.js .hero-name,
.js .hero-sub,
.js .hero-actions {
  opacity: 0;
  transform: translateY(26px);
  filter: blur(6px);
}

.js .is-loaded .hero-name {
  animation: heroUp 1s var(--ease) .2s both;
}

.js .is-loaded .hero-sub {
  animation: heroUp .9s var(--ease) .5s both;
}

.js .is-loaded .hero-actions {
  animation: heroUp .9s var(--ease) .68s both;
}

@keyframes heroUp {
  from { opacity: 0; transform: translateY(26px); filter: blur(6px); }
  to   { opacity: 1; transform: translateY(0); filter: blur(0); }
}

/* THE GUARD. Everything above is off here, and "off" means visible and still,
   never merely faster. The `[data-reveal]` reset carries !important because
   the hidden states are set on the same selectors and a shorter transition
   would otherwise leave a blurred element sitting blurred. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    animation-delay: 0s !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }

  html {
    scroll-behavior: auto;
  }

  .js [data-reveal],
  .js [data-reveal].is-in {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
  }

  .js .reveal-photo img,
  .js .reveal-photo.is-in img {
    transform: none !important;
  }

  .js .section-head[data-reveal] .eyebrow::before,
  .js .section-head[data-reveal].is-in .eyebrow::before {
    transform: none !important;
  }

  /* The drawing checks are a hidden state, so "off" has to mean fully drawn
     and still, never a faster draw. */
  .js .fit-list[data-reveal] li::before,
  .js .fit-list[data-reveal].is-in li::before {
    clip-path: none !important;
  }

  /* THE THREE ROUND 3 COMPONENTS, ALL OFF. Off means every title fully inked
     and at its settled tracking, every label and link shown, the diamond
     upright, and every loan block in full ink with its details link, exactly
     as the reference file has it. site.js also stops observing, so nothing
     waits on a scroll position either. */
  .js .learn-item[data-reveal] .learn-title,
  .js .learn-item[data-reveal].is-in .learn-title {
    opacity: 1 !important;
    letter-spacing: .005em !important;
    filter: none !important;
    background-position: 0 0 !important;
  }

  .js .learn-item[data-reveal] .learn-topic {
    opacity: 1 !important;
    letter-spacing: .22em !important;
  }

  .js .learn-item[data-reveal] .learn-go {
    opacity: 1 !important;
  }

  /* The one line under each title is a hidden state on a desktop, so "off"
     here has to mean shown on every item, not shown a little faster. */
  .js .learn-item[data-reveal] .learn-excerpt {
    opacity: 1 !important;
  }

  .js .learn-go::before {
    width: 34px !important;
    margin-right: 10px !important;
  }

  /* ROUND 6 PART 4: the cards are static and the ONLY hover left is the blue
     underline under the title. No lift, no shadow move, no picture zoom. The
     filter still works; site.js drops the fade and swaps the grid in one step
     rather than waiting out a transition nobody will see. */
  .js .topic-card:hover .topic-card-inner,
  .js .topic-card:focus-visible .topic-card-inner {
    transform: none !important;
    box-shadow: 0 2px 6px rgba(16, 20, 24, .04) !important;
  }

  .js .topic-card:hover .topic-card-media img,
  .js .topic-card:focus-visible .topic-card-media img {
    transform: none !important;
  }

  .js .award[data-reveal] .award-year,
  .js .award[data-reveal] .award-desc {
    opacity: 1 !important;
  }

  .js .award[data-reveal] .award-name {
    opacity: 1 !important;
    letter-spacing: .01em !important;
    filter: none !important;
  }

  .js .award-sep[data-reveal],
  .js .award-sep[data-reveal].is-in {
    opacity: 1 !important;
    transform: none !important;
  }

  .js .loan-item h3,
  .js .loan-item.is-active h3 {
    color: var(--ink) !important;
  }

  .js .loan-item p,
  .js .loan-item.is-active p {
    color: var(--ink-soft) !important;
  }

  .js .loan-item .go {
    opacity: 1 !important;
    transform: none !important;
  }

  .js .hero-name,
  .js .hero-sub,
  .js .hero-actions {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
  }

  .mobile-menu {
    transition: none;
  }
}

/* ------------------------------------------------------------ legal pages */
.legal {
  padding: var(--gap-section) 0;
}

.legal .prose h2 {
  font-size: 22px;
}

/* ROUND 7 PART 4.3.1. A legal page has a plain header with no lede of its own,
   so its opening paragraph IS the on ramp and takes the same serif italic every
   other page opens on. `.legal-updated` is a sibling of `.prose`, not a child,
   so `:first-of-type` still lands on the first paragraph of the policy. */
.legal .prose > p:first-of-type {
  font-style: italic;
  font-size: 20px;
  line-height: 1.6;
}

.legal-updated {
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--gap-head);
}

/* --------------------------------------------------------- article layout */
.article {
  padding: var(--gap-section) 0;
}

/* ROUND 12 PART 2: THE ARTICLE IS ONE CENTRED COLUMN. The reading column was
   already capped at 62ch, but it sat against the LEFT edge of a 1165px
   container at 1205px, so the page read as a slab with 540px of nothing down
   its right. The note asks for 68ch, centred, on the article template only.
   `.article-foot` is a `.prose` too, so the disclaimer and its hairline follow
   the column rather than being left behind at the old edge.
   68ch IS MEASURED IN THIS ELEMENT'S OWN FONT, the 18px text serif, which is
   684px. Everything that has to share the column's two edges takes the same
   font context below, for the same reason `.article-hero` always has.
   Nothing here changes a type size, the drop cap, the blue bold or the pull
   lines. Below about 720px the cap never binds, so a phone is unchanged. */
.article .prose {
  max-width: 68ch;
  margin-inline: auto;
}

/* The eyebrow, the title and the italic lede above the article sit on the same
   column. The CONTAINER takes the text font and the cap, so all three share one
   left edge whatever their own faces are: each of them sets its own family and
   size, so nothing inside inherits the 18px. The band itself stays full width,
   and the shared frame in `.page-header` (height, padding, top anchor) is not
   restated. The lede drops its own 62ch cap so it fills the column rather than
   stopping short of the body's right edge. */
.page-header.is-article .container {
  font-family: var(--text);
  font-size: 18px;
  max-width: 68ch;
}

.page-header.is-article .lede {
  max-width: none;
}

/* THE FIRST BODY PARAGRAPH IS ADDRESSED THROUGH THE TOPIC LABEL ABOVE IT, not
   through `:first-of-type`, and that is a fix rather than a preference. The
   first `<p>` in an article's prose is the TOPIC LABEL, so
   `.article .prose p:first-of-type` was pointing at it, and at three classes to
   one it outranked `p.article-topic` and quietly reset the label to 18px full
   ink. The label is always first and the copy always follows it, so `+` names
   the paragraph that was meant all along. */
.article .prose .article-topic + p {
  color: var(--ink);
}

/* ROUND 7 PART 4.3.2: THE DROP CAP, ON THE LEARN ARTICLE TEMPLATE ONLY. The
   display cut, in the accent, three lines deep.
   THE DEPTH COMES FROM THE LINE BOX, NOT FROM THE GLYPH. A float pushes any
   line it touches, so the number that decides "three lines" is font-size times
   line-height against the paragraph's own 31.5px line: 86.4 x .78 is 67px,
   which reaches into the third line and stops there. Change one of the two and
   check the other, or the cap silently becomes a two line or a four line one. */
.article .prose .article-topic + p::first-letter {
  float: left;
  font-family: var(--display);
  font-weight: 400;
  font-size: 4.8em;
  line-height: .78;
  color: var(--blue);
  margin: .02em .09em 0 0;
}

/* ROUND 4 PART 7.1: THE HEADER IMAGE, 16:9, DIRECTLY UNDER THE TITLE.
   It takes the SAME measure as the article body rather than the full 1240px
   wrap. An article is a text column capped for reading length, so an image
   running 600px wider than every line beneath it does not read as the top of
   this article, it reads as a band that belongs to the page. Sharing one left
   and right edge with the prose is what makes it the article's own picture. */
/* THE FAMILY IS HERE SO THE MEASURE MATCHES. `ch` is the width of a zero in the
   element's own font, and the column under this picture is now 62ch of Noto
   Serif rather than 62ch of Inter, so a frame capped in the sans would sit
   visibly narrower than every line beneath it.
   ROUND 12 PART 2: THE SIZE IS HERE FOR THE SAME REASON. The family alone was
   not enough: the frame inherited 16px, so its 62ch came out 555px against
   624px of text, 69px short on the right. At 18px it takes the column's
   exact two edges, and it is centred with the column. */
.article-hero {
  font-family: var(--text);
  font-size: 18px;
  margin: 0 auto var(--gap-head);
  max-width: 68ch;
  overflow: hidden;
}

.article-hero img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.article-foot {
  margin-top: var(--gap-tight);
  padding-top: 22px;
  border-top: 1px solid var(--line);
}

.article-foot .disclaimer {
  font-size: 13px;
  color: var(--ink-faint);
  font-style: italic;
  margin-bottom: 20px;
}

/* ROUND 4 PART 7.2: the four step questionnaire that replaced the flat button
   at the foot of every article. The card itself is the wizard component, so it
   already carries the rounding, the shadow and the progress bar; this is only
   the heading above it and the measure it sits in. */
/* ROUND 12 PART 2: the questionnaire keeps its own 720px and is centred on the
   column axis, so its hairline sits under the disclaimer's rather than off at
   the old left edge. Its width is untouched. */
.article-quiz {
  margin-top: var(--gap-section);
  margin-inline: auto;
  padding-top: var(--gap-tight);
  border-top: 1px solid var(--line);
  max-width: 720px;
}

.article-quiz .section-head {
  margin-bottom: var(--gap-head);
}

.related {
  margin-top: var(--gap-section);
  padding-top: var(--gap-tight);
  border-top: 1px solid var(--line);
}
