/* ============================================================================
   HOUSTON CREDIT BUILDER — site.css

   THIS is the file you edit. `base.css` is the engine and stays untouched.

   DESIGN CHOICE (record this so the next session doesn't re-decide it):
   v2 (2026-08-18): EARTHY PRIMARY, BLUE ACCENT. Client asked to flip v1's
   navy-led palette — earthy tones (warm terracotta brand, coffee-brown dark
   ground, warm ivory/sand neutrals) are now the dominant family, and one
   locked blue is the single accent. Blue appears in exactly one recurring
   role, the "Free Consultation" CTA, plus small decorative marks. Orange from
   v1 is fully retired.
   v2.1 (2026-08-18): lightened the two main earthy tones, terracotta primary
   (was a darker burnt sienna) and coffee dark ground (was near-black espresso),
   after review found the browns too heavy. The coffee ground is now the
   lightest it can be while the light-blue accent still clears 4.5:1 on it.
   ========================================================================== */

/* ---- 1. fonts -------------------------------------------------------------
   Google Fonts @import for now. Self-host the two .woff2 files before launch.

   Do NOT bother adding SOFT/WONK to this URL. Tested 2026-09-04: the CSS2 API
   accepts the axes and returns a different woff2, but that file carries only
   opsz and wght — an A/B render of WONK 0 vs WONK 1 was pixel-identical, and
   the Fontsource "full" build is byte-for-byte the same file. Fraunces' wonky
   letterforms need the upstream variable font, self-hosted. */
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..700;1,9..144,300..700&family=Public+Sans:wght@400;500;600;700&display=swap');

@import 'base.css';

/* ---- 2. client tokens -----------------------------------------------------
   MIRROR of design-systems/houston-credit-builder/tokens/*.css. Keep byte-for-
   byte identical — change a value in the design system FIRST, then copy here. */
:root{
--brand-primary:#B0592A;
--brand-secondary:#C1703F;

--bg-light:#FAF7F1;
--bg-light-alt:#F1EAE0;
--bg-dark:#4E3A2B;
--surface-dark-alt:#5C4636;

--text-on-light-primary:#2B2621;
--text-on-dark-primary:#F5F1EA;
--text-on-dark-muted:#C9BBA8;
--text-muted:#6B6255;

--accent-decorative:#3E6E8E;
--accent-on-dark:#8FB4CC;
--accent-text:#35658A;
--link-on-dark:#8FB4CC;

--border-hairline-light:#E3DACB;
--border-hairline-dark:rgba(255,255,255,.15);

--color-success:#3F6B42;
--color-warning-decorative:#B8862B;
--color-warning-text:#835A1C;
--color-error:#A6402F;

--font-display:'Fraunces',Georgia,serif;
--font-body:'Public Sans',system-ui,sans-serif;
--font-eyebrow:'Public Sans',system-ui,sans-serif;

--lh-display:1.05;
--lh-heading:1.15;
--lh-lead:1.6;
--lh-body:1.7;

--ls-none:0;
--ls-display:-0.01em;
--ls-heading:-0.005em;
--ls-eyebrow:0.16em;
--ls-btn:0.08em;

--fs-eyebrow:12px;
--fs-label:13px;
--fs-body-sm:14px;
--fs-body:17px;
--fs-body-lg:19px;
--fs-h3:22px;
--fs-h2:clamp(26px,3.2vw,33px);
--fs-h1:clamp(32px,4.4vw,46px);
--fs-display:clamp(42px,6vw,64px);

--fw-regular:400;
--fw-semibold:600;
--fw-bold:700;
--fw-extrabold:700;

--space-xs:4px;
--space-sm:8px;
--space-md:16px;
--space-lg:24px;
--space-xl:40px;
--space-2xl:64px;
--space-3xl:88px;
--section-y:88px;
--section-y-sm:56px;
--gutter:32px;
--gutter-sm:20px;

--border-hairline:1px;
--border-rule:2px;
--border-accent:3px;

--radius-sm:4px;
--radius-md:8px;
--radius-lg:16px;
--radius-pill:99px;

--measure:68ch;
--container:1120px;
}

/* ---- 3. the contract ------------------------------------------------------
   Every pairing below has been run through tools/contrast.py. See the notes in
   design-systems/houston-credit-builder/tokens/colors.css for what changed and
   why — three values here were revised while wiring this exact file, because
   the contract checks pairings (eyebrow-on-bg-alt, alert-text-on-its-own-tint)
   that the standalone swatches didn't need to clear. */
:root{
  /* surfaces */
  --s-bg:var(--bg-light);
  --s-bg-alt:var(--bg-light-alt);
  --s-surface-dark:var(--bg-dark);
  --s-surface-dark-alt:var(--surface-dark-alt);   /* one step lighter than espresso, for card separation. Cream on it = 10.09:1 */

  /* text */
  --s-text:var(--text-on-light-primary);
  --s-text-muted:var(--text-muted);
  --s-text-invert:var(--text-on-dark-primary);
  --s-text-invert-muted:var(--text-on-dark-muted);  /* warm tan — AA (5.84:1) on espresso bg-dark */

  /* accent (blue) — three separate decisions, all measured (see colors.css notes) */
  --s-accent:var(--accent-decorative);        /* decorative only, LIGHT grounds: rules, borders, bullets, nav underline. Never text, never on dark. */
  --s-accent-text:var(--accent-text);         /* accent blue AS TEXT on light grounds — clears bg (5.81) AND bg-alt (5.20) */
  --s-accent-on-dark:var(--accent-on-dark);   /* accent blue AS TEXT/UI on espresso — 6.26:1 */

  /* primary (terracotta) as DECORATION on light grounds — used in §4c.
     #B0592A measures 4.56:1 on --bg and 4.08:1 on --bg-alt, so it is safe for
     large display numerals and rules (both need only 3:1) but NOT for small
     body text on bg-alt. Dark grounds keep the blue-on-dark accent: terracotta
     on espresso is nowhere near a usable ratio. */
  --s-primary:var(--brand-primary);

  --s-link:var(--accent-text);                /* body links are the blue accent on light — 5.81:1 */
  --s-link-on-dark:var(--link-on-dark);       /* accent-decorative blue only reaches 2.50:1 on espresso — links on dark need the lighter tint */

  /* buttons — two different jobs, deliberately different colors:
     primary (sienna, the dominant earthy brand) is the ordinary "submit / confirm" action.
     secondary (blue accent) is reserved for exactly one thing across the whole
     site — "Free Consultation" — so it stays meaningful instead of decorative. */
  --s-btn-primary-bg:var(--brand-primary);
  --s-btn-primary-text:#FFFFFF;               /* pure white, not cream: cream (#F5F1EA) on the lighter terracotta is only 4.33; white is 4.87 AA */
  --s-btn-primary-bg-hover:#96471E;           /* darker terracotta — white on it 6.53:1 */
  --s-btn-secondary-bg:var(--accent-text);
  --s-btn-secondary-text:#FFFFFF;
  --s-btn-secondary-bg-hover:#2A506E;         /* darker blue — white on it 8.50:1 */

  /* lines + focus */
  --s-border:var(--border-hairline-light);
  --s-border-invert:var(--border-hairline-dark);
  --s-focus:var(--accent-text);               /* blue, 5.81:1 on light — well clear of the 3:1 UI bar */
  --s-focus-on-dark:var(--accent-on-dark);    /* blue, 6.26:1 on espresso */

  /* state */
  --s-success:var(--color-success);
  --s-success-tint:rgba(63,107,66,.06);
  --s-warning:var(--color-warning-text);
  --s-warning-tint:rgba(131,90,28,.06);
  --s-error:var(--color-error);
  --s-error-tint:rgba(166,64,47,.06);

  /* type */
  --s-font-display:var(--font-display);
  --s-font-sans:var(--font-body);

  --s-size-display:var(--fs-display);
  --s-size-h1:var(--fs-h1);
  --s-size-h2:var(--fs-h2);
  --s-size-h3:var(--fs-h3);
  --s-size-lead:var(--fs-body-lg);
  --s-size-body:var(--fs-body);
  --s-size-small:var(--fs-body-sm);
  --s-size-label:var(--fs-label);
  --s-size-eyebrow:var(--fs-eyebrow);

  --s-lh-display:var(--lh-display);
  --s-lh-heading:var(--lh-heading);
  --s-lh-lead:var(--lh-lead);
  --s-lh-body:var(--lh-body);

  --s-track-display:var(--ls-display);
  --s-track-heading:var(--ls-heading);
  --s-track-label:var(--ls-eyebrow);
  --s-track-btn:var(--ls-btn);

  --s-fw-regular:var(--fw-regular);
  --s-fw-semibold:var(--fw-semibold);
  --s-fw-bold:var(--fw-bold);
  --s-fw-extrabold:var(--fw-extrabold);

  /* Fraunces headings read as sentence case, not the shouty uppercase a lot of
     service-business templates default to — matches "welcoming," not "corporate." */
  --s-heading-transform:none;
  --s-label-transform:uppercase;
  --s-btn-transform:none;

  /* space */
  --s-space-xs:var(--space-xs);
  --s-space-sm:var(--space-sm);
  --s-space-md:var(--space-md);
  --s-space-lg:var(--space-lg);
  --s-space-xl:var(--space-xl);
  --s-space-2xl:var(--space-2xl);
  --s-space-3xl:var(--space-3xl);
  --s-section-y:var(--section-y);
  --s-section-y-sm:var(--section-y-sm);
  --s-gutter:var(--gutter);
  --s-gutter-sm:var(--gutter-sm);
  --s-maxw:var(--container);
  --s-measure:var(--measure);

  /* form + motion */
  --s-radius:var(--radius-md);
  --s-radius-btn:var(--radius-sm);
  --s-radius-pill:var(--radius-pill);
  --s-border-w:var(--border-hairline);
  --s-border-rule:var(--border-rule);
  --s-border-accent:var(--border-accent);
  --s-shadow-card:0 1px 2px rgba(78,58,43,.05), 0 10px 24px rgba(78,58,43,.06);  /* soft warm-COFFEE-tinted lift, never black (see texture/depth block below) */
  --s-transition:.15s ease;
  --s-header-h:76px;
}

@media (max-width:620px){
  :root{--s-header-h:64px}
}

/* ---- 4. client additions ---------------------------------------------------
   This build carries no public phone number: every CTA is a "Free Consultation"
   button routing to the contact form. The pattern below replaces base.css's
   assumed tel: link with that button across header, footer and hero. If a
   public number is confirmed later it can be ADDED alongside this pattern,
   not instead of it. */
.header__cta{
  background:var(--s-btn-secondary-bg);color:var(--s-btn-secondary-text);
  font-size:var(--s-size-small);font-weight:var(--s-fw-bold);
  letter-spacing:.02em;
  padding:12px 20px;border-radius:var(--s-radius-btn);
  text-decoration:none;white-space:nowrap;
  transition:background-color var(--s-transition);
}
.header__cta:hover{background:var(--s-btn-secondary-bg-hover)}

/* Trust strip — verified-fact chips that used to live inside .hero. Moved out
   into their own slim section per design-taste-frontend's hero-stack rule:
   the hero is eyebrow + headline + subtext + CTAs and nothing else: a trust
   micro-strip crammed into the hero is exactly the pattern that rule bans. */
.trust-strip{background:var(--s-bg-alt);border-bottom:var(--s-border-w) solid var(--s-border)}
.trust-strip__row{
  display:flex;flex-wrap:wrap;gap:var(--s-space-lg);
  /* Longhand on purpose: this element is also .wrap, and the `padding: y 0`
     shorthand wiped .wrap's horizontal gutter, leaving the strip flush to the
     viewport edge on mobile and 32px out of alignment with every heading on
     desktop. Keep the vertical padding on its own properties. */
  padding-top:var(--s-space-md);padding-bottom:var(--s-space-md);
  font-size:var(--s-size-eyebrow);font-weight:var(--s-fw-bold);
  letter-spacing:var(--s-track-label);text-transform:uppercase;
  color:var(--s-text-muted);
}
.trust-strip__row span{position:relative;padding-left:var(--s-space-lg)}
.trust-strip__row span:first-child{padding-left:0}
.trust-strip__row span:not(:first-child)::before{
  content:"";position:absolute;left:0;top:2px;bottom:2px;width:var(--s-border-w);
  background:var(--s-border);
}

/* "Why Houston Credit Builders" — deliberately NOT a third equal-card grid on
   this page (the services section above already is one). Split header + a
   stacked trait list gives the homepage a second, distinct layout family
   instead of repeating the same card rhythm twice in a row. */
/* The heading column is short and capped at 40ch; the trait list is long. The
   taller the trait column runs, the more empty ground sits under the heading,
   so the split is weighted toward the list rather than 1:1.3. */
.why-split{display:grid;grid-template-columns:minmax(0,.85fr) minmax(0,1.55fr);gap:var(--s-space-2xl);align-items:start}
.why-split__head{margin-bottom:0;max-width:40ch}
.trait-list{display:flex;flex-direction:column;gap:var(--s-space-lg)}
.trait-list .step{padding-top:var(--s-space-md)}
@media (max-width:760px){
  .why-split{grid-template-columns:1fr}
}

/* ---- 4b. vertical rhythm --------------------------------------------------
   Overrides, not token edits and not base.css edits. --section-y (88px) and
   --section-y-sm (56px) stay mirrored byte-for-byte from the design system in
   §2 so tools/qa.py's drift check passes; the rhythm this build actually wants
   is tighter than that, so the COMPONENTS are overridden here instead.

   Measured on the rendered pages (2026-09-04, 1440px viewport): section padding
   alone accounted for ~1,050px of the homepage's 3,138px, and every one of the
   six pages had bands where the padding was 45-49% of the section's height.
   That is what read as dead space.

   Two rules for anyone changing these:
   - Scope the band paddings to `screen and (min-width:901px)`. base.css already
     drops .hero/.section/.block to --section-y-sm below 900px and to 12pt for
     print; an unscoped rule here sits after that @import and would silently
     win in both. Sections that carry their own unconditional value below are
     ones base.css's mobile rule never targeted anyway.
   - Nothing here touches the --space-* scale. Changing the scale moves gaps
     inside components too, which is a different (and much riskier) edit.

   (a) The band rhythm. 88+88 put 176px between every pair of sections; 56+56
       reads as a deliberate break without the void. */
@media screen and (min-width:901px){
  .section,
  .block,
  .hero,
  .error-page{padding-top:56px;padding-bottom:56px}
  .footer{padding-top:52px;padding-bottom:var(--s-space-lg)}
  .section--tight{padding-top:40px;padding-bottom:40px}
}

/*   (b) The closing dark CTA repeats on all six pages and was the worst case:
       389px tall around 213px of content. It is a heading, a line and two
       buttons — it does not need a full band's padding at any width. */
.section--dark{padding:40px 0}

/*   (c) Compact heroes on the interior pages are a heading plus one line, and
       were sitting in 128px of padding for 138px of content. */
.hero--compact{padding:32px 0}

/*   (d) The gap under a section heading. 40px is the scale's --space-xl and is
       right when a heading introduces a long block; here it introduces a card
       row or a three-item grid that is already visually separate. */
.section-head{margin-bottom:28px}
.block__head{margin-bottom:24px}

/*   (e) The FAQ list. Its problem was containment AND width: a 707px measure
       floating in a 1440px viewport, 1,537px tall, with nothing holding the
       column. Hairline rules give the answers a spine; above 960px the list
       runs in two columns inside the full 1120px container, which uses the
       horizontal room the single column was wasting and roughly halves the
       page's height. faq.html drops .wrap--narrow so the wide layout has the
       container to work in.

       The rule is on every item rather than on `div + div`, because in a
       two-column grid the source-order sibling is not the one rendered above.
       Whichever items start a column get it stripped back off. */
.faq-list{gap:0}
.faq-list > div{
  /* 24 + 24 puts the rule midway and totals 48px between answers, down from the
     64px void it replaces. Going wider (40+40) reads as more space, not less. */
  margin-top:var(--s-space-lg);
  padding-top:var(--s-space-lg);
  border-top:var(--s-border-w) solid var(--s-border);
}
.faq-list > div:first-child{margin-top:0;padding-top:0;border-top:0}
.faq-list h3{margin-bottom:var(--s-space-sm)}

@media (min-width:960px){
  .faq-list{grid-template-columns:repeat(2,minmax(0,1fr));column-gap:var(--s-space-2xl)}
  /* Two columns means two items sit on the top row, not one. */
  .faq-list > div:nth-child(2){margin-top:0;padding-top:0;border-top:0}
}

/* ---- 4c. brand presence ----------------------------------------------------
   Measured on the rendered homepage 2026-09-04: terracotta, the brand's stated
   primary, covered 0.17% of the page. Coffee brown covered 37%. The palette was
   running entirely on its neutrals, with blue doing both the decorative work
   AND the call-to-action work — which is why nothing read as this brand
   specifically.

   The rule now: TERRACOTTA is the recurring structural decoration (numerals,
   rules, markers). BLUE is reserved for things you can act on (CTAs, links,
   eyebrow labels). Blue means something again because it is scarcer.

   Contrast is why this is decoration and not text: see --s-primary in §2.
   Dark sections are deliberately untouched here — base.css already routes
   .section--dark .step__n to the on-dark blue, and its two-class specificity
   wins over these single-class rules regardless of order. */

/* (a) Numerals as a signature. They already recur in three places (home cards,
       the steps, the prices blocks) but were 13-22px and grey-blue. Big, warm
       and wonky, they become the thing the eye remembers. */
.link-card__num,
.step__n,
.block__n{
  color:var(--s-primary);
  font-weight:var(--s-fw-semibold);
  line-height:1;
}
.link-card__num{font-size:34px;margin-bottom:var(--s-space-sm)}
.step__n{font-size:42px;margin-bottom:var(--s-space-xs)}
.block__n{font-size:38px;padding-top:var(--s-space-sm)}

/* (b) The rules and markers that structure every page. */
.step{border-top-color:var(--s-primary)}
.block__n{border-top-color:var(--s-primary)}
.spec-list li::before{background:var(--s-primary)}
.callout{border-left-color:var(--s-primary)}

/* ---- 4d. legal -------------------------------------------------------------
   The footer legal block that carries the registration statement, and the two
   prose pages it links to.

   Deliberately PLAIN TEXT — no badge, no seal, no framed panel. Texas registers
   credit services organizations, it does not vet or endorse them, and a designed
   seal reads as a government mark. Styling this block to look official would
   turn a true statement into a misleading one. Do not "improve" it into a
   badge. */
.footer__legal{
  padding-top:var(--s-space-lg);
  font-size:var(--s-size-small);color:var(--s-text-invert-muted);max-width:70ch;
}
.footer__legal p{margin:0 0 var(--s-space-sm)}
.footer__legal p:last-child{margin-bottom:0}
/* .footer__top already ends in a hairline, so the block takes no border of its
   own — two adjacent rules read as a rendering fault. The separator moves to
   the copyright strip instead, and only when the legal block is above it. */
.footer__legal + .footer__bottom{
  margin-top:var(--s-space-lg);
  border-top:var(--s-border-w) solid var(--s-border-invert);
}

/* Values filled from site-settings.js by scripts/settings.js. [hidden] needs the
   !important because layout rules (the contact panel's dl rows) set display and
   would otherwise win. .setting-missing marks a REQUIRED setting left blank, so
   the gap is obvious on review rather than silently empty. */
[hidden]{display:none !important}
.setting-missing{
  background:#FFE08A;color:#3A2A00;padding:0 .3em;border-radius:3px;font-weight:600;
}

/* Consent checkbox on the contact form. Reuses base.css's .check component
   rather than restyling a .label — .check already has the hit target, the
   custom box and the checked state. Only the error pairing is missing, because
   base.css only ever wires [data-state="error"] to .input/.label. */
.field[data-state="error"] .check{color:var(--s-error)}
.field[data-state="error"] .check input{border-color:var(--s-error)}
.check a{color:var(--s-link);text-underline-offset:2px}

/* The privacy line under the submit button. */
.form-note{
  max-width:620px;margin:var(--s-space-md) 0 0;
  font-size:var(--s-size-small);color:var(--s-text-muted);line-height:var(--s-lh-body);
}

/* Prose pages — privacy.html, terms.html. Long-form legal text is the one
   place on this site that gets numbered headings and a jump list: people
   arrive at these pages looking for one specific clause, not to read. */
.legal{max-width:var(--s-measure)}
.legal h2{
  font-size:var(--s-size-h3);
  margin:var(--s-space-2xl) 0 var(--s-space-md);
  padding-top:var(--s-space-lg);
  border-top:var(--s-border-w) solid var(--s-border);
  scroll-margin-top:calc(var(--s-header-h) + var(--s-space-lg));
}
.legal h3{
  font-size:var(--s-size-body);font-family:var(--s-font-sans);
  font-weight:var(--s-fw-bold);letter-spacing:0;
  margin:var(--s-space-xl) 0 var(--s-space-sm);
}
.legal p{margin:0 0 var(--s-space-md);line-height:var(--s-lh-body)}
.legal ul{margin:0 0 var(--s-space-md);padding-left:var(--s-space-lg)}
.legal li{margin-bottom:var(--s-space-sm);line-height:var(--s-lh-body)}
.legal li::marker{color:var(--s-accent)}
.legal a{color:var(--s-link)}

.legal__meta{
  font-size:var(--s-size-eyebrow);font-weight:var(--s-fw-bold);
  letter-spacing:var(--s-track-label);text-transform:var(--s-label-transform);
  color:var(--s-text-muted);
}

/* Sits above the jump list on terms.html: the one thing a reader must not
   miss is that this page is NOT the service agreement. */
.legal__callout{
  background:var(--s-bg-alt);
  border-left:var(--s-border-accent) solid var(--s-primary);
  border-radius:var(--s-radius);
  padding:var(--s-space-lg);margin:var(--s-space-xl) 0;
}
.legal__callout p:last-child{margin-bottom:0}

.legal__toc{
  margin:var(--s-space-xl) 0 var(--s-space-2xl);
  padding:var(--s-space-lg) var(--s-space-lg) var(--s-space-md);
  border:var(--s-border-w) solid var(--s-border);border-radius:var(--s-radius);
}
.legal__toc-title{
  font-family:var(--s-font-sans);font-size:var(--s-size-eyebrow);
  font-weight:var(--s-fw-extrabold);letter-spacing:var(--s-track-label);
  text-transform:var(--s-label-transform);color:var(--s-text-muted);
  margin:0 0 var(--s-space-md);padding-top:0;border-top:0;
}
.legal__toc ol{
  margin:0;padding-left:var(--s-space-lg);
  columns:2;column-gap:var(--s-space-xl);
  font-size:var(--s-size-small);
}
.legal__toc li{margin-bottom:var(--s-space-xs);break-inside:avoid}
@media (max-width:620px){
  .legal__toc ol{columns:1}
}

/* ---- 4e. blog ---------------------------------------------------------------
   blog/ is generated by tools/build-blog.mjs from blog_articles/*.txt. The
   listing is one column of cards at reading width, newest first; a post is
   plain prose on the same measure as the legal pages. */
.post-list{
  list-style:none;margin:0;padding:0;max-width:var(--s-measure);
  display:flex;flex-direction:column;gap:var(--s-space-lg);
}
.post-card__date{
  font-size:var(--s-size-eyebrow);font-weight:var(--s-fw-bold);
  letter-spacing:var(--s-track-label);text-transform:var(--s-label-transform);
  color:var(--s-text-muted);
}
.post-card__title{font-size:var(--s-size-h3);margin:var(--s-space-xs) 0 var(--s-space-sm)}
.post-card__title a{color:inherit;text-decoration:none}
.post-card__title a:hover{text-decoration:underline;text-underline-offset:4px;text-decoration-thickness:1px}
.post-card__more{margin-top:var(--s-space-md);align-self:flex-start}
.post-empty{color:var(--s-text-muted)}

.post{max-width:var(--s-measure)}
.post h2{font-size:var(--s-size-h3);margin:var(--s-space-xl) 0 var(--s-space-md)}
.post p{margin:0 0 var(--s-space-md);line-height:var(--s-lh-body)}
.post ul{margin:0 0 var(--s-space-md);padding-left:var(--s-space-lg)}
.post li{margin-bottom:var(--s-space-sm);line-height:var(--s-lh-body)}
.post li::marker{color:var(--s-accent)}
.post a{color:var(--s-link)}
.post__back{margin:var(--s-space-xl) 0 0}

/* The Blog link made the nav seven items. Just above the 900px drawer
   breakpoint that squeezed the header wordmark from two lines to three, so
   the nav gives back some of its gap there. */
@media (min-width:901px) and (max-width:1120px){
  .nav{gap:var(--s-space-md)}
}

/* ---- 5. texture & depth ----------------------------------------------------
   The flat v2 build read a little sterile for a warm, earthy, trust-first brand.
   Three subtle passes add warmth and dimension WITHOUT motion or flashiness:
   (a) fine paper grain over the whole page, (b) warm-tinted elevation on cards
   and the CTA (shadow set on --s-shadow-card above), (c) a same-hue tonal
   vignette on the dark sections. Every shadow is tinted to the coffee/blue
   brand hues, never black (design-taste-frontend 4.4). The biggest remaining
   depth lever is real warm photography, which needs client assets. */

/* (a) PAPER GRAIN. Fixed + pointer-events-none per the perf rule (grain must
       never sit on a scrolling container). ~4.5% opacity: tooth, not noise.
       Reads as paper tooth on the ivory, matte fiber on the coffee. */
body::after{
  content:"";
  position:fixed; inset:0; z-index:60; pointer-events:none;
  opacity:.045;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='170'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* (b) TACTILE CTA. The primary action gets a deeper, blue-tinted lift and a real
       press on :active (base.css already carries the transition + reduced-motion
       guard). Keeps depth on the one thing that matters most. */
.btn--secondary{box-shadow:0 2px 5px rgba(53,101,138,.20), 0 12px 26px rgba(53,101,138,.22);}
.btn--secondary:hover{transform:translateY(-1px)}
.btn--secondary:active{transform:translateY(0)}
.btn--primary{box-shadow:0 2px 5px rgba(78,58,43,.14), 0 12px 24px rgba(78,58,43,.16);}
.btn--primary:hover{transform:translateY(-1px)}
.btn--primary:active{transform:translateY(0)}

/* (c) TONAL DEPTH on the dark ground. A soft same-hue vignette so the coffee
       hero/footer/dark panels aren't one flat block. CRITICAL: the lightest
       point equals the token coffee (#4E3A2B) and everywhere else is darker, so
       no text area is lighter than the value the blue accent (4.87:1) and cream
       (9.50:1) were verified on — the vignette only ever improves contrast. */
.hero,
.section--dark,
.footer,
.error-page{
  background-color:var(--s-surface-dark);
  background-image:radial-gradient(135% 130% at 50% 22%, #4E3A2B 0%, #4E3A2B 42%, #43301F 100%);
}
.contact-panel{
  background-color:var(--s-surface-dark);
  background-image:radial-gradient(140% 130% at 30% 0%, #4E3A2B 0%, #4E3A2B 46%, #43301F 100%);
}

/* ---- 6. card materiality ---------------------------------------------------
   The card grids ("What we do", the trait sections) were flat ivory-on-ivory
   boxes reading only via a hairline. Bumping the card surface to a bright warm
   near-white makes them read as raised paper panels ON the warm ground, paired
   with the warm shadow from §5. Subtle, no accent-bar-on-card tell. Text still
   clears AAA/AA on the brighter surface (14.7 / 5.9). */
.card,
.section--alt .card{background:#FFFDF9}

/* ---- 7. imagery ------------------------------------------------------------
   Warm-graded photography, toned to match the brand so it reads as one earthy
   set rather than assorted stock. Two pieces:
   (a) .media  - a framed, object-fit image tile used in content sections.
   (b) .hero--split - an asymmetric split hero (copy left, image right) on the
       home + prices pages, per the design-taste anti-center-hero preference.
   Images are decorative-illustrative and are never captioned as a specific
   person. */

.media{
  border-radius:var(--s-radius);
  overflow:hidden;
  background:var(--s-surface-dark-alt);            /* warm fallback while loading */
  box-shadow:0 1px 2px rgba(78,58,43,.06), 0 14px 30px rgba(78,58,43,.10);
  aspect-ratio:4/5;                                 /* portrait default */
}
.media img{display:block;width:100%;height:100%;object-fit:cover}
.media--wide{aspect-ratio:16/10}
.media--tall{aspect-ratio:3/4}

/* Copy column beside a media tile. Same rule as the split hero below: the COPY
   sets the row height and the tile fills it. A fixed aspect-ratio tile in a
   ~530px column comes to ~700px tall, which on the about page left 260px of
   empty ground under a 440px copy column (measured 2026-09-04). The tile keeps
   .media's frame and shadow and loses only its ratio; object-position is biased
   up because these are portrait sources and the subject sits in the top half. */
.media-split{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:var(--s-space-2xl);
  align-items:stretch;
}
.media-split .media{position:relative;aspect-ratio:auto;min-height:360px}
.media-split .media img{position:absolute;inset:0;object-position:center 32%}
@media (max-width:860px){
  /* Stacked, so there is no copy column for the tile to match and nothing to
     save by letting it size itself — it gets a ratio back. 4/3 rather than
     .media's 4/5 default: a portrait tile under the copy is a long scroll past
     on a phone. */
  .media-split{grid-template-columns:1fr;gap:var(--s-space-xl)}
  .media-split .media{aspect-ratio:4/3;min-height:0}
}

/* Split hero — copy left, photograph bleeding off the right edge.

   v2.2 (2026-09-04). Two earlier versions of this are worth knowing about so
   nobody walks back into them:

   v1 boxed the image in its column with a fixed 4/5 ratio. A ~500px column
   made it 540px tall next to ~330px of copy, and `align-items:center` split
   the leftover into a void above and below the headline — 691px of hero for
   330px of copy.

   v2 made the copy set the height and stretched the image to match. That fixed
   the void, but a 470x380 tile with a hairline ring reads as a thumbnail
   dropped into the band, not as a hero.

   v2.3 takes the image OUT OF FLOW and pins it to the hero band itself: floor
   to ceiling, running off the right edge of the viewport, dissolving into the
   coffee ground on its left. §5 already named real warm photography as the
   biggest remaining depth lever on this build — this is what spending it looks
   like. The drama comes from the photograph, not from an effect layered on it.

   HOW THE PINNING WORKS. base.css gives .hero `position:relative` and
   `overflow:hidden`, so the tile is absolutely positioned against the BAND and
   the overhang past the viewport edge is clipped rather than scrolled. (That
   clipping is also why the width can be a vw unit without the scrollbar
   causing horizontal overflow.) The grid keeps a second column even though it
   is now empty: the fr units are explicit, so the track still reserves the
   space that keeps the copy clear of the photograph at every width. Checked at
   900 / 1440 / 2560px — the smallest gap between copy and image is 49px.

   The band's height is set entirely by the copy column now, so the min-height
   is what stops the prices hero (a heading, one line and one button) from
   collapsing to a strip too short to carry a photograph. */
/* THE HOMEPAGE HEADLINE RUNS AT DISPLAY SIZE. --s-size-display (42-64px), its
   1.05 leading and its display tracking are all in the contract, mirrored from
   the design system — and until now NOTHING on the site consumed them, so the
   largest type anywhere was h1 at 46px. A hero that has to carry the whole
   first impression was running one step off the scale's top.

   The selector is the page, not a new class: index.html is the only .hero that
   is not .hero--compact, and 404.html is .error-page rather than .hero. The
   interior pages keep h1 — their heroes are a heading and a line, and display
   size on those would be shouting.

   14ch, down from base.css's 16ch, because the measure has to come down as the
   size goes up or the headline runs to the reserved image column. */
.hero:not(.hero--compact) h1{
  font-size:var(--s-size-display);
  line-height:var(--s-lh-display);
  letter-spacing:var(--s-track-display);
  max-width:14ch;
}

/* Fraunces' italic is already in the §1 @import (the `ital` axis, both 0 and 1)
   and, like the display scale, was being paid for and never used. One italic
   word puts the stress on the word the sentence turns on. Same weight as the
   roman — dropping the weight as well makes it read as a mistake rather than
   emphasis. */
.hero h1 em{font-style:italic}

/* A short terracotta rule leading the eyebrow. §4c's rule is that terracotta is
   the recurring structural decoration and blue is reserved for things you can
   act on; the eyebrow text stays blue (--s-accent-on-dark, 4.87:1) and only the
   mark is terracotta. Decoration, never a carrier of meaning, so the 2.4:1 of
   terracotta on coffee is not a text or UI pairing. */
.hero .eyebrow{display:flex;align-items:center;gap:12px}
.hero .eyebrow::before{content:"";flex:none;width:28px;height:2px;background:var(--s-primary)}

.hero--split .hero__grid{
  display:grid;
  grid-template-columns:1.15fr .85fr;
  gap:var(--s-space-2xl);
  align-items:center;
  min-height:340px;
}
.hero__media{position:relative;overflow:hidden}
.hero__media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}

@media (min-width:861px){
  .hero--split .hero__media{
    position:absolute;
    top:0;bottom:0;right:0;
    /* Capped so the tile does not become a letterbox strip on a very wide
       monitor: past ~1900px it stops growing and simply sits further right. */
    width:clamp(320px,46vw,880px);
    border-radius:0;
  }
  /* Both sources are portrait (1000x1500) and the bleeding frame is landscape,
     so a centred crop takes the top off the subject — it decapitated the woman
     on prices.html outright. Biased up, since in both photographs the person is
     in the upper half and the desk fills the lower. */
  .hero--split .hero__media img{object-position:50% 22%}

  /* The scrim is what makes it a bleed rather than a pasted rectangle. It is
     the ground colour itself, opaque at the left edge and gone by 45%, so the
     photograph resolves out of the coffee instead of butting against it. Sits
     over the <img> by paint order — no z-index needed. */
  .hero--split .hero__media::after{
    content:"";
    position:absolute;inset:0;
    /* Literal coffee, not var() + color-mix, and matching §5(c)'s vignette
       literal deliberately. Two reasons: an unsupported color-mix inside a
       gradient invalidates the whole declaration (the scrim would vanish and
       the photograph would butt hard against the ground, with no @supports to
       catch it), and the stops have to agree with the vignette this sits on
       top of or the blend shows a seam. Fade to transparent COFFEE rather than
       `transparent` — the keyword is transparent black, and premultiplied
       interpolation is the only thing standing between that and a grey haze. */
    background:linear-gradient(90deg,
      #4E3A2B 0%,
      rgba(78,58,43,.60) 24%,
      rgba(78,58,43,0) 52%);
  }
}

@media (max-width:860px){
  .hero--split .hero__grid{grid-template-columns:1fr}
  .hero__media{display:none}                        /* text-first hero on mobile */
}


/* ---- 8. image reveal -------------------------------------------------------
   Each image fades in from the side opposite its text: the copy sits left in
   every current layout, so the images travel in from the right. The direction
   is set per element in the markup (`data-reveal="right|left"`) rather than
   inferred, so a flipped layout stays correct instead of silently animating the
   wrong way.

   Everything here is scoped to `.js`, which an inline snippet in each page's
   <head> sets before first paint. No JS means no hidden state and the images
   just render — the failure mode of a scroll reveal must never be an invisible
   image. See scripts/reveal.js.

   Only opacity and transform are animated, so this stays on the compositor and
   cannot shift layout. The images already carry width/height, so there is no
   CLS either way. */
.js [data-reveal]{
  opacity:0;
  transition:opacity .75s ease, transform .75s cubic-bezier(.22,.61,.36,1);
  will-change:opacity,transform;
}
.js [data-reveal="right"]{transform:translate3d(36px,0,0)}
.js [data-reveal="left"]{transform:translate3d(-36px,0,0)}
.js [data-reveal].is-revealed{opacity:1;transform:none;will-change:auto}

/* Stacked layouts have no "opposite side" to come from, so below the split
   breakpoint the motion becomes a short rise instead of a sideways slide. */
@media (max-width:860px){
  .js [data-reveal="right"],
  .js [data-reveal="left"]{transform:translate3d(0,24px,0)}
}

/* base.css's reduced-motion block already forces transition-duration to .01ms
   globally, and reveal.js reveals everything immediately when the query
   matches. This is the third belt: no starting offset at all. */
@media (prefers-reduced-motion:reduce){
  .js [data-reveal]{opacity:1;transform:none;transition:none}
}


/* ---- 9. ambient background -------------------------------------------------
   Large, very slow shapes drifting behind the page, with the sand-coloured
   bands let down to ~88% so they read through. Same discipline as §5(c)'s
   vignette: this must never cost contrast.

   HOW THAT IS GUARANTEED — the blobs are pitched between the two light grounds
   the site already uses, so they can never take any text below a ratio the
   palette has not already accepted. Measured relative luminance:
       --bg-light      #FAF7F1  L = 0.932   (ivory sections)
       --bg-light-alt  #F1EAE0  L = 0.829   (sand bands)
       clay blob       #F2E1CE  L = 0.772
       peach blob      #F7EADB  L = 0.837
       cream blob      #FDF8F0  L = 0.930
   All three are warm. An earlier pass used a cool blue for one of them; blue is
   the CTA accent on this site and belongs on things you can click, not in the
   scenery. Depth variation across the three warms does the work instead.
   The blobs land at roughly sand-band depth, so at their strongest an ivory
   section reads about as deep as the sand band next to it — a ground the same
   text already sits on. Chroma does most of the visible work; the luminance
   drop is the smaller part.

   THE BUDGET, explicitly: the binding pairing is --text-muted #6B6255
   (L 0.125) needing 4.5:1, which allows the background down to L = 0.739.
   These blobs bottom out around 0.80, leaving real margin. Do not go past 0.78
   without re-measuring.

   Tuned by measurement. Three metrics matter and they pull against each other:
   SEPARATION (the ivory/sand step across a section boundary, same x same frame,
   must stay positive or the bands merge), SAFETY (--text-muted on the darkest
   ground, AA floor 4.50) and VISIBILITY (max per-channel swing at a fixed point
   across the animation cycle; under 3/255 is invisible). Current measured
   values: separation 0.078, safety 4.65:1, visibility 9/255. If you retune, run
   all three — an earlier pass that looked safe on paper was invisible, and a
   later one that looked visible inverted the section alternation.

   Cheap by construction: three elements, radial-gradients rather than
   filter:blur (a blurred 60vw element is a real per-frame cost), and only
   transform is animated so it stays on the compositor.

   Sizes are ~46-58vw on purpose. An earlier pass used 70-80vw, which is wider
   than the viewport, so only a horizontal slice of each circle was ever on
   screen and they read as flat colour bands rather than as shapes. Keep them
   under ~60vw so the curvature is visible.

   Dark sections, the hero and the footer are deliberately left opaque. Their
   cream/blue-on-coffee ratios are the tightest on the site and the token file
   notes that #8FB4CC at 4.87:1 is what caps how light that ground may go. The
   solid dark bands are also what give the drifting light areas something to
   sit against. */
.ambient{
  position:fixed; inset:0; z-index:0; pointer-events:none; overflow:hidden;
  contain:strict;              /* isolate: never triggers layout elsewhere */
}
.header,main,.footer{position:relative; z-index:1}

.ambient__blob{
  position:absolute; border-radius:50%;
  will-change:transform;
  animation-iteration-count:infinite;
  animation-direction:alternate;
  animation-timing-function:cubic-bezier(.45,0,.55,1);
}
.ambient__blob--a{
  width:52vw; height:52vw; left:-10vw; top:-8vw;
  background:radial-gradient(circle at 50% 50%, #F2E1CE 0%, #F2E1CE 34%, rgba(242,225,206,0) 76%);
  animation-name:ambient-drift-a; animation-duration:58s;
}
.ambient__blob--b{
  width:58vw; height:58vw; right:-14vw; top:24%;
  background:radial-gradient(circle at 50% 50%, #FDF8F0 0%, #FDF8F0 32%, rgba(253,248,240,0) 74%);
  animation-name:ambient-drift-b; animation-duration:76s;
}
.ambient__blob--c{
  width:46vw; height:46vw; left:30vw; bottom:-16vw;
  background:radial-gradient(circle at 50% 50%, #F7EADB 0%, #F7EADB 30%, rgba(247,234,219,0) 78%);
  animation-name:ambient-drift-c; animation-duration:94s;
}

@keyframes ambient-drift-a{
  from{transform:translate3d(0,0,0) scale(1)}
  to  {transform:translate3d(16vw,12vh,0) scale(1.22)}
}
@keyframes ambient-drift-b{
  from{transform:translate3d(0,0,0) scale(1.06)}
  to  {transform:translate3d(-18vw,-13vh,0) scale(1)}
}
@keyframes ambient-drift-c{
  from{transform:translate3d(0,0,0) scale(1)}
  to  {transform:translate3d(13vw,-11vh,0) scale(1.18)}
}

/* WHY .section GETS AN EXPLICIT BACKGROUND, which it never had before:
   with no background of its own it showed the ambient layer at FULL strength,
   while the sand bands muted it to a fraction. Under a blob core an ivory
   section could therefore go darker than the sand band next to it, inverting
   the alternation and making the two read as one merged block. Giving every
   light ground the same alpha keeps the tonal step intact wherever the drift
   happens to be. Cards stay opaque on purpose — §6 makes them raised paper,
   and that only works if they are solid. */
/* The alternating band is deepened for this build, from the token sand #F1EAE0
   (L 0.829) to #F0E5D6 (L 0.794). Reason: the ivory/sand step was 0.103 and the
   ambient drift varied by more than that across a section boundary, so the two
   grounds could meet or invert and the alternation read as one merged block.
   At 0.138 apart the drift cannot close the gap; the measured worst-case step across a boundary is 0.11. Measured, not assumed:
   --text-muted #6B6255 on #F0E5D6 is 4.82:1, still AA. Do not deepen further
   without re-measuring — this ground is the binding pairing on the site.
   The token itself is untouched; this is a site-level override. */
:root{--ambient-bg-alt:#F0E5D6}
.section--alt,
.block--alt,
.trust-strip{background:var(--ambient-bg-alt)}

@supports (background:color-mix(in srgb,red 50%,transparent)){
  /* Deliberately NOT the same alpha. The sand band carries the binding pairing
     (--text-muted at 4.8:1) so it gets only 15% of the drift; the ivory ground
     starts at 5.6:1 and can afford 30%. The asymmetry is safe in the direction
     that matters: even at its darkest the ivory ground (L 0.88) stays clear of
     the sand ground at its lightest (L 0.81), so the alternation cannot invert.
     Measured worst-case step across a boundary: 0.08. */
  /* :not(.section--dark) is load-bearing, not tidiness. This block is the last
     thing in the file and `.section` matches a dark section too, at the same
     (0,1,0) specificity as the `.section--dark` rules in base.css and §5(c) —
     so the plain selector won on order and, being the `background` SHORTHAND,
     wiped the coffee colour AND §5(c)'s vignette off every closing CTA band.
     Measured 2026-09-04: those bands were painting #F8F5EF with --s-text-invert
     (#F5F1EA) on top, about 1.02:1. The dark grounds are opaque on purpose —
     the ambient layer is a light-ground effect and has nothing to show through
     on coffee. */
  .section:not(.section--dark),
  .block{background:color-mix(in srgb,var(--s-bg) 70%,transparent)}
  .section--alt,
  .block--alt,
  .trust-strip{background:color-mix(in srgb,var(--ambient-bg-alt) 85%,transparent)}
}

/* Motion is the decoration, not the shapes. With reduced motion the blobs stay
   exactly where they are — the page keeps its depth and loses only the drift. */
@media (prefers-reduced-motion:reduce){
  .ambient__blob{animation:none}
}
