/* ══════════════════════════════════════════════════════════════════════════
   SF-BASE — design-option variable consumption + structural hook base skins.
   Static template file (NOT overwritten at build). Loaded on every page right
   after the "SF type hook" style line, BEFORE the page's own inline styles.

   Contract (themes/options.mjs header): the storefront consumes
     · --sf-* option variables (every consumption below carries a fallback that
       reproduces the raw template's current look, so the template renders
       unchanged when no design is applied), and
     · opt-<option>-<value> body classes, whose rules ship inside the generated
       theme.css and are appended AFTER theme/layout/motion CSS.

   Specificity plan: consumption rules that must beat page utility/skin classes
   are written as `body .sf-*` (0,2,0). Catalog option rules are
   `body.opt-… .sf-*` (0,2,1) and always win. Never use IDs here.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── derived helpers (template-owned, not catalog-owned) ─────────────────── */
:root {
  /* Header height follows the nav-height option. 13px fallback keeps the raw
     template's exact 64px chrome math (38px logo row + 2×13px). */
  --sf-nav-h: calc(38px + 2 * var(--sf-nav-pad-y, 13px));
}

/* ── type rhythm ─────────────────────────────────────────────────────────── */
body {
  font-size: var(--sf-body-fs, 16px);
  line-height: var(--sf-body-lh, 1.6);
}

/* ── focus ring baseline (zero specificity — page rules still win) ───────── */
:where(a, button, input, select, textarea, summary):focus-visible {
  outline: var(--sf-focus-ring, 2px solid var(--sf-accent));
  outline-offset: var(--sf-focus-offset, 2px);
}

/* ── numerals ────────────────────────────────────────────────────────────── */
body .sf-price,
body .sf-purity-value,
body .sf-card-price-row {
  font-variant-numeric: var(--sf-numvariant, normal);
}

/* ── dividers ────────────────────────────────────────────────────────────── */
body .sf-divider {
  height: var(--sf-divider-h, 1px);
  background: var(--sf-divider-bg, color-mix(in srgb, var(--sf-text) 12%, transparent));
  border: none;
}

/* ── badges / status chips ───────────────────────────────────────────────── */
body .sf-badge,
body .sf-verified-pill {
  font-size: var(--sf-badge-fs, 0.72rem);
  padding: var(--sf-badge-pad, 3px 11px);
  border-radius: var(--sf-badge-radius, 4px);
}
body .sf-new-tag {
  font-size: var(--sf-badge-fs, 0.72rem);
  border-radius: var(--sf-badge-radius, 4px);
}

/* ── buttons ─────────────────────────────────────────────────────────────── */
body .sf-btn-primary,
body .sf-btn-secondary {
  border-radius: var(--sf-btn-radius, var(--sf-radius, 8px));
  font-weight: var(--sf-btn-weight, 600);
  text-transform: var(--sf-btn-case, none);
  letter-spacing: var(--sf-btn-track, 0.02em);
  padding: var(--sf-btn-pad-y, 0.75rem) var(--sf-btn-pad-x, 1.6rem);
  font-size: var(--sf-btn-fs, 0.875rem);
}
body .sf-btn-primary { box-shadow: var(--sf-btn-shadow, none); }

/* ── .btn-primary — ONE definition, sitewide ─────────────────────────────────
   Twelve pages carried their own `.btn-primary` skin in seven different bodies:
   three painted it in the accent, nine in `var(--sf-bg)` with a `var(--sf-bg)`
   border, and the padding/width/cursor set differed page by page. Collapsed
   here, into the stylesheet every page already loads, so a client's primary
   button is one decision.

   Accent, not --sf-bg, is the surviving skin. `background:var(--sf-bg)` with a
   same-colour border is invisible on a light theme (white button, white border,
   white page) and it is the mirror-bug shape that has already shipped twice in
   this template; --sf-accent / --sf-accent-text is the pair tokens.mjs judges
   for contrast and never emits failing, and it is what .snav-btn and
   .sf-btn-primary already use.

   Placed in sf-base.css, which loads BEFORE tailwind.min.css and before every
   page's own <style>: Tailwind utilities on the button (px-8, py-4, w-full)
   still win at equal specificity, so the padding below is only the default for
   buttons that carry no utilities. Widths belong to .sf-form-btn (below), never
   here — a full-width default would stretch every CTA on the site. */
.btn-primary {
  /* FILL role, so it takes --sf-brand: the client's own hex, unclamped. It
     equals --sf-accent for every client whose brand was not too dark for the
     accent-as-text clamp, so this changes nothing for them. For a deep-navy or
     oxblood brand it is the difference between the client's colour and a washed
     mid-tone the factory invented (themes/tokens.mjs, 2026-08-16). The fallback
     chain keeps a hand-written theme.css with no --sf-brand working. */
  background: var(--sf-brand, var(--sf-accent));
  color: var(--sf-brand-text, var(--sf-accent-text));
  border: 1px solid var(--sf-brand, var(--sf-accent));
  border-radius: var(--sf-btn-radius, var(--sf-radius));
  font-family: inherit;
  font-weight: var(--sf-btn-weight, 700);
  letter-spacing: 0.02em;
  padding: 14px 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.2s ease, box-shadow 0.2s ease;
}
.btn-primary:hover {
  background: var(--sf-accent-4);
  border-color: var(--sf-accent-4);
  transform: translateY(-2px);
  box-shadow: 0 8px 28px rgb(var(--sf-shadow) / 0.22);
}
.btn-primary:active { transform: translateY(0) scale(0.98); }
.btn-primary:focus-visible { outline: 2px solid var(--sf-accent); outline-offset: 3px; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; pointer-events: none; }
/* Touch devices: drop the sticky :hover lift so a tap activates on the first
   press instead of "tap to hover, tap again to click". Mouse hover unchanged. */
@media (hover: none) { .btn-primary:hover { transform: none; } }
/* Hero/banner CTA emphasis multiplier (cta-emphasis option). */
body .sf-btn-cta {
  font-size: calc(var(--sf-btn-fs, 0.875rem) * var(--sf-cta-scale, 1));
  padding: calc(var(--sf-btn-pad-y, 0.75rem) * var(--sf-cta-scale, 1))
           calc(var(--sf-btn-pad-x, 1.6rem) * var(--sf-cta-scale, 1));
}

/* ── forms ───────────────────────────────────────────────────────────────── */
body .sf-input {
  padding: var(--sf-input-pad-y, 10px) var(--sf-input-pad-x, 13px);
  font-size: var(--sf-input-fs, 0.88rem);
  border-radius: var(--sf-input-radius, 0px);
}
/* Field stacks opt into the field-gap option wherever the page marks them. */
body .sf-field-stack { display: flex; flex-direction: column; gap: var(--sf-field-gap, 14px); }
body .sf-form-btn { width: var(--sf-form-btn-w, 100%); }

/* ── overlays ────────────────────────────────────────────────────────────── */
body .sf-scrim {
  background: var(--sf-scrim, rgb(0 0 0 / 0.45));
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}
body .sf-modal-panel {
  border-radius: var(--sf-modal-radius, 14px);
  width: min(var(--sf-modal-max, 520px), 100%);
}
/* Toast base — the hook contract's floating notice. No template element
   carries it today (the engine has no transient toasts); base + catalog rules
   are ready for markup that does. */
body .sf-toast {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 240;
  background: var(--sf-elevated);
  color: var(--sf-text);
  border: 1px solid var(--sf-border-color, color-mix(in srgb, var(--sf-text) 12%, transparent));
  border-radius: var(--sf-menu-radius, 10px);
  padding: 12px 18px;
  font-size: 0.85rem;
  box-shadow: var(--sf-shadow-2, 0 18px 38px -14px rgb(0 0 0 / 0.4));
}

/* ── product-card anatomy extras ─────────────────────────────────────────── */
/* Category/meta line on tiles: visible by default, hidden by card-density
   minimal; the extra line only appears under card-density detailed. */
body .sf-card-meta {
  font-size: 0.66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--sf-muted);
}
body .sf-card-meta-extra {
  display: none;
  font-size: 0.75rem;
  color: var(--sf-muted);
}
body .sf-card-title { font-size: var(--sf-card-title-fs, 1rem); }

/* ── hero stats + trust row (shown on built sites; raw template hides) ───── */
.sf-hero-stats { display: flex; flex-wrap: wrap; gap: 12px 32px; margin-top: 1.6rem; }
.sf-hero-stats b { display: block; color: var(--sf-accent); font-size: 1.25em; font-weight: 800; letter-spacing: -0.02em; }
.sf-hero-stats span { color: var(--sf-muted); font-size: 0.85rem; }
.sf-trust-row { display: flex; flex-wrap: wrap; gap: 10px 22px; color: var(--sf-muted); font-size: 0.85rem; margin-top: 1.2rem; }
body.layout-default .sf-hero-stats,
body.layout-default .sf-trust-row { display: none; }

/* ── footer (class-level so footer-* options can restyle it) ─────────────── */
/* THE FOOTER IS BRAND-COLOURED (BUILD-DEFAULTS 2a) and it is painted HERE,
   from variables the footer-tone axis rewrites. Before this the ground lived
   in the axis values themselves, so body.opt-footer-tone-match at 0,2,0 beat
   this rule at 0,1,1 and the declaration below never painted a built site at
   all. Values that set variables instead of backgrounds make that specificity
   fight disappear.

   Every ink in this block used to be a percentage of --sf-text mixed toward
   transparent. Measured on the first real client, on its own footer ground:
   tagline 2.26, section title 2.09, link 3.08, fine print 1.78, copyright
   1.55. Those are WCAG failures that shipped, and they are failures on the
   OLD ground too - the brand ground did not create them, it made them
   visible. The quiet ink is now the solved --sf-footer-ink-dim. */
body .sf-footer {
  background: var(--sf-footer-bg, var(--sf-chrome-wash, var(--sf-bg)));
  color: var(--sf-footer-ink, var(--sf-text));
  border-top: 1px solid var(--sf-border-color, transparent);
  padding: var(--sf-footer-pad, 4.75rem) 2rem 1.5rem;
  font-family: var(--sf-font-body);
}
body .sf-footer-wrap { max-width: 1280px; margin: 0 auto; }
body .sf-footer-inner {
  display: grid;
  grid-template-columns: 1.55fr 1fr 1fr 1.2fr;
  gap: 3.5rem;
  padding-bottom: 3.25rem;
}
/* The brand column is the widest and the only one with prose in it, so it gets
   a measure rather than the full track: legal fine print set to 400px+ is the
   line length nobody reads. */
body .sf-ftr-brand { max-width: 34rem; }
@media (max-width: 1000px) {
  body .sf-footer-inner { grid-template-columns: 1fr 1fr; gap: 2.25rem; }
  body .sf-footer-inner > div:first-child { grid-column: 1 / -1; }
}
@media (max-width: 560px) {
  body .sf-footer-inner { grid-template-columns: 1fr; }
}
body .sf-footer-logo { display: flex; align-items: center; gap: 10px; margin-bottom: 1.35rem; }
/* No drop-shadow. The white halo behind the logo (and behind every nav link and
   icon) was the owner's own storefront signature; a client site a visitor
   recognises as that store is a failed deliverable. Same strip in _nav.part. */
/* The footer is where a TALL lockup belongs — it is the one brand block on the
   page with vertical room. The height is measured per client from the artwork's
   own proportions (--sf-logo-foot-h, factory/tools/lib/logo-prep.mjs): a wide
   mark+wordmark lock stays small, a stacked lockup gets the room its wordmark
   needs to be legible. The flat 36px this replaced was a nav-shaped number
   applied to every shape, and on a 2:1 lockup it put the words under 6px. */
body .sf-footer .sf-logo-img {
  height: var(--sf-logo-foot-h, 36px);
  width: auto;
  object-fit: contain;
  /* NONE (BUILD-DEFAULTS §2a). The wash starts on the page ground, so the logo
     sits on white and keeps every colour it was drawn with. This used to be a
     flat-white silhouette, which is the defect that named this task.

     --sf-logo-filter-FOOT, not -chrome (2026-08-19). The footer and the nav are
     two surfaces and they stopped sharing a ground: a client can now run a
     light bar over a dark page, and one variable cannot be right for both. The
     footer's value is decided by build.mjs pointFooterLogoAtItsGround() and it
     travels WITH the artwork — `none` whenever the footer is drawing
     LOGO-ONDARK (a real knockout that keeps the client's colours), and the old
     silhouette only where no knockout could be derived and something has to
     keep the mark visible. Never edit this to a literal: a hard-coded filter
     here is the flat-white blob this rule exists to stop. */
  filter: var(--sf-logo-filter-foot, var(--sf-logo-filter-chrome, none));
}
body .sf-ftr-tagline {
  color: var(--sf-footer-ink-dim, var(--sf-muted));
  font-size: 0.92rem;
  line-height: 1.75;
  margin-bottom: 1.15rem;
}
body .sf-footer-social { display: flex; gap: 7px; margin-top: 1.1rem; }
body .sf-footer-social a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  color: var(--sf-footer-ink, var(--sf-text));
  text-decoration: none;
  transition: color 0.15s ease;
}
body .sf-footer-social a:hover { color: var(--sf-footer-ink-dim, var(--sf-muted)); }
/* De-inlined footer text roles (shared by every page's footer markup). */
body .sf-ftr-title {
  color: var(--sf-footer-ink, var(--sf-text));
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 1.6rem;
}
body .sf-ftr-stack { display: flex; flex-direction: column; gap: 1.45rem; }
body .sf-ftr-link {
  color: var(--sf-footer-ink-dim, var(--sf-muted));
  font-size: 0.92rem;
  text-decoration: none;
  transition: color 0.15s ease;
}
body .sf-ftr-link:hover { color: var(--sf-footer-ink, var(--sf-text)); }
body .sf-footer .sf-muted { color: var(--sf-footer-ink-dim, var(--sf-muted)); }
body .sf-ftr-fine {
  color: var(--sf-footer-ink-dim, var(--sf-muted));
  font-size: 0.86rem;
  line-height: 1.72;
  margin-bottom: 1.5rem;
}
body .sf-ftr-copy { color: var(--sf-footer-ink-dim, var(--sf-muted)); font-size: 0.78rem; }
/* The RUO sentence in the footer is a COMPLIANCE surface, so it is the one
   footer ink that never takes the quiet register. It is a BADGE now rather than
   a line of micro-caps: on a light wash a 0.58rem letterspaced strap read as
   decoration, and this is the one line in the footer that has to read as a
   statement. The chip ground is a tint of the ink itself, so it composites
   against the wash it sits on and needs no second solved colour. */
body .sf-ftr-ruo {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--sf-footer-ink, var(--sf-text));
  background: color-mix(in srgb, var(--sf-bg) 72%, transparent);
  border: 1px solid color-mix(in srgb, var(--sf-footer-ink, var(--sf-text)) 16%, transparent);
  border-radius: var(--sf-badge-radius, 8px);
  padding: 0.72rem 1.05rem;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.09em;
}
body .sf-ftr-warn { color: var(--sf-footer-ink, var(--sf-text)); font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; }
body .sf-ftr-copy a { color: inherit; text-decoration: none; }

/* ── CONTACT COLUMN ────────────────────────────────────────────────────────
   Icon + value rows. The icon takes the accent register and the value takes
   the loud ink: a light footer has room for a coloured second voice that the
   brand-filled one never did, which is what --sf-footer-accent is for. Each row
   is deleted whole by build.mjs when its field is absent (data-sf-contact), so
   there is no rule here that hides one. */
body .sf-ftr-contact { display: flex; flex-direction: column; gap: 1.4rem; }
body .sf-ftr-ct { display: flex; align-items: flex-start; gap: 0.8rem; }
body .sf-ftr-ct-icon {
  flex: 0 0 auto;
  margin-top: 0.15rem;
  color: var(--sf-footer-accent, var(--sf-accent));
}
body .sf-ftr-ct-body {
  color: var(--sf-footer-ink-dim, var(--sf-muted));
  font-size: 0.88rem;
  line-height: 1.6;
  margin: 0;
}
body .sf-ftr-ct-strong {
  display: block;
  color: var(--sf-footer-accent, var(--sf-accent));
  font-size: 0.94rem;
  font-weight: 700;
  text-decoration: none;
  transition: color 0.15s ease;
}
body .sf-ftr-ct-strong:hover { color: var(--sf-footer-ink, var(--sf-text)); }
body .sf-ftr-ct-note {
  display: block;
  color: var(--sf-footer-ink-dim, var(--sf-muted));
  font-size: 0.72rem;
  margin-top: 0.25rem;
}
body .sf-ftr-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  align-self: flex-start;
  margin-top: 0.7rem;
  padding-bottom: 4px;
  border-bottom: 1px solid currentColor;
  color: var(--sf-footer-accent, var(--sf-accent));
  font-size: 0.82rem;
  font-weight: 700;
  text-decoration: none;
  transition: color 0.15s ease;
}
body .sf-ftr-cta:hover { color: var(--sf-footer-ink, var(--sf-text)); }

/* ── RESEARCH USE NOTICE ───────────────────────────────────────────────────
   The one compliance callout in the footer. The left rule is the accent, the
   panel is a tint of the ink over the wash — no third solved colour, because a
   surface that needs its own ink solved is a surface a gate has to learn. */
body .sf-ftr-notice {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding: 1.2rem 1.5rem;
  margin-bottom: 1.9rem;
  border: 1px solid color-mix(in srgb, var(--sf-footer-ink, var(--sf-text)) 12%, transparent);
  border-left: 4px solid var(--sf-footer-accent, var(--sf-accent));
  border-radius: var(--sf-card-radius, 10px);
  background: color-mix(in srgb, var(--sf-bg) 72%, transparent);
}
body .sf-ftr-notice-icon { flex: 0 0 auto; margin-top: 0.1rem; color: var(--sf-footer-accent, var(--sf-accent)); }
body .sf-ftr-notice-body { flex: 1 1 auto; min-width: 0; }
body .sf-ftr-notice-title {
  color: var(--sf-footer-ink, var(--sf-text));
  font-size: 0.88rem;
  font-weight: 700;
  margin: 0 0 0.3rem;
}
body .sf-ftr-notice-text {
  color: var(--sf-footer-ink-dim, var(--sf-muted));
  font-size: 0.78rem;
  line-height: 1.6;
  margin: 0;
}
body .sf-ftr-notice-link {
  flex: 0 0 auto;
  align-self: center;
  color: var(--sf-footer-accent, var(--sf-accent));
  font-size: 0.8rem;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.15s ease;
}
body .sf-ftr-notice-link:hover { color: var(--sf-footer-ink, var(--sf-text)); }
@media (max-width: 720px) {
  body .sf-ftr-notice { flex-wrap: wrap; }
  body .sf-ftr-notice-link { align-self: flex-start; margin-left: 2.5rem; }
}

/* ── BOTTOM BAR ────────────────────────────────────────────────────────────
   Separated by a rule rather than by its own ground: the wash already ends
   here, and a second band under a gradient reads as a seam. */
body .sf-ftr-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem 1.75rem;
  flex-wrap: wrap;
  padding-top: 1.6rem;
  border-top: 1px solid color-mix(in srgb, var(--sf-footer-ink, var(--sf-text)) 14%, transparent);
}
body .sf-ftr-bottom-links { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; }
body .sf-ftr-bottom-links .sf-ftr-link { font-size: 0.8rem; }
/* The per-value re-ink block that used to sit here is gone: every footer ink
   above resolves through --sf-footer-ink / --sf-footer-ink-dim, and each
   footer-tone value sets that pair, so a tone change needs no override rules. */

/* ── SECTION RHYTHM (BUILD-DEFAULTS 2a) ───────────────────────────────────
   .sf-section was a markup hook with no ground: the only .sf-section rules in
   the engine were padding, in the three layout stylesheets, so a home page had
   one continuous field from the hero to the footer whatever the markup said.
   On the first real client every band token was inside 4% of the page ground
   (measured 1.040:1), which is why no amount of section markup ever read as a
   rhythm.

   The alternation is EXPLICIT, not :nth-of-type: a band is a composition
   decision per page, and an implicit rule silently repaints whenever a section
   is inserted or removed. --sf-band is solved in themes/tokens.mjs against a
   contrast target and every ink is solved against it. */
body .sf-section { background: var(--sf-bg); }
body .sf-section.sf-band { background: var(--sf-band, var(--sf-surface)); }

/* ── PRODUCT PHOTO GROUND (BUILD-DEFAULTS 2a) ─────────────────────────────
   One token for the same artifact on all five surfaces. The vial master is a
   true cut-out - factory/tools/vial.mjs un-premultiplies against the render own
   white studio floor precisely so the pixels read on any ground - so nothing
   about the artwork changes here, only which ground it is given. */
body .sf-card-media,
body .card-img-wrap,
body .hero-tile-img,
body .sf-gallery,
body .sf-related .rel-media { background: var(--sf-photo-bg, var(--sf-elevated)); }

/* ── header behavior adaptation (nav-sticky option: "scrolls away") ──────── */
/* The template's #site-nav/#site-disclaimer are position:fixed at ID level; the
   catalog's `.sf-nav{position:static}` cannot beat an ID, so the template
   adapts itself off the emitted body class. */
body.opt-nav-sticky-static #site-nav { position: static; }
body.opt-nav-sticky-static #site-disclaimer { position: static; top: auto; }
/* _nav.part's RUO block adds `body { padding-top: 48px }` to clear the fixed
   strip. Once the strip is in normal flow that padding is a 48px hole, so it is
   taken back here — 0,1,0 beats the part's 0,0,1 regardless of source order. */
body.opt-nav-sticky-static { padding-top: 0; }
body.opt-nav-sticky-static main { padding-top: 32px !important; }
body.opt-nav-sticky-static #hero { margin-top: 0 !important; }

/* ── mobile menu adaptation (mobile-menu option) ─────────────────────────── */
body.opt-mobile-menu-full #snav-mobile-menu { width: 100%; max-width: 100vw; border-right: none; }
body.opt-mobile-menu-sheet #snav-mobile-menu { background: var(--sf-elevated); }

/* ── decorative canvases ─────────────────────────────────────────────────── */
/* Density is read by the canvas JS once at init (--sf-particle-density);
   the off value's rule hides .sf-particles, the beams option hides .sf-beams. */

/* ══ THE CLOSING CTA BAND ════════════════════════════════════════════════════
   One component, every page. Before this each page carried its own inline
   closing panel — a centred card on index/standards/products, a pair of loose
   button rows on coa-library, a bare link on product — so "the CTA" was six
   different objects. This is the single one.

   Colour is the BRAND FILL PAIR, not a picked pair and not a baked hex:
   --sf-brand is the client's own brand colour and --sf-brand-text is the ink
   themes/tokens.mjs already solved against it — theme.css says so in as many
   words ("a background carrying --sf-brand-text"). So the band arrives deep and
   brand-coloured on every client, and legible on every client, without this
   file knowing a single hex.

   NOT the chrome triple, which is the trap here: --sf-chrome-bg is the PAGE
   ground (tokens.mjs: `const chromeBg = bg`), so on a light client it is
   near-white — core-chain-labs solves it to #F0F7FA. A band painted from it
   would be a white card, not the brand band. .sf-assure-band DOES want the
   chrome ground and keeps it; this one wants the fill.

   The quiet ink is mixed toward the BAND'S OWN GROUND, never toward
   `transparent`: an alpha ink composites whatever scrolls under it, so the
   ratio a reader gets is not the ratio anything measured. Same shape
   tokens.mjs uses for chromeInkDim. */
.sf-ctaband, .sf-ctaband-card {
  --sf-ctaband-bg: var(--sf-brand, var(--sf-accent));
  --sf-ctaband-ink: var(--sf-brand-text, var(--sf-accent-text));
  /* SOLVED per client by themes/options.mjs designVars() and emitted into
     theme.css as --sf-ctaband-dim-solved. The color-mix below is the FALLBACK
     for an unbuilt template only — it is a fixed 80% fade, and a fixed fade is
     what qa.mjs measured at 3.77:1 on the badge value of a real client while
     the sub-line wearing the same token measured 4.74:1. The band is three
     composited layers, not one colour; the solve reasons about all of them.
     It has to be a separate token because this rule is a CLASS selector and
     sf-base.css loads after theme.css, so a :root override would lose. */
  --sf-ctaband-ink-dim: var(--sf-ctaband-dim-solved, color-mix(in srgb, var(--sf-brand-text, var(--sf-accent-text)) 80%, var(--sf-brand, var(--sf-accent))));
}
.sf-ctaband { padding: calc(4.5rem * var(--sf-section-pad-scale, 1)) 2rem; position: relative; }
.sf-ctaband-card {
  position: relative; isolation: isolate;
  /* clip, NOT hidden. The rings overhang the card, so this box has to clip —
     but an overflow:hidden box is still a SCROLL CONTAINER, and focus or an
     anchor jump scrolls its content permanently out of view with no scrollbar
     to bring it back. qa.mjs measured this card scrolled 70px. `clip` clips
     without ever becoming scrollable; `hidden` stays as the older fallback. */
  overflow: hidden; overflow: clip;
  max-width: 1280px; margin: 0 auto;
  border-radius: 20px;
  padding: 3.25rem 3.25rem;
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 2.5rem; align-items: center;
  color: var(--sf-ctaband-ink);
  background:
    linear-gradient(103deg, color-mix(in srgb, var(--sf-accent) 22%, transparent) 0%, transparent 64%),
    var(--sf-ctaband-bg);
  box-shadow: 0 18px 56px rgb(var(--sf-shadow) / 0.18), 0 2px 8px rgb(var(--sf-shadow) / 0.08);
}
/* Concentric rings, right side. Drawn from the band's OWN ink so they can never
   be a colour this brand did not choose, and kept under 14% so they read as
   texture rather than as a second element competing with the headline. */
.sf-ctaband-rings {
  position: absolute; right: -4%; top: 50%; width: 34rem; height: 34rem;
  transform: translateY(-50%); pointer-events: none; z-index: 0;
  background:
    radial-gradient(circle at 50% 50%,
      color-mix(in srgb, var(--sf-ctaband-ink) 7%, transparent) 0 33%,
      transparent 33% 46%,
      color-mix(in srgb, var(--sf-ctaband-ink) 13%, transparent) 46% 46.35%,
      transparent 46.35% 71%,
      color-mix(in srgb, var(--sf-ctaband-ink) 10%, transparent) 71% 71.3%,
      transparent 71.3%);
}
.sf-ctaband-text { position: relative; z-index: 1; min-width: 0; }
.sf-ctaband-kicker {
  font-size: 0.68rem; font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.17em; line-height: 1.4; margin: 0 0 0.85rem;
  color: var(--sf-ctaband-ink-dim);
}
.sf-ctaband-title {
  font-family: var(--sf-font-display, inherit);
  font-size: clamp(1.75rem, 3.4vw, 2.75rem);
  font-weight: var(--sf-h-weight, 800);
  letter-spacing: -0.035em; line-height: 1.08;
  margin: 0 0 1.05rem; color: var(--sf-ctaband-ink);
  max-width: 22ch; text-wrap: balance;
}
.sf-ctaband-sub {
  font-size: 0.95rem; line-height: 1.7; margin: 0;
  color: var(--sf-ctaband-ink-dim); max-width: 56ch;
}
.sf-ctaband-actions {
  position: relative; z-index: 1;
  display: flex; gap: 0.7rem; flex-wrap: wrap; align-items: center;
}
.sf-ctaband-btn {
  display: inline-flex; align-items: center; gap: 0.5rem;
  border-radius: 999px; white-space: nowrap; text-decoration: none;
  font-family: inherit; font-size: 0.875rem; font-weight: 700;
  letter-spacing: var(--sf-btn-track, 0.01em);
  padding: 0.85rem 1.7rem; cursor: pointer;
  border: 1px solid transparent;
  transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease, opacity 0.18s ease;
}
/* The primary is the band's ink INVERTED — ink on fill becomes fill on ink — so
   its contrast is the same solved number read the other way round, on every
   palette, with no second solve to keep in step. */
.sf-ctaband-btn-primary { background: var(--sf-ctaband-ink); color: var(--sf-ctaband-bg); }
.sf-ctaband-btn-primary:hover { transform: translateY(-1px); opacity: 0.92; }
.sf-ctaband-btn-ghost {
  background: transparent; color: var(--sf-ctaband-ink);
  border-color: color-mix(in srgb, var(--sf-ctaband-ink) 42%, transparent);
}
.sf-ctaband-btn-ghost:hover {
  transform: translateY(-1px);
  border-color: var(--sf-ctaband-ink);
  background: color-mix(in srgb, var(--sf-ctaband-ink) 10%, transparent);
}
.sf-ctaband-btn:active { transform: translateY(0); }
.sf-ctaband-btn:focus-visible { outline: 2px solid var(--sf-ctaband-ink); outline-offset: 3px; }
.sf-ctaband-arrow { font-size: 1em; line-height: 1; transition: transform 0.18s ease; }
.sf-ctaband-btn:hover .sf-ctaband-arrow { transform: translateX(3px); }

@media (max-width: 62rem) {
  .sf-ctaband-card { grid-template-columns: 1fr; gap: 2rem; padding: 2.75rem 2.25rem; }
  .sf-ctaband-title { max-width: none; }
  .sf-ctaband-rings { right: -30%; opacity: 0.7; }
}
@media (max-width: 40rem) {
  .sf-ctaband { padding: calc(3rem * var(--sf-section-pad-scale, 1)) 1.1rem; }
  .sf-ctaband-card { padding: 2.25rem 1.5rem; border-radius: 16px; }
  .sf-ctaband-actions { width: 100%; }
  .sf-ctaband-btn { flex: 1 1 100%; justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
  .sf-ctaband-btn, .sf-ctaband-arrow { transition: none; }
  .sf-ctaband-btn:hover { transform: none; }
  .sf-ctaband-btn:hover .sf-ctaband-arrow { transform: none; }
}

/* ── the same CTA shape, on a LIGHT card ─────────────────────────────────────
   Not every CTA sits on the brand band: the COA empty-state, the product page's
   catalogue link and the about panel are cards on the page ground. They take
   the band's SHAPE (pill, arrow, weight, size) so the site has one button
   language, and their own SOLVED pair (--sf-accent / --sf-accent-text) so the
   contrast is the one the theme already measured for that ground. */
.sf-ctaband-btn-solid { background: var(--sf-accent); color: var(--sf-accent-text); }
.sf-ctaband-btn-solid:hover { background: var(--sf-accent-4, var(--sf-accent)); transform: translateY(-1px); }
.sf-ctaband-btn-outline {
  background: transparent; color: var(--sf-text);
  border-color: color-mix(in srgb, var(--sf-text) 24%, transparent);
}
.sf-ctaband-btn-outline:hover {
  transform: translateY(-1px);
  border-color: color-mix(in srgb, var(--sf-text) 48%, transparent);
  background: color-mix(in srgb, var(--sf-text) 5%, transparent);
}
.sf-ctaband-btn-solid:focus-visible,
.sf-ctaband-btn-outline:focus-visible { outline: 2px solid var(--sf-accent); outline-offset: 3px; }

/* The page's own primary/secondary CTA pair takes the pill too, so the hero,
   the cards and the band are one shape rather than three. Radius only — the
   colours stay whatever each page solved. .btn-navy is left alone on purpose:
   it is a full-width card footer, not a CTA. */
body .btn-teal,
body .btn-ghost-white,
body .btn-primary,
body .btn-ghost { border-radius: 999px; }

/* ── DOCUMENT PAGES (404 + re-shelled legal) ──────────────────────────────
   PROPOSED ADDITION TO engine/template/sf-base.css (2026-08-20).

   factory/build.mjs dressDocPage() replaces the page's <main> with
   docSection()/legalMainHtml() markup and then STRIPS every page-local <style>
   in the document. That markup is classed `sheet doc-sheet` / `wrap wrap-tight`
   / `doc-head` / `tl` / `doc-lede` / `doc-updated` / `doc-actions`, and NOTHING
   in engine/template/ defines any of them — the vocabulary was borrowed from a
   hand-authored client stylesheet (factory/clients/catharos/frontend/
   catharos.css). A template-first client therefore ships a 404 whose <main> is
   full-bleed at y=0 under the fixed nav, with its <h1> at body size.

   The body rule is here for the same reason: every storefront page sets its own
   ground, ink and body face in a page-local <style>, and the dressed page is
   the one page that loses it. Page-local rules still win — this file is linked
   before them on every page. */
/* SCOPED to the dressed pages on purpose. dressDocPage() strips the page-local
   <style> that every storefront page sets its ground, ink and body face in, so
   THESE pages need one from a linked sheet. A bare `body` rule here would reach
   every page of every client to fix the handful that lost theirs. */
body:has(.doc-sheet) {
  background: var(--sf-bg);
  color: var(--sf-text);
  font-family: var(--sf-font-body);
}

.doc-sheet {
  padding: calc(var(--sf-nav-h, 64px) + 3rem) 2rem 6rem;
  background: var(--sf-bg);
}
.doc-sheet .wrap { max-width: var(--sf-nav-max, 1160px); margin: 0 auto; }
.doc-sheet .wrap-tight { max-width: 720px; }
.doc-head { margin: 0 0 2rem; }
.doc-head .tl {
  font-family: var(--sf-font-display);
  font-size: clamp(2rem, 4.6vw, 3rem);
  font-weight: var(--sf-h-weight, 800);
  letter-spacing: var(--sf-h-track, -0.03em);
  line-height: 1.06;
  margin: 0 0 0.75rem;
  color: var(--sf-text);
}
.doc-lede { font-size: 1.05rem; line-height: 1.7; margin: 0; }
.doc-updated { font-size: 0.85rem; margin: 0; }
.doc-sheet .muted { color: var(--sf-muted); }
.doc-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 2rem 0 0; }
.doc-sheet .prose { max-width: 68ch; line-height: 1.75; }
.doc-sheet .prose h2 { font-family: var(--sf-font-display); margin: 2.5rem 0 0.75rem; }
.doc-sheet .prose h3 { font-family: var(--sf-font-display); margin: 1.75rem 0 0.5rem; }
.doc-sheet .prose p, .doc-sheet .prose li { color: var(--sf-muted); }
/* docSection() emits class="btn btn-glass" on the secondary action. sf-base defines .btn-primary and
   nothing called .btn or .btn-glass, so the secondary action had no geometry at
   all. Give both the same shape as .btn-primary. */
.doc-actions .btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0.85rem 1.7rem; border-radius: 999px; text-decoration: none;
  font-family: inherit; font-size: 0.875rem; font-weight: 700;
  letter-spacing: var(--sf-btn-track, 0.02em); cursor: pointer;
  border: 1px solid transparent;
}
.btn-glass {
  background: color-mix(in srgb, var(--sf-text) 8%, transparent);
  color: var(--sf-text);
  border: 1px solid var(--sf-border-color, color-mix(in srgb, var(--sf-text) 18%, transparent));
}
@media (max-width: 640px) {
  .doc-sheet { padding: calc(var(--sf-nav-h, 64px) + 2rem) 1.25rem 4rem; }
}
