/* base.css — shared design "bones" every site gets (structure + primitives).
 *
 * Tokens come from the site's Theme settings, emitted as CSS variables in <head>
 * by render_head(): --container, --space, --radius, --shadow, --font-size,
 * --font-scale, plus the brand vars --color-* and --font-*. The :root block below
 * repeats them as fallbacks so this works even if the inline tokens are missing.
 *
 * Because everything READS the tokens, two sites with different Theme settings
 * (or different starter "looks") render the same structure with a genuinely
 * different feel. Put SITE/LOOK-specific flavor in style.css — it loads after
 * this and overrides. Don't rewrite base.css.
 */

/* ═══════════════════════════════════════════════════════════════════════════
 * EDITING THIS FILE (to change ALL FUTURE generations)
 *
 * ── VERIFY EVERY EDIT ────────────────────────────────────────────────────
 * After ANY change here, screenshot the regression harness and eyeball it.
 * Run this in the TERMINAL (copy/paste the whole line) — it's a shell script,
 * NOT something you open in a browser. It opens the resulting PNGs when done:
 *
 *     cd "/Users/tylerwillis/Documents/Digital/Website-Development/SFTP/clayjar-websites/site/platform/patterns" && ./shoot-base-regression.sh --open
 *
 * (If you get "permission denied", use: bash shoot-base-regression.sh --open)
 *
 * It links THIS file directly and screenshots every primitive + the ⚑ REGRESSION
 * GUARDS (the bugs that came back before) at desktop + mobile widths. Scan the
 * guards first. Fixing a NEW class of bug? Add a fixture for it to
 * patterns/base-regression.html BEFORE you finish, so it can't regress — if the
 * bug only shows under a per-site skin (e.g. a skin flips a grid to auto-fit and
 * orphans a row), have the fixture emulate that skin inline so the guard proves
 * base.css neutralises it. (The shoot script caps the capture at 4000px tall —
 * raise --window-size if a new fixture sits below that.)
 *
 * Faster loop for ONE component — open the live page in a browser instead:
 *     open "/Users/tylerwillis/Documents/Digital/Website-Development/SFTP/clayjar-websites/site/platform/patterns/base-regression.html"
 * ─────────────────────────────────────────────────────────────────────────
 *
 * PROPAGATION — this file is a per-site SNAPSHOT, copied into each site at
 * provision time (sites.php → recurse_copy), so:
 *   • NEW sites pick up your edit automatically (copied before the AI runs;
 *     the AI's style.css only layers flavor ON TOP of this). Nothing else to do.
 *   • EXISTING sites keep the base.css they were born with — they do NOT
 *     auto-update. To backfill, copy ONLY this file (and includes/media.php if
 *     touched) into each site/s/<slug>/…. Do NOT use push_template for this: it
 *     also overwrites the site's generated style.css + pages with blank starters.
 *
 * This file is the SOURCE copied into every generated site, so a fix here lands
 * on every future build. (The copies under site/s/<slug>/… are per-site — editing
 * those only touches one site.) Decide where a fix belongs:
 *   • It LOOKS wrong — spacing, centering, color, a component's look, contrast
 *       on a colored band ........................ HERE (base.css).
 *   • A behavior is broken — slider, reveal, scroll effect, tabs, carousel
 *       ....................................... assets/js/main.js
 *   • The AI keeps CHOOSING wrong — thin copy, bad section order/mix, wrong
 *       component, mixed alignment .............. includes/generate.php (prompt text)
 *   • A rendered component's HTML is off — blog card, testimonial, contact panel,
 *       collections ............................ template/includes/content.php
 *
 * MAP — search (⌘F) the quoted headers below to jump to a region:
 *   :root ......................... design TOKENS: --space, --radius, --shadow,
 *                                   --surface/-2, --border, colors, type scale.
 *   "Site chrome" ................. header, nav + dropdown caret, mobile hamburger, footer
 *   "Layout primitives" ........... .container, section vertical rhythm, .grid
 *   "Components" .................. buttons, cards, card-as-link, .scrim (text on photo)
 *   "SECTION PRIMITIVES" .......... .display/.lede, .hero, section width/rhythm modifiers
 *   "COMPOSITION ARCHETYPES" ...... .split, .feature-rows (zig-zag), .offset, editorial grid
 *   feature cards · stats · steps · pricing · testimonials · logos · CTA · FAQ
 *   "MOTION & POLISH LAYER" ....... scroll-progress bar, custom scrollbar, reveal-on-scroll, parallax
 *   "2026 reference components" ... .chip-kicker, .aura glow, .device, .media-stat, .hl two-tone
 *   "Business components" ......... .team, .checklist, .contact-panel, .embed, .announcement
 *   "RICH SECTION BLOCKS" ......... .carousel--slides + dots, .accolades, .metric-split,
 *                                   .spotlight, .resource-list, .listing, .callout-band,
 *                                   .logo-grid, .comparison matrix cells, .price-toggle,
 *                                   .compare-cols (two-panel), .signup-form, .price-solo,
 *                                   .testimonial-wall (masonry), .meters (progress bars)
 *   "HARDENING LAYER" ............. dark-surface / overflow / mobile safety nets
 *   "OPTIONS SYSTEM" + "AXIS A".."AXIS H" — the data-options="…" modifier vocabulary:
 *       A Surface(background-*) · B Alignment(text-center) · C Density(tight/roomy/flush)
 *       D Width(narrow/wide/measure) · E Motifs · F Shape · G Type · H Media grade
 *
 * QUICK HITS:
 *   • Centering / alignment issues → "AXIS B — ALIGNMENT" and ".section-head".
 *   • A pill/label/button is invisible on a colored band → the on-band rules in
 *     "Components" and near ".cta"/".chip-kicker" (search "on ANY colored").
 *   • TEST without a full generation: put the markup in a static .html that
 *     <link>s this file and open it in a browser — iterate in seconds.
 * ═══════════════════════════════════════════════════════════════════════════ */

:root {
    --container: 1080px;
    --space: 1rem;
    --radius: 8px;
    --shadow: 0 1px 3px rgba(0, 0, 0, .12);
    --font-size: 17px;
    --font-scale: 1.25;

    /* Depth vocabulary — derived from the brand tokens so it re-skins with them
       and needs NO extra <head> injection (var() resolves lazily at use-time).
       This is what separates a "flat framework default" from a designed surface:
       elevated surfaces, a real hairline border, and LAYERED shadows (a tight
       contact shadow + a soft ambient one) instead of a single flat drop. A
       site's style.css can override any of these; the section primitives below
       read them so depth is consistent everywhere.
         --surface / --surface-2  slightly raised panels on the page background
         --border                 hairline that tracks the text color, not black
         --shadow-sm/-lg          smaller / larger companions to --shadow
         --radius-lg              a larger corner (scales with --radius, so a
                                  radius:0 brand stays sharp everywhere) */
    --surface:   color-mix(in srgb, var(--color-text, #111) 3%, var(--color-background, #fff));
    --surface-2: color-mix(in srgb, var(--color-text, #111) 6%, var(--color-background, #fff));
    --border:    color-mix(in srgb, var(--color-text, #111) 12%, transparent);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .06);
    --shadow-lg: 0 18px 48px -20px rgba(0, 0, 0, .28), 0 4px 12px -6px rgba(0, 0, 0, .10);
    --radius-lg: calc(var(--radius, 8px) * 2);

    /* Vertical rhythm between flowing text (headings/paragraphs). Deliberately
       generous — tight stacking is what reads as "elementary". A site's style.css
       can retune it. */
    --flow: calc(var(--space, 1rem) * 1.2);
}

/* --- Reset -------------------------------------------------------------- *
 * A modern, minimal normalize so browser defaults don't bleed into a site's
 * look. This is the shared baseline every site inherits — style.css should add
 * flavor on top, never restate a reset. Keep it token-aware where it matters
 * (headings/type live below); everything here is layout-neutral. */

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

/* Strip default margins — spacing is composed with tokens (--space), not
   inherited from the UA stylesheet. */
* { margin: 0; }

html {
    font-size: var(--font-size);
    -webkit-text-size-adjust: 100%;  /* stop iOS bumping type on rotate */
    text-size-adjust: 100%;
    scroll-behavior: smooth;
    tab-size: 4;
}

body {
    min-height: 100vh;
    font-family: var(--font-body, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
    color: var(--color-text, #1a1a1a);
    background: var(--color-background, #fff);
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

/* Media: block-level and never overflow their container. */
img, picture, video, canvas, svg {
    display: block;
    max-width: 100%;
    height: auto;
}

/* Form controls inherit typography instead of the UA's tiny system font. */
input, button, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; }

/* Long words / URLs shouldn't force horizontal scroll. */
p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }

/* Lists lose their bullets/padding by default; add them back per component
   (e.g. inside .prose long-form content) rather than fighting the UA. */
ul, ol { list-style: none; padding: 0; }

table { border-collapse: collapse; border-spacing: 0; }
hr { border: none; border-top: 1px solid rgba(0, 0, 0, .12); }

/* Honor users who ask for less motion (see the smooth scroll above + JS). */
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
}

/* Modular type scale: each heading steps up by --font-scale from body text.
   1rem == the base size (set on <html>), so these track the Theme type tokens. */
h1, h2, h3, h4 { font-family: var(--font-heading, inherit); line-height: 1.2; }
h1 { font-size: calc(1rem * var(--font-scale) * var(--font-scale) * var(--font-scale)); line-height: 1.1; }
h2 { font-size: calc(1rem * var(--font-scale) * var(--font-scale)); }
h3 { font-size: calc(1rem * var(--font-scale)); }

a { color: var(--color-primary, #0066cc); }

code { background: #f4f4f5; padding: 0.1em 0.35em; border-radius: 4px; }

/* --- Site chrome (header / footer) -------------------------------------- */

.site-header {
    /* Sticky + a solid background + a stacking context ABOVE page sections so a
       hero photo / colored band can never paint over the nav (the .is-scrolled
       shadow below finally has something to stick to). z-index sits under modals
       (1000) and the mobile nav sheet. A site's style.css can make it transparent
       over the hero or drop the stickiness — this is the safe default. */
    position: sticky;
    top: 0;
    z-index: 90;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space);
    padding: var(--space) calc(var(--space) * 1.5);
    background: var(--color-background, #fff);
    border-bottom: 1px solid var(--border, rgba(0, 0, 0, .08));
}
.brand { font-weight: 700; text-decoration: none; color: var(--color-text, inherit); flex-shrink: 0; }
.brand-logo { display: block; height: 50px; width: 200px; object-fit: contain; object-position: left; border-radius: 0; }

/* Primary nav. Desktop: a horizontal row with hover/focus dropdowns. The
   hamburger (.nav-toggle) is hidden until the mobile breakpoint below. */
.nav-menu {
    display: flex;
    align-items: center;
    gap: calc(var(--space) * 1.25);
    margin: 0;
    padding: 0;
    list-style: none;
}
.nav-item { position: relative; }
.nav-link {
    display: inline-block;
    text-decoration: none;
    color: var(--color-text, inherit);
    padding: 0.35em 0;
}
.nav-item:hover > .nav-link,
.nav-link:focus { color: var(--color-primary, #0066cc); }

/* Current-page state. The header adds .is-active to the link for the page being
   viewed (and .has-current to a dropdown parent whose child is current). Color is
   the universal indicator; if a site draws its own hover underline (a scaled
   ::after in style.css), the active link keeps that underline shown. Ignores
   #anchor and external links — see nav_is_current(). */
.nav-link.is-active,
.nav-item.has-current > .nav-link { color: var(--color-primary, #0066cc); }
.nav-menu > .nav-item > .nav-link.is-active::after { transform: scaleX(1) !important; }

/* A dropdown parent shows a caret; the submenu opens on hover/focus (desktop).
   Defensive: a per-site style.css occasionally re-implements the nav and hides this
   button (deleting the caret). Force it visible so a dropdown always shows its caret. */
.nav-item.has-dropdown > .nav-sub-toggle { display: inline-flex !important; }
.nav-sub-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.4em;
    height: 1.4em;
    margin-left: 0.15em;
    margin-bottom: 4px;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    cursor: pointer;
    vertical-align: middle;
}
.nav-sub-toggle::before {
    content: "";
    width: 0.45em;
    height: 0.45em;
    /* Nudge up: a chevron drawn from two borders is bottom-heavy, so its optical
       center sits below the geometric one — this re-centers it against the label. */
    margin-top: -0.12em;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg);
    transition: transform .2s ease;
}
/* Desktop: flip the caret up while its dropdown is open (hover/focus) — the
   animation that was previously only wired for the mobile aria-expanded toggle. */
.nav-item.has-dropdown:hover > .nav-sub-toggle::before,
.nav-item.has-dropdown:focus-within > .nav-sub-toggle::before {
    margin-top: 0.12em;
    transform: rotate(-135deg);
}
.nav-dropdown {
    position: absolute;
    left: 0;
    top: 100%;
    min-width: 12rem;
    margin: 0;
    padding: calc(var(--space) * 0.5);
    list-style: none;
    background: var(--color-background, #fff);
    border: 1px solid rgba(0, 0, 0, .1);
    border-radius: var(--radius, 8px);
    box-shadow: var(--shadow, 0 8px 24px rgba(0, 0, 0, .12));
    opacity: 0;
    visibility: hidden;
    transform: translateY(4px);
    transition: opacity .15s ease, transform .15s ease, visibility .15s;
    z-index: 50;
}
.nav-dropdown .nav-link { display: block; padding: 0.4em 0.5em; border-radius: calc(var(--radius, 8px) * 0.6); }
.nav-item.has-dropdown:hover > .nav-dropdown,
.nav-item.has-dropdown:focus-within > .nav-dropdown {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

/* Hamburger — hidden on desktop, shown at the mobile breakpoint. */
.nav-toggle {
    display: none;
    flex-direction: column;
    justify-content: center;
    gap: 5px;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
}
.nav-toggle-bar {
    display: block;
    width: 1.4rem;
    height: 2px;
    margin-inline: auto;
    background: var(--color-text, currentColor);
    transition: transform .2s ease, opacity .2s ease;
}

@media (max-width: 768px) {
    .nav-toggle { display: flex; }
    .site-nav {
        position: absolute;
        left: 0;
        right: 0;
        top: 100%;
        display: none;
        padding: var(--space) calc(var(--space) * 1.5);
        background: var(--color-background, #fff);
        border-bottom: 1px solid rgba(0, 0, 0, .08);
        box-shadow: var(--shadow, 0 8px 24px rgba(0, 0, 0, .12));
        z-index: 40;
    }
    .site-header { position: relative; flex-wrap: wrap; }
    .site-nav.is-open { display: block; }
    .nav-menu { flex-direction: column; align-items: stretch; gap: 0; }
    .nav-item { border-bottom: 1px solid rgba(0, 0, 0, .06); }
    .nav-item .nav-link { padding: 0.75em 0; }

    /* On mobile the dropdown is an inline accordion toggled by aria-expanded. */
    .has-dropdown { display: grid; grid-template-columns: 1fr auto; align-items: center; }
    .has-dropdown > .nav-dropdown { grid-column: 1 / -1; }
    .nav-dropdown {
        position: static;
        display: none;
        min-width: 0;
        padding: 0 0 0 var(--space);
        border: 0;
        border-radius: 0;
        box-shadow: none;
        opacity: 1;
        visibility: visible;
        transform: none;
    }
    .nav-sub-toggle[aria-expanded="true"] + .nav-dropdown { display: block; }
    .nav-sub-toggle[aria-expanded="true"]::before { transform: rotate(-135deg); }
    .nav-sub-toggle { width: 2.5rem; height: 2.5rem; }
}

/* The same sheet whenever the hamburger is visible, at ANY width. base.js sets
   data-nav-collapsed while the toggle is displayed, so a site whose style.css
   shows the hamburger above 768px still gets a menu that opens. :where() keeps
   these at the media-query rules' specificity, so a site's own sheet styling
   (loaded after base.css) still wins. */
:where(.site-header[data-nav-collapsed]) .site-nav {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    display: none;
    padding: var(--space) calc(var(--space) * 1.5);
    background: var(--color-background, #fff);
    border-bottom: 1px solid rgba(0, 0, 0, .08);
    box-shadow: var(--shadow, 0 8px 24px rgba(0, 0, 0, .12));
    z-index: 40;
}
:where(.site-header[data-nav-collapsed]) .site-nav.is-open { display: block; }
:where(.site-header[data-nav-collapsed]) .nav-menu { flex-direction: column; align-items: stretch; gap: 0; }
:where(.site-header[data-nav-collapsed]) .nav-item { border-bottom: 1px solid rgba(0, 0, 0, .06); }
:where(.site-header[data-nav-collapsed]) .nav-item .nav-link { padding: 0.75em 0; }
:where(.site-header[data-nav-collapsed]) .has-dropdown { display: grid; grid-template-columns: 1fr auto; align-items: center; }
:where(.site-header[data-nav-collapsed]) .has-dropdown > .nav-dropdown { grid-column: 1 / -1; }
:where(.site-header[data-nav-collapsed]) .nav-dropdown {
    position: static;
    display: none;
    min-width: 0;
    padding: 0 0 0 var(--space);
    border: 0;
    border-radius: 0;
    box-shadow: none;
    opacity: 1;
    visibility: visible;
    transform: none;
}
:where(.site-header[data-nav-collapsed]) .nav-sub-toggle[aria-expanded="true"] + .nav-dropdown { display: block; }
:where(.site-header[data-nav-collapsed]) .nav-sub-toggle[aria-expanded="true"]::before { transform: rotate(-135deg); }
:where(.site-header[data-nav-collapsed]) .nav-sub-toggle { width: 2.5rem; height: 2.5rem; }

/* HEADER LAYOUTS — config 'header_layout' puts data-layout on .site-header (see
   components/header.php). Every rule is scoped to [data-layout], so a header without
   it (the standard row, and every site built before this) renders exactly as before.
   action/status: brand · nav · primary action button · search.  centered: brand
   over a centered nav.  minimal: no search icon (markup omits it). */
.site-header[data-layout] .site-nav { margin-left: auto; }
.header-action { display: inline-flex; align-items: center; gap: .45em; flex: none; white-space: nowrap; }
.header-action svg { width: 1.05em; height: 1.05em; flex: none; }
.site-header[data-layout="centered"] { flex-wrap: wrap; justify-content: center; row-gap: calc(var(--space) * .5); }
.site-header[data-layout="centered"] .brand { flex-basis: 100%; display: flex; justify-content: center; }
.site-header[data-layout="centered"] .brand-logo { object-position: center; }
.site-header[data-layout="centered"] .site-nav { margin-left: 0; }
.site-header[data-layout="centered"] .nav-search { position: absolute; right: calc(var(--space) * 1.5); top: var(--space); }
@media (max-width: 768px) {
    /* brand · action · hamburger on ONE row (the menu sheet is absolutely positioned
       below, so nothing needs to wrap); a long name shrinks instead of pushing the
       hamburger onto a second line. */
    .site-header[data-layout] { flex-wrap: nowrap; gap: .6rem; padding-inline: var(--space); }
    .site-header[data-layout] .brand { margin-right: auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .site-header[data-layout] .brand-logo { max-width: 100%; }
    .site-header[data-layout] .header-action { order: 2; padding: .5em .85em; font-size: .9rem; }
    .site-header[data-layout] .nav-search { order: 3; }
    .site-header[data-layout] .nav-toggle { order: 4; }
    .site-header[data-layout] .site-nav { order: 5; margin-left: 0; }
    .site-header[data-layout="centered"] { justify-content: space-between; }
    .site-header[data-layout="centered"] .brand { flex-basis: auto; justify-content: flex-start; }
    .site-header[data-layout="centered"] .brand-logo { object-position: left; }
    .site-header[data-layout="centered"] .nav-search { position: static; }
}
@media (max-width: 420px) {
    /* A long action label won't fit beside the brand on a small phone — the page
       carries the same action. A phone number (status) is short and stays. */
    .site-header[data-layout="action"] .header-action { display: none; }
    .site-header[data-layout="status"] .nav-search { display: none; }
}

/* FOOTER LAYOUTS — config 'footer_layout' puts data-layout on .site-footer.
   minimal: one compact tier.  statement: the business name set huge across the
   bottom (.footer-wordmark), sized to fit its own length on any width. */
.site-footer[data-layout="minimal"] { padding-block: calc(var(--space) * 1.5); }
.site-footer[data-layout="minimal"] .site-footer-inner { align-items: center; gap: var(--space) calc(var(--space) * 2); }
.site-footer[data-layout="minimal"] .footer-bottom { margin-top: var(--space); padding-top: var(--space); }
.footer-wordmark {
    max-width: var(--container);
    margin: calc(var(--space) * 1.5) auto 0;
    padding-inline: calc(var(--space) * 1.5);
    font-family: var(--font-heading, inherit);
    font-weight: 700;
    line-height: .82;
    letter-spacing: -.035em;
    white-space: nowrap;
    overflow: hidden;
    color: var(--color-text, inherit);
    /* fit the name's own length: ~.58em per character across the content width */
    font-size: clamp(2.25rem, calc((min(100vw, var(--container, 1200px)) - 3rem) / (var(--wm-len, 12) * .58)), 12rem);
}
/* base.js measures this span and shrinks the wordmark until it fits. */
.footer-wordmark-text { display: inline-block; }

/* The footer ELEMENT spans the full viewport width so a background color/border
   set on it (by a site's style.css) bleeds edge to edge — the content inside is
   held to the container width by .site-footer-inner and stays aligned with the
   page's main column. (Older sites put content directly in .site-footer; the flex
   fallback below keeps them laid out even without the inner wrapper.) */
.site-footer {
    padding-block: calc(var(--space) * 2.5);
    color: var(--color-muted, #888);
    border-top: 1px solid var(--border, rgba(0, 0, 0, .08));
}
/* Top tier: the brand/logo block + a nav sitemap (rendered from the Header nav).
   Back-compat: an old footer that put copyright + legal STRAIGHT into
   .site-footer-inner still lays out as a sensible row. */
.site-footer-inner,
.site-footer:not(:has(.site-footer-inner)) {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: calc(var(--space) * 2);
    max-width: var(--container);
    margin-inline: auto;
    padding-inline: calc(var(--space) * 1.5);
}
.footer-brand { display: grid; gap: .7rem; max-width: 24rem; }
.footer-brand .brand-logo { height: 50px; width: 200px; object-fit: contain; object-position: left; }
.footer-tagline { color: var(--color-muted, #888); font-size: .95rem; line-height: 1.55; }
.footer-contact { display: grid; gap: .3rem; font-size: .92rem; }
.footer-contact a { color: var(--color-muted, #888); text-decoration: none; }
.footer-contact a:hover { color: var(--color-primary, #0066cc); }
.footer-contact span { color: var(--color-muted, #888); }
.footer-social { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: .3rem; }
.footer-social a { color: var(--color-muted, #888); text-decoration: none; font-size: .9rem; font-weight: 600; }
.footer-social a:hover { color: var(--color-primary, #0066cc); }
.footer-nav { display: flex; flex-wrap: wrap; gap: calc(var(--space) * 2.5); }
.footer-col { display: grid; gap: .55rem; align-content: start; }
/* Optional persistent newsletter signup (config 'footer_newsletter'). A footer
   column: heading + blurb + the inline .signup-form primitive. */
.footer-signup { display: grid; gap: .5rem; align-content: start; max-width: 22rem; }
/* When a footer HAS a newsletter, pair it with the brand on the top row (brand left,
   signup right via the inner's space-between) and drop the sitemap to a full-width
   row below — so the signup fills the empty top-right instead of wrapping to the far
   left. Only applies when .footer-signup exists; footers without one are untouched. */
.site-footer-inner:has(.footer-signup) .footer-brand  { order: 1; }
.site-footer-inner:has(.footer-signup) .footer-signup { order: 2; }
.site-footer-inner:has(.footer-signup) .footer-nav    { order: 3; flex-basis: 100%; }
.footer-signup .footer-col-title { margin: 0; }
.footer-signup-blurb { color: var(--color-muted, #888); font-size: .9rem; line-height: 1.5; margin: 0; }
/* Input + a compact ICON submit sitting inline to its right — not a full-width bar.
   The field grows to fill; the button is a fixed square. nowrap keeps them on one row
   (the field can shrink because min-width:0), so on any footer width they read as one
   control pair. A .footer-signup-btn with no icon still shows its text label. */
/* display:flex is explicit (not inherited from .signup-form) so a per-site skin that
   sets `.signup-form { display: grid }` can't stack the field + button — this
   higher-specificity rule keeps the footer signup a single inline row regardless. */
.footer-signup .signup-form { display: flex; margin-top: .35rem; gap: .5rem; flex-wrap: nowrap; align-items: end; }
.footer-signup .signup-form .field { flex: 1 1 auto; min-width: 0; }
.footer-signup .signup-form .footer-signup-btn { flex: 0 0 auto; width: 2.6rem; padding: 0; justify-content: center; }
.footer-signup .signup-form .footer-signup-btn .icon { width: 1.2rem; height: 1.2rem; }
/* The input and the button must read as ONE control pair: same compact height,
   same footprint. And a white field on a dark footer looks pasted-on — derive the
   field from the footer's OWN foreground (currentColor) so it adapts to a light OR
   dark footer automatically instead of punching a bright white box into the band. */
.footer-signup .signup-form :is(input, .btn, button) {
    height: 2.6rem;
    padding: 0 .8rem;
    font-size: .9rem;
    line-height: 1;
}
.footer-signup .signup-form input {
    width: 100%;
    /* !important to beat the high-specificity global `input:not(…)…` baseline (six
       attribute :not()s) — without it a solid white field punches through a dark
       footer. currentColor = the footer's own text, so this adapts to any footer. */
    background: color-mix(in srgb, currentColor 8%, transparent) !important;
    border: 1px solid color-mix(in srgb, currentColor 28%, transparent) !important;
    color: inherit !important;
}
.footer-signup .signup-form input::placeholder { color: color-mix(in srgb, currentColor 55%, transparent); }
.footer-signup .signup-form .btn { flex: 0 0 auto; display: inline-flex; align-items: center; }
.footer-col-title {
    font-family: var(--font-heading, inherit);
    font-weight: 700;
    font-size: .82rem;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--color-text, #1a1a1a);
    text-decoration: none;
}
.footer-col a { color: var(--color-muted, #888); text-decoration: none; font-size: .95rem; }
.footer-col a:hover,
.footer-col-title:hover { color: var(--color-primary, #0066cc); }

/* A footer column with sub-links reads as a titled column (the header treatment
   above is right). But when a column is JUST its own link — no children — the bold
   uppercase title looks like a header for nothing. Detect the no-children case
   (only one anchor in the column) and render that title as a plain inline link. */
.footer-col:not(:has(> a + a)) .footer-col-title {
    font-family: inherit;
    font-weight: 500;
    font-size: .95rem;
    letter-spacing: 0;
    text-transform: none;
    color: var(--color-muted, #888);
}
/* When the WHOLE menu is a flat list (no column has sub-links), it's a link row, not
   a set of columns — tighten the big inter-column gap so it reads as inline links. */
.footer-nav:not(:has(.footer-col > a + a)) { gap: .5rem 1.6rem; }

/* Bottom tier: copyright + legal links, divided from the sitemap above. */
.footer-bottom {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space);
    max-width: var(--container);
    margin: calc(var(--space) * 2) auto 0;
    padding-top: calc(var(--space) * 1.5);
    padding-inline: calc(var(--space) * 1.5);
    border-top: 1px solid var(--border, rgba(0, 0, 0, .08));
    font-size: .9rem;
}
.footer-legal { display: flex; flex-wrap: wrap; gap: var(--space); }
.footer-legal a { color: var(--color-muted, #888); text-decoration: none; }
.footer-legal a:hover { color: var(--color-primary, #0066cc); text-decoration: underline; }

/* --- Layout primitives -------------------------------------------------- */

/* main is a FULL-WIDTH flow of sections. Width is capped by the .container INSIDE
   each section — so a section's background color or hero PHOTO bleeds edge to edge
   while its text stays aligned to the page column. (Previously main itself capped
   the width, which boxed every hero photo into a centered 1080px band and forced
   the tone bands to fake full-bleed with a width:100vw hack.) */
main { width: 100%; }

/* Safety net: bare content dropped STRAIGHT into <main> (a simple legal/prose page
   with no wrapping .container or .section) still reads as a centered, capped,
   padded column. Zero specificity via :where(), so a real .container / .prose /
   style.css rule inside always wins; full-bleed sections opt out by class. */
main > :where(:not(.section):not([class*="section-"]):not(.hero):not(.bleed):not(.cta)) {
    max-width: var(--container);
    margin-inline: auto;
    padding-inline: var(--space);
}

.container {
    width: 100%;
    max-width: var(--container);
    margin-inline: auto;
    /* A little more gutter on wide viewports than the base unit. */
    padding-inline: clamp(var(--space), 3vw, calc(var(--space) * 1.75));
}

/* Vertical section rhythm — fluid, so sections feel generous on desktop and
   tighten on phones instead of one flat value at every width. */
.section { padding-block: clamp(calc(var(--space) * 2.25), 5.5vw, calc(var(--space) * 4.5)); }

/* Even vertical spacing between stacked children (no margins to hand-tune). */
.stack > * + * { margin-top: var(--space); }

/* Responsive auto-fit grid — wraps columns without media queries. */
.grid {
    display: grid;
    gap: var(--space);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}

/* Full-bleed band: breaks out of the centered column to span the viewport. */
.bleed {
    width: 100vw;
    margin-inline: calc(50% - 50vw);
    padding-inline: var(--space);
}

/* --- Components ---------------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    gap: .5em;
    padding: 0.7em 1.35em;
    border: 0;
    border-radius: var(--radius);
    background: var(--color-primary, #0066cc);
    /* Readable on the brand color whether it's dark or light (derived in <head>). */
    color: var(--color-on-primary, #fff);
    /* A little presence: a contact shadow so a primary CTA sits ABOVE the page
       instead of reading as a flat rectangle. Grows on hover, presses on click. */
    box-shadow: var(--shadow-sm);
    font: inherit;
    font-weight: 600;
    line-height: 1.2;
    text-decoration: none;
    cursor: pointer;
}
.btn:hover { filter: brightness(0.96); box-shadow: var(--shadow); transform: translateY(-1px); }
.btn:active { transform: translateY(0); box-shadow: var(--shadow-sm); filter: brightness(0.92); }
/* A larger, more prominent CTA for the hero's primary action. */
.btn-lg { padding: 0.9em 1.75em; font-size: 1.05rem; }
/* A normal button label + its trailing icon shouldn't break across lines, but an
   over-long label (a whole sentence dropped into a CTA, a modal-open shortcode) must
   NOT run off the page edge and force horizontal scroll. Cap the width to the
   available space and let a genuinely long label wrap inside it — flex items are
   content-sized, so ordinary short buttons still sit on one line. */
.btn, .btn-outline { max-width: 100%; overflow-wrap: break-word; }

/* .btn-outline is a COMPLETE button on its own (it carries the same box as .btn —
   display/padding/radius/line-height — so `class="btn-outline"` used ALONE is not a
   squished run of text with a tight outline). It only swaps the fill for a 2px
   inset outline. A site's style.css restyles the look; the box stays. */
.btn-outline {
    display: inline-flex;
    align-items: center;
    gap: .5em;
    padding: 0.7em 1.35em;
    border: 1px solid currentColor;
    border-radius: var(--radius);
    background: transparent;
    color: var(--color-primary, #0066cc);
    font: inherit;
    font-weight: 600;
    line-height: 1.2;
    text-decoration: none;
    cursor: pointer;
}
.btn-outline:hover { transform: translateY(-1px); }

.card {
    padding: calc(var(--space) * 1.4);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--color-background, #fff);
    box-shadow: var(--shadow);
}

/* Card-as-link: a common pattern is to make a whole card a single <a> so the
   entire tile is clickable. Left alone, the anchor turns every line inside it
   (heading, excerpt, meta) into underlined link-colored text — the "everything is
   a link" look. Neutralize that: the card reads as normal content, lifts on hover
   to signal it's clickable, and its heading takes the brand color on hover. Any
   real .link-underline / .btn inside keeps its own affordance. */
a.card { text-decoration: none; color: var(--color-text, inherit); display: block; }
a.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg, 0 12px 30px rgba(0, 0, 0, .12)); }
a.card :is(h1, h2, h3, h4) { color: var(--color-text, inherit); transition: color .15s ease; }
a.card:hover :is(h1, h2, h3, h4) { color: var(--color-primary, #0066cc); }
a.card :is(p, li, .post-meta, time, small) { color: var(--color-muted, #555); text-decoration: none; }
a.card .link-underline { color: var(--color-primary, #0066cc); }
a.card:hover .link-underline { background-size: 100% 2px; }

/* Text over a photo — the sanctioned way to keep hero/banner copy readable.
   Put .scrim on the element whose background-image is the photo; it lays a dark
   gradient between the image and the text and forces white text + a soft shadow.
   Don't hand-roll text-on-image without this — that's where contrast breaks. */
.scrim {
    position: relative;
    color: #fff;
    background-size: cover;
    background-position: center;
}
.scrim::before {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, rgba(0, 0, 0, .25), rgba(0, 0, 0, .6));
    z-index: 0;
}
.scrim > * { position: relative; z-index: 1; }
.scrim :is(h1, h2, h3, h4, p) { color: #fff; text-shadow: 0 1px 3px rgba(0, 0, 0, .5); }

/* Text-on-photo owns its OWN foreground contract: EVERY foreground element on a
   scrim (or a background-photo band) reads white — not just headings/paragraphs.
   Without this, a per-site skin that hand-colors a .btn-outline / .eyebrow / link
   (e.g. to the brand blue) leaves it low-contrast on the dark photo — the
   "invisible hero button" bug. The surface owns their color here, so the skin
   never has to (and shouldn't). Higher specificity than a bare `.btn-outline{}`
   skin rule, so it wins by construction. Primary `.btn` is left alone — a solid
   fill is contrast-safe on any background via --color-on-primary. */
:is(.scrim, [data-options~="background-photo"]) { --border: color-mix(in srgb, #fff 30%, transparent); }
:is(.scrim, [data-options~="background-photo"]) :is(.lede, .stat-label, li) { color: color-mix(in srgb, #fff 88%, transparent); }
:is(.scrim, [data-options~="background-photo"]) .eyebrow { color: #fff; }
:is(.scrim, [data-options~="background-photo"]) :is(a:not(.btn):not(.btn-outline), .link-underline) { color: #fff; }
/* A plain text link (e.g. a phone number) on a colored/dark band must inherit the
   band's readable foreground, not stay its brand link-color — otherwise it's a faint
   blue-on-blue link. Buttons keep their own on-band treatment. */
:is(.cta, .section-invert,
    [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"])
    :is(a:not(.btn):not(.btn-outline), .link-underline) { color: inherit; }
:is(.scrim, [data-options~="background-photo"]) .btn-outline {
    color: #fff; border-color: #fff; background: transparent;
}

/* ======================================================================
   SECTION PRIMITIVES — a vocabulary of full, production-ready sections.
   Compose pages from these instead of hand-rolling section layout; they
   read the same Theme tokens, so they look intentional under ANY palette/
   token set and hold up on mobile with no extra CSS. A site's style.css
   adds brand flavor ON TOP; these guarantee a high floor (no flat columns,
   no broken grids). Class reference lives in GUIDE.md.
   ====================================================================== */

/* --- Typographic helpers ------------------------------------------------ */

/* Small uppercase kicker above a heading. */
.eyebrow {
    display: inline-block;
    margin: 0 0 .5em;
    font-family: var(--font-body, inherit);
    font-size: .8rem;
    font-weight: 600;
    letter-spacing: .12em;
    text-transform: uppercase;
    /* Accent as small text → the WCAG-nudged accent token (falls back to raw accent
       on older templates that don't emit it). */
    color: var(--color-accent-text, var(--color-accent, var(--color-primary, #0066cc)));
}
/* Oversized hero headline — fluid, tight leading, slight negative tracking. */
.display {
    font-size: clamp(2.2rem, 1.4rem + 3.6vw, calc(1rem * var(--font-scale) * var(--font-scale) * var(--font-scale) * 1.15));
    line-height: 1.05;
    letter-spacing: -0.02em;
    margin: 0 0 .3em;
}
/* Intro paragraph — larger, calmer, capped measure. */
.lede {
    font-size: clamp(1.1rem, 1rem + .5vw, 1.35rem);
    line-height: 1.5;
    color: var(--color-muted, #555);
    max-width: 60ch;
}
/* Long-form rich text: comfortable measure + vertical rhythm for CMS/markdown. */
.prose { max-width: 68ch; }
.prose > * + * { margin-top: 1em; }
.prose :is(h2, h3) { margin-top: 1.6em; }
.prose img { max-width: 100%; height: auto; border-radius: var(--radius); }
/* Restore native list affordances the global reset strips — long-form content
   needs bullets/numbers and indent back. */
.prose :is(ul, ol) { list-style: revert; padding-left: 1.5em; }
.prose li + li { margin-top: .35em; }

/* Blog post: center the reading column with a gutter; any full-width band the post
   adds below (e.g. the "Related posts" section) still spans the post's <main>. */
.blog-post > .prose { margin-inline: auto; padding-inline: 1.5rem; }

/* Share row (cms_share_html) — a line of icon pills under the article + a copy button
   with a "Copied" tooltip that main.js toggles. */
.share { display: flex; align-items: center; flex-wrap: wrap; gap: .6rem; margin-top: calc(var(--space) * 2); padding-top: calc(var(--space) * 1.25); border-top: 1px solid var(--border); }
.share-label { font-size: .78rem; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--color-muted, #667); margin-right: .2rem; }
/* Works for both the <a> network links and the copy <button> — the reset (appearance,
   font, line-height) makes the button render identically to the anchors. */
.share-link { -webkit-appearance: none; appearance: none; font: inherit; line-height: 0; display: inline-grid; place-items: center; width: 2.4rem; height: 2.4rem; padding: 0; border-radius: 999px; border: 1px solid var(--border); background: var(--surface, #fff); cursor: pointer; position: relative; transition: color .2s, border-color .2s, transform .2s; }
.share-link:hover { color: var(--color-primary, #0066cc); border-color: color-mix(in srgb, var(--color-primary, #0066cc) 40%, var(--border)); transform: translateY(-1px); }
.share-link svg { width: 1.15rem; height: 1.15rem; }
.share-link svg path { fill: var(--color-text, #1a1a1a); }
.share-link:hover svg path { fill: var(--color-primary, #0066cc); }
.share-copied { position: absolute; bottom: calc(100% + .4rem); left: 50%; transform: translateX(-50%); background: var(--color-text, #1a1a1a); color: var(--color-background, #fff); font-size: .7rem; padding: .2em .5em; border-radius: 6px; opacity: 0; pointer-events: none; transition: opacity .2s; white-space: nowrap; }
.share-copy.is-copied .share-copied { opacity: 1; }

/* --- Site search (header icon + pages/search.php) ------------------------- */
/* place-items centers the icon in the grid; align-items + justify-content are
   stated explicitly too so a per-site style.css that flips this to display:flex
   (a common re-skin) still centers the glyph on BOTH axes instead of stranding
   it top-left. */
.nav-search { display: inline-grid; place-items: center; align-items: center; justify-content: center; width: 2.4rem; height: 2.4rem; border-radius: 999px; color: inherit; flex: none; text-decoration: none; transition: color .2s, background-color .2s; }
.nav-search svg { width: 1.15rem; height: 1.15rem; }
.nav-search:hover { color: var(--color-primary, #0066cc); background: color-mix(in srgb, currentColor 8%, transparent); }
.search-page { min-height: 55vh; }
.search-page .section-head { margin-bottom: clamp(calc(var(--space) * 2), 0vw, calc(var(--space) * 3.5)); }
.search-box { position: relative; margin-top: calc(var(--space) * 1.5); }
.search-box-icon { position: absolute; left: 1rem; top: 50%; transform: translateY(-50%); color: var(--color-muted, #667); pointer-events: none; display: grid; }
.search-box-icon svg { width: 1.2rem; height: 1.2rem; display: block; }
input[type=search].search-input { width: 100%; padding: .9em 1em .9em 50px; font-size: max(16px, 1.02rem); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface, #fff); color: var(--color-text, #1a1a1a); padding-left: 50px !important; }
.search-input:focus { outline: none; border-color: var(--color-primary, #0066cc); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary, #0066cc) 18%, transparent); }
.search-results { margin-top: calc(var(--space) * 1.5); display: grid; gap: .6rem; }
.search-result { display: grid; gap: .2rem; padding: 1rem 1.15rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface, #fff); text-decoration: none; color: var(--color-text, #1a1a1a); transition: border-color .2s, transform .2s, box-shadow .2s; }
.search-result:hover { border-color: color-mix(in srgb, var(--color-primary, #0066cc) 45%, var(--border)); transform: translateY(-1px); box-shadow: var(--shadow-sm, 0 1px 3px rgba(0,0,0,.08)); }
.search-result-kind { font-size: .68rem; text-transform: uppercase; letter-spacing: .06em; color: var(--color-primary, #0066cc); font-weight: 700; }
.search-result-title { font-weight: 600; font-size: 1.05rem; }
.search-result-snip { font-size: .92rem; color: var(--color-muted, #667); }
.search-result-snip mark { background: color-mix(in srgb, var(--color-primary, #0066cc) 18%, transparent); color: inherit; padding: 0 .12em; border-radius: 2px; }
.search-empty { color: var(--color-muted, #667); margin-top: 1rem; }

/* --- Section rhythm & width modifiers ----------------------------------- */

.section-lg { padding-block: calc(var(--space) * 5); }
.section-sm { padding-block: calc(var(--space) * 1.5); }
/* A narrow, centered measure for focused content (CTA, single column). */
.measure { max-width: 56ch; margin-inline: auto; }
.text-center { text-align: center; }
.text-center .lede,
.text-center .measure { margin-inline: auto; }
/* A measure/lede that carries text-center on ITSELF (not just via an ancestor) must
   also centre its own box, or the capped column strands at the left with centered
   text inside it. */
.measure.text-center, .lede.text-center { margin-inline: auto; }

/* Explicit-count grids that collapse gracefully (no media query needed). */
.grid-2 { display: grid; gap: calc(var(--space) * 1.5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); }
/* justify-content:center + a per-item max-width so a partial last row (e.g. 4
   items wrapping to 3 + 1) CENTERS at a natural width rather than stretching the
   lone orphan full-bleed — the recurring "N in a row then 1 giant one" tell. */
.grid-3 { display: flex; flex-wrap: wrap; justify-content: center; gap: calc(var(--space) * 1.5); }
.grid-3 > * { flex: 1 1 15rem; max-width: 28rem; }
.grid-4 { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space); }
.grid-4 > * { flex: 1 1 12rem; max-width: 22rem; }

/* --- Hero --------------------------------------------------------------- */

.hero { padding-block: clamp(calc(var(--space) * 3), 8vw, calc(var(--space) * 7)); }
.hero .lede { margin-top: var(--space); }
/* stretch so a solid + a ghost button share the exact same height; a bare text
   link (not a button) opts back out to center so it doesn't stretch tall. */
.hero-actions { display: flex; flex-wrap: wrap; align-items: stretch; gap: var(--space); margin-top: calc(var(--space) * 1.5); }
.hero-actions > :not(.btn):not(.btn-outline) { align-self: center; }
.hero.text-center .hero-actions { justify-content: center; }
/* When the actions are the SECOND column of a 2-up split/CTA (copy on one side,
   buttons on the other) the split vertically centers both columns — but the
   hero-actions' own top margin (meant for stacking UNDER hero copy) then shoves
   the button row below the copy's centerline, reading as misaligned. Zero it in
   that context so the buttons sit on the copy's optical center. */
:is(.split, .split-wide, .split-narrow) > .hero-actions { margin-top: 0; align-self: center; }
/* A product shot / media that follows the CTA row (e.g. the aura hero) needs real
   air below the buttons so the buttons don't sit right on the frame's top edge. */
.hero-actions + :is(.aura, .device, .media, figure, [data-reveal]) { margin-top: calc(var(--space) * 2.5); }

/* Split hero / any media-beside-text section. Image and copy share a row and
   stack on narrow screens. Put .media-text first or last; add .reverse to flip. */
.split {
    display: grid;
    gap: clamp(var(--space), 4vw, calc(var(--space) * 3));
    grid-template-columns: 1fr 1fr;
    align-items: center;
}
.split.reverse > :first-child { order: 2; }
.split img,
.split .media {
    width: 100%;
    height: 100%;
    min-height: 16rem;
    object-fit: cover;
    border-radius: var(--radius);
    background: var(--surface-2, #e9e9ee);
}

/* ======================================================================
   COMPOSITION ARCHETYPES — asymmetric, editorial layouts.
   The auto-fit grids above are the SAFE default (symmetric rows of equal
   cards). That symmetry is the #1 "AI-built" tell. These break it: unequal
   splits, an alternating zig-zag of media rows, an offset/overlapping hero,
   and a magazine grid. Reach for one of these to give a page a composition
   that reads as hand-designed — vary which one you use site-to-site.
   All collapse to a single sane column on phones.
   ====================================================================== */

/* Asymmetric split — an editorial 7/5 (copy-led) or 5/7 (media-led) instead
   of the safe 50/50. Same markup/behavior as .split; only the ratio differs. */
.split-wide,
.split-narrow {
    display: grid;
    gap: clamp(var(--space), 4vw, calc(var(--space) * 3.5));
    align-items: center;
}
.split-wide   { grid-template-columns: 1.4fr 1fr; }   /* copy larger, media smaller */
.split-narrow { grid-template-columns: 1fr 1.4fr; }   /* media larger, copy smaller */
.split-wide.reverse > :first-child,
.split-narrow.reverse > :first-child { order: 2; }
.split-wide img, .split-wide .media,
.split-narrow img, .split-narrow .media {
    width: 100%; height: 100%; min-height: 18rem;
    object-fit: cover; border-radius: var(--radius);
    background: var(--surface-2, #e9e9ee); overflow: hidden;
}

/* Feature ROWS — a repeating zig-zag of media-beside-text rows that AUTO-
   ALTERNATE side each row. This is the antidote to "three identical cards":
   several substantial features, each given room, in an editorial rhythm.
   Markup: .feature-rows > .feature-row (each = a media element + a text block). */
.feature-rows { display: grid; gap: calc(var(--space) * 3.5); }
.feature-row {
    display: grid;
    gap: clamp(var(--space), 4vw, calc(var(--space) * 3));
    grid-template-columns: 1fr 1fr;
    align-items: center;
}
.feature-row:nth-child(even) > :first-child { order: 2; }   /* flip every other row */
.feature-row img, .feature-row .media {
    width: 100%; height: 100%; min-height: 17rem;
    object-fit: cover; border-radius: var(--radius);
    background: var(--surface-2, #e9e9ee); overflow: hidden;
}
.feature-row .feature-row-body { display: grid; gap: .6rem; align-content: center; }

/* --- Fragility guard for the 2-up archetypes -----------------------------
   .split / .split-wide / .split-narrow / .feature-row assume EXACTLY two children:
   a media block and a SINGLE copy wrapper. The model sometimes drops the heading /
   paragraph / button as DIRECT children instead of wrapping them, so grid auto-
   placement scatters them across cells (and .flow spacing, which targets grand-
   children, never applies). When a 3rd direct child exists, fall back to one column
   so the content just stacks cleanly instead of scattering. */
:is(.split, .split-wide, .split-narrow, .feature-row):has(> :nth-child(3)) {
    grid-template-columns: 1fr;
}

/* Offset / overlap hero — a media block with a card that overlaps its lower
   corner (negative margin), the classic "designed, not stacked" composition.
   Markup: .offset > (.offset-media + .offset-card). */
/* The card and the image share ONE grid cell, so the card overlaps the photo's
   lower-right corner INSIDE its box and only peeks a small fixed tail below it
   (the --offset-tail, reserved by the container's padding-bottom). This is
   self-adjusting — the tail stays ~constant no matter how tall the card's copy
   is — unlike a negative-margin, whose overshoot depends on card height. */
.offset { display: grid; grid-template-columns: 1fr; gap: 0; position: relative; --offset-tail: clamp(2.5rem, 6vw, 6rem); padding-bottom: var(--offset-tail); }
/* All children share the single cell to create the overlap. Pin any stray extra
   child (a heading the model added as a third child) into that cell too, so it can't
   spawn an empty second row that breaks the composition. */
.offset > * { grid-area: 1 / 1; }
.offset-media {
    grid-area: 1 / 1;
    width: 100%; min-height: 22rem; object-fit: cover;
    border-radius: var(--radius-lg); overflow: hidden;
    background: var(--surface-2, #e9e9ee);
}
.offset-media img { width: 100%; height: 100%; object-fit: cover; }
.offset-card {
    grid-area: 1 / 1;
    align-self: end;
    justify-self: end;
    position: relative;
    top: var(--offset-tail);
    width: min(34rem, 90%);
    margin-right: clamp(0px, 4vw, calc(var(--space) * 2));
    padding: calc(var(--space) * 1.6);
    background: var(--color-background, #fff);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    z-index: 2;
}

/* Editorial / magazine grid — an asymmetric 12-col grid where children claim
   spans, so content sits in a deliberate composition rather than equal cells.
   Give a child .col-5/.col-7/.col-4/.col-8 to size it; unset children flow. */
.editorial-grid {
    display: grid;
    gap: calc(var(--space) * 1.75);
    grid-template-columns: repeat(12, 1fr);
    align-items: start;
}
.editorial-grid > * { grid-column: span 12; }
.col-4 { grid-column: span 4; }
.col-5 { grid-column: span 5; }
.col-6 { grid-column: span 6; }
.col-7 { grid-column: span 7; }
.col-8 { grid-column: span 8; }

@media (max-width: 720px) {
    .split-wide, .split-narrow,
    .feature-row { grid-template-columns: 1fr; }
    .split-wide.reverse > :first-child,
    .split-narrow.reverse > :first-child,
    .feature-row:nth-child(even) > :first-child { order: 0; }
    /* Mobile: unstack the overlap — image then card in normal flow, slight lift. */
    .offset { padding-bottom: 0; }
    .offset-media { grid-area: auto; }
    .offset-card { grid-area: auto; align-self: auto; position: static; top: auto;
        margin-top: calc(var(--space) * -2.5); width: 92%; justify-self: center; margin-right: 0; }
    .editorial-grid > *,
    .col-4, .col-5, .col-6, .col-7, .col-8 { grid-column: span 12; }
}

/* --- Overflow safety -----------------------------------------------------
   Grid/flex children default to min-width:auto, which refuses to shrink below
   their content's intrinsic size — so a wide media or a long token pushes the
   track past the viewport and the page scrolls sideways (the option-2 bleed).
   Force the composition cells to be shrinkable, and break over-long words so
   copy wraps instead of widening the layout. Cheap, universal, no visual cost
   on well-behaved content. */
:is(.split, .split-wide, .split-narrow, .feature-row,
    .editorial-grid, .grid-2, .grid-3, .grid-4,
    .stats, .feature-cards, .bento, .testimonials, .steps, .pricing) > * { min-width: 0; }
.offset-card { min-width: 0; }
:is(.offset-card, .feature-row-body, .split .flow, .split-wide .flow, .flow, .prose) { overflow-wrap: break-word; }

/* --- Feature / value cards ---------------------------------------------- */

.feature { display: grid; gap: .5rem; align-content: start; }
/* Give a lead icon real breathing room from the heading it introduces (a glyph
   jammed against the title reads as a spacing bug). */
.feature .icon { color: var(--color-primary, #0066cc); margin-bottom: .5rem; }
.feature h3 { margin: 0; }
.feature p { margin: 0; color: var(--color-muted, #555); }
/* As cards: wrap each .feature in .card, or use .feature-cards for the grid.
   Centered partial last row + a width cap so a lone trailing card doesn't
   stretch full-bleed (the "3 then 1 giant card" orphan). */
.feature-cards { display: flex; flex-wrap: wrap; justify-content: center; gap: calc(var(--space) * 1.25); }
.feature-cards > * { flex: 1 1 14rem; max-width: 26rem; }
.feature-cards .card { display: grid; gap: .5rem; align-content: start; }

/* --- Stats band --------------------------------------------------------- */

.stats { display: grid; gap: calc(var(--space) * 1.5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 12.5rem), 1fr)); text-align: center; }
/* A stats band dropped straight after body copy (a lede/quote/paragraph, with no
   .flow or .section-head owning the rhythm — e.g. the proof band) hugs the text
   above it. Give it real separation. A .section-head already carries its own
   margin-bottom, so this doesn't double up there. */
:is(.lede, blockquote, p) + .stats { margin-top: calc(var(--space) * 2); }
/* Scope to :is(.stat, .stats) so the number/label keep their styling even when the
   model writes them flat inside .stats without a per-stat .stat wrapper. */
:is(.stat, .stats) .stat-num {
    display: block;
    font-family: var(--font-heading, inherit);
    font-size: clamp(1.85rem, 1.3rem + 1.9vw, 2.5rem);
    font-weight: 800;
    line-height: 1;
    color: var(--color-primary, #0066cc);
    letter-spacing: -0.02em;
    /* A long compound figure ("$500+/MWh") must never punch past its cell into the
       page edge. min-width:0 above lets the track shrink; cap the figure to its
       cell and, as a last resort on a genuinely too-narrow track, allow the token
       to scale down rather than overflow. */
    max-width: 100%;
    /* A formatted figure ("$184,000", "11 months") is a single logical unit — never
       let it break mid-number into "$184,00 / 0". Keep it on one line; if a per-site
       skin sizes it large, it bleeds into the column gap rather than fracturing (and
       the auto-fit grid drops to fewer, wider columns before that ever overflows the
       container). Belt-and-suspenders: also stop hyphenation/anywhere-breaks. */
    white-space: nowrap;
    overflow-wrap: normal;
    word-break: keep-all;
    hyphens: none;
    overflow: hidden;
    text-overflow: ellipsis;
}
:is(.stat, .stats) .stat-label { color: var(--color-muted, #555); font-size: .95rem; }
/* No .stat wrapper at all → the flat num/label sequence would scatter across auto-fit
   cells. Stack to one column so each num sits directly above its label, in order. */
.stats:not(:has(> .stat)) { grid-template-columns: 1fr; }
/* A caption / source line dropped straight AFTER a stats band (a "Sources: …" or
   "Figures reflect …" note) otherwise hugs the numbers. Give it separation. */
.stats + :is(p, small, .stat-note, .source, .footnote, .fine-print) {
    margin-top: calc(var(--space) * 1.75);
    color: var(--color-muted, #667);
    font-size: .85rem;
}

/* --- Numbered steps / process ------------------------------------------- */

/* Flex-fill (not grid auto-fit) so a full row's items grow to share the width.
   justify-content:center + a max-width cap on each step means the LAST row —
   when it holds fewer items than the rows above (5 steps → 4 + 1) — CENTERS at a
   natural card width instead of ballooning the lone orphan across the whole row.
   That single stretched-full-width orphan is the recurring "4 in a row then 1"
   tell; the cap kills it at every item count. */
.steps { display: flex; flex-wrap: wrap; justify-content: center; gap: calc(var(--space) * 1.5); counter-reset: step; }
.steps > .step { flex: 1 1 13rem; max-width: 24rem; }
/* Count-aware balanced columns so a partial last row never orphans. These out-
   specify a plain `.steps` (a per-site style.css almost always re-declares .steps
   as a `grid-template-columns: auto-fit … 1fr` grid, which re-introduces the
   stretched-orphan bug the flex rule above prevents) — so they hold whether steps
   renders as the base flex OR a re-skinned grid. The flex-basis line covers the
   flex case (grid ignores flex-basis); grid-template-columns covers the grid case
   (flex ignores it). Scoped to wide screens so phones still stack. */
@media (min-width: 64rem) {
    .steps:has(> .step:nth-child(5):last-child)  { grid-template-columns: repeat(5, 1fr); } /* 5 → 5-across */
    .steps:has(> .step:nth-child(6):last-child)  { grid-template-columns: repeat(3, 1fr); } /* 6 → 3×2 */
    .steps:has(> .step:nth-child(8):last-child)  { grid-template-columns: repeat(4, 1fr); } /* 8 → 4×2 */
    .steps:has(> .step:nth-child(9):last-child)  { grid-template-columns: repeat(3, 1fr); } /* 9 → 3×3 */
    .steps:has(> .step:nth-child(5):last-child) > .step,
    .steps:has(> .step:nth-child(6):last-child) > .step { flex-basis: 0; max-width: none; }
}

/* ------------------------------------------------------------------------ *
   ORPHAN GUARD (generic) — the site's own style.css almost always re-declares
   these card rows as `grid-template-columns: repeat(auto-fit, minmax(…, 1fr))`
   (gen_normalize_skin even rewrites fixed grids INTO that form for reflow). At
   the awkward counts that don't divide the auto-computed column count, the last
   row stretches its few cells full-width — the recurring "N in a row then a giant
   orphan" tell. These count-aware rules force a BALANCED column count for the
   common composite counts, always REDUCING columns (cells get wider, never
   crushed — safe for copy-bearing cards), and out-specify the plain auto-fit
   rule so they win whatever the skin wrote. Scoped to wide screens; phones keep
   the responsive default. Inert on flex-based primitives (which cap orphan width
   themselves). Prime counts (5, 7) are left to the per-primitive rules above.   */
@media (min-width: 52rem) {
    /* Only UNIFORM card rows whose base layout uses auto-fit 1fr (which stretches
       orphans). NOT .bento (deliberately asymmetric), NOT .team (already caps cell
       width + centers partial rows), NOT a .split (two-panel). */
    :is(.stats, .feature-cards, .testimonials):has(> :nth-child(6):last-child)  { grid-template-columns: repeat(3, 1fr); } /* 6 → 3×2 */
    :is(.stats, .feature-cards, .testimonials):has(> :nth-child(8):last-child)  { grid-template-columns: repeat(4, 1fr); } /* 8 → 4×2 */
    :is(.stats, .feature-cards, .testimonials):has(> :nth-child(9):last-child)  { grid-template-columns: repeat(3, 1fr); } /* 9 → 3×3 */
    /* .stats holds short numbers, so a 5-up single row reads clean (unlike copy cards). */
    .stats:has(> :nth-child(5):last-child) { grid-template-columns: repeat(5, 1fr); } /* 5 → 5-across */
}
.step { display: grid; gap: .4rem; align-content: start; }
.step .step-num {
    display: inline-grid;
    place-items: center;
    width: 2.25rem; height: 2.25rem;
    border-radius: 999px;
    background: var(--color-primary, #0066cc);
    color: var(--color-on-primary, #fff);
    font-weight: 700;
}
.step h3 { margin: .25rem 0 0; }
.step p { margin: 0; color: var(--color-muted, #555); }

/* --- Pricing ------------------------------------------------------------ */

.pricing { display: grid; gap: calc(var(--space) * 1.5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); align-items: stretch; }
.price-card {
    display: grid;
    gap: var(--space);
    align-content: start;
    padding: calc(var(--space) * 1.5);
    border: 1px solid rgba(0, 0, 0, .1);
    border-radius: var(--radius);
    background: var(--color-background, #fff);
}
.price-card.featured {
    border-color: var(--color-primary, #0066cc);
    box-shadow: var(--shadow);
}
.price-card .price { font-family: var(--font-heading, inherit); font-size: clamp(2rem, 1.5rem + 2vw, 2.75rem); font-weight: 800; line-height: 1; letter-spacing: -0.02em; }
.price-card .price small { font-size: .9rem; font-weight: 500; color: var(--color-muted, #555); }
.price-card ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.price-card li { display: flex; gap: .5rem; align-items: baseline; color: var(--color-muted, #444); }
.price-card .btn { margin-top: auto; text-align: center; }

/* --- Testimonials ------------------------------------------------------- */

.testimonials { display: grid; gap: calc(var(--space) * 1.5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); }
.testimonial {
    display: grid;
    gap: var(--space);
    padding: calc(var(--space) * 1.5);
    border-radius: var(--radius);
    background: var(--color-background, #fff);
    box-shadow: var(--shadow);
    border: 1px solid rgba(0, 0, 0, .05);
}
.testimonial blockquote { margin: 0; font-size: 1.1rem; line-height: 1.5; }
.testimonial .author { display: flex; align-items: center; gap: .75rem; }
/* A hand-written testimonial that omits the .author-meta wrapper puts name + role as
   direct flex children, rendering them side-by-side ("Jane Doe   CEO"). When the
   wrapper is absent, let the text wrap and give the name a full-width basis so the
   role drops onto its own line. */
.testimonial .author:not(:has(.author-meta)) { flex-wrap: wrap; }
.testimonial .author:not(:has(.author-meta)) .author-name { flex-basis: 100%; }
.testimonial .author img { width: 2.75rem; height: 2.75rem; border-radius: 999px; object-fit: cover; }
.testimonial .author-name { font-weight: 600; }
.testimonial .author-role { color: var(--color-muted, #555); font-size: .9rem; }
.stars { color: var(--color-accent, #f5a623); letter-spacing: .1em; }

/* --- Logo cloud --------------------------------------------------------- */

.logos { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: calc(var(--space) * 2); }
.logos img { height: 1.75rem; width: auto; opacity: .6; filter: grayscale(1); }
/* A logo row dropped straight after an eyebrow/heading (e.g. the composite proof
   band, which has no .flow to space its children) hugs the label above and the
   quote below. Give it real breathing room on both sides. */
:is(.eyebrow, .section-head, h2) + .logos { margin-top: calc(var(--space) * 1.5); }
.logos + :is(blockquote, .display, .lede) { margin-top: calc(var(--space) * 2); }

/* --- Call to action band ------------------------------------------------ */

.cta {
    display: grid;
    gap: var(--space);
    justify-items: center;
    text-align: center;
    padding: calc(var(--space) * 3.5) var(--space);
    border-radius: var(--radius);
    background: var(--color-band-primary, var(--color-primary, #0066cc));
    color: var(--color-on-band-primary, var(--color-on-primary, #fff));
}
.cta :is(h1, h2, h3) { color: inherit; margin: 0; }
.cta p { color: inherit; opacity: .9; max-width: 52ch; margin: 0; }
.cta .btn { background: var(--color-on-band-primary, var(--color-on-primary, #fff)); color: var(--color-band-primary, var(--color-primary, #0066cc)); }
/* A FILLED .btn on ANY colored/dark band (not just inside a .cta) would render brand-
   on-brand and vanish — e.g. a blue button on a blue band. Give it the same inversion
   the .cta uses (light fill + brand-colored text) so the primary action stays legible
   on every band. Light bands (tint/surface) are excluded — a filled button reads fine
   there. .btn-outline has its own on-band treatment below. */
:is(.scrim, .section-invert,
    [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"],
    [data-options~="background-photo"]) .btn:not(.btn-outline) {
    background: var(--color-on-band-primary, #fff);
    color: var(--color-band-primary, var(--color-primary, #0066cc));
}
/* Inside a CTA, let the .cta's own grid gap own the spacing so the content reads
   as one centered cluster (not a head + a stranded button pushed apart by their
   separate margins). */
.cta > .section-head { margin-bottom: 0; }
/* The CTA centers its content (justify-items/text-align:center), but .section-head is
   a column flex whose default align-items:stretch left-aligns any child that can't
   fill the width — e.g. .cta p is capped at 52ch, so its centered text stranded in a
   left-hugging box. Center the head's children so the paragraph sits under the title. */
.cta > .section-head { align-items: center; }
.cta .hero-actions { margin-top: calc(var(--space) * .5); }

/* --- FAQ (native disclosure) -------------------------------------------- */

/* width:100% + min-width:0 so the accordion fills its column and does not shrink-wrap
   to the closed summaries — otherwise opening an item (a wider answer) visibly widens
   the whole block. */
.faq { display: grid; gap: .5rem; width: 100%; min-width: 0; max-width: 52rem; margin-inline: auto; }
.faq details { border-bottom: 1px solid rgba(0, 0, 0, .1); }
.faq summary {
    cursor: pointer;
    list-style: none;
    padding: calc(var(--space) * 1.1) calc(var(--space) * 1.25);
    font-weight: 600;
    display: flex;
    justify-content: space-between;
    gap: var(--space);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; font-weight: 400; transition: transform .2s ease; }
.faq details[open] summary::after { transform: rotate(45deg); }
.faq details > :not(summary) { padding: 0 calc(var(--space) * 1.25) calc(var(--space) * 1.1); color: var(--color-muted, #444); margin-top: 0; }
/* On a TEXTURED or PATTERNED band a bare-underline accordion dissolves into the
   busy background. Lift each item onto its own self-contained light surface panel
   (with its own dark text) so the questions stand out and stay legible on a light
   OR a colored textured band — the panel owns its contrast, not the surface. */
:is([data-options~="texture-paper"], [data-options~="texture-stucco"], [data-options~="texture-concrete"],
    [data-options~="texture-brushed"], [data-options~="texture-canvas"], [data-options~="texture-linen"],
    [data-options~="texture-carbon"], [data-options~="texture-crosshatch"], [data-options~="texture-stripes"],
    [data-options~="pattern-dots"], [data-options~="pattern-grid"], [data-options~="pattern-waves"],
    [data-options~="background-mesh"], [data-options~="grain"]) .faq { gap: .6rem; }
:is([data-options~="texture-paper"], [data-options~="texture-stucco"], [data-options~="texture-concrete"],
    [data-options~="texture-brushed"], [data-options~="texture-canvas"], [data-options~="texture-linen"],
    [data-options~="texture-carbon"], [data-options~="texture-crosshatch"], [data-options~="texture-stripes"],
    [data-options~="pattern-dots"], [data-options~="pattern-grid"], [data-options~="pattern-waves"],
    [data-options~="background-mesh"], [data-options~="grain"]) .faq details {
    background: var(--surface, #fff);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}
:is([data-options~="texture-paper"], [data-options~="texture-stucco"], [data-options~="texture-concrete"],
    [data-options~="texture-brushed"], [data-options~="texture-canvas"], [data-options~="texture-linen"],
    [data-options~="texture-carbon"], [data-options~="texture-crosshatch"], [data-options~="texture-stripes"],
    [data-options~="pattern-dots"], [data-options~="pattern-grid"], [data-options~="pattern-waves"],
    [data-options~="background-mesh"], [data-options~="grain"]) .faq summary { color: var(--color-text, #14161c); }
:is([data-options~="texture-paper"], [data-options~="texture-stucco"], [data-options~="texture-concrete"],
    [data-options~="texture-brushed"], [data-options~="texture-canvas"], [data-options~="texture-linen"],
    [data-options~="texture-carbon"], [data-options~="texture-crosshatch"], [data-options~="texture-stripes"],
    [data-options~="pattern-dots"], [data-options~="pattern-grid"], [data-options~="pattern-waves"],
    [data-options~="background-mesh"], [data-options~="grain"]) .faq details > :not(summary) { color: var(--color-muted, #555); }

/* --- Small UI bits ------------------------------------------------------ */

.badge, .chip {
    display: inline-flex;
    align-items: center;
    gap: .35em;
    padding: .25em .7em;
    border-radius: 999px;
    font-size: .8rem;
    font-weight: 600;
    line-height: 1.4;
    /* A pill hugs its label. In a flex parent (e.g. a filter row built as .grid grid-3,
       which is a flex row whose children get flex:1 1 15rem) it would otherwise GROW to
       fill; in a grid cell the default stretch would blow it to the cell width. flex:none
       stops the grow, fit-content + start alignment stop the stretch — pill stays pill. */
    flex: none;
    width: fit-content;
    justify-self: start;
    align-self: start;
}
.badge { background: var(--color-primary, #0066cc); color: var(--color-on-primary, #fff); }
.chip { background: rgba(0, 0, 0, .05); color: var(--color-text, #1a1a1a); }
/* Consistent inline-SVG icon sizing — pair with currentColor SVGs. Center the
   glyph within the wrapper so that when a box (an icon tile / .resource-icon /
   .accolade-figure) sizes the SVG smaller than the 1.5em wrapper, it stays
   optically centered instead of sitting at the wrapper's top-left corner. */
.icon { display: inline-flex; align-items: center; justify-content: center; width: 1.5em; height: 1.5em; vertical-align: middle; flex: none; }
.icon svg { width: 100%; height: 100%; display: block; }

/* --- Safety nets: graceful degradation when style.css is missing/late -----
   These carry ZERO specificity (via :where), so ANY rule in a site's style.css
   overrides them. Their only job is to stop an UNSTYLED element from rendering
   catastrophically: a bare inline <svg> that has a viewBox but no width/height
   falls back to the SVG default (~300px), which is the classic "giant icon"
   blowout when a page's style.css hasn't loaded yet. With these, such a page
   degrades to "plain but sane" (small icons, contained media) instead of broken.
   A styled site is completely unaffected. */
svg:where(:not([width]):not([height])) { max-width: 2rem; max-height: 2rem; }
:where(img) { max-width: 100%; height: auto; }

/* Interactive polish: smooth, consistent motion + clear focus everywhere. */
a, .btn, .btn-outline, .card, summary { transition: color .15s ease, background-color .15s ease, box-shadow .2s ease, transform .2s ease, filter .15s ease; }
/* Hover actually LIFTS: the shadow grows to the larger ambient one (not the same
   flat drop re-applied), and the hairline warms toward the brand — a small, real
   depth cue. */
.card:hover { box-shadow: var(--shadow-lg); border-color: color-mix(in srgb, var(--color-primary, #0066cc) 30%, var(--border)); transform: translateY(-3px); }
:where(a, button, summary, [tabindex]):focus-visible { outline: 2px solid var(--color-primary, #0066cc); outline-offset: 2px; border-radius: 2px; }

/* The one responsive break the section primitives need: collapse paired
   columns to a single column on phones. Everything else auto-fits already. */
@media (max-width: 720px) {
    .split { grid-template-columns: 1fr; }
    .split.reverse > :first-child { order: 0; }
}

/* --- Blog card (cms_blog_card_html) --------------------------------------
   A blog card's cover image bleeds to the card edges while the text is inset. The
   TITLE renders as <h3> inside .blog-card-link. Own this layout HERE so a per-site
   style.css can't half-break it — the recurring bug was a stylesheet zeroing the card
   padding and re-padding only `.blog-card h2`, leaving the real <h3> jammed against a
   top-accent border. Card padding is zeroed; every text element is inset instead. */
.blog-card { padding: 0; overflow: hidden; }
/* The card title lives inside this link — reset it so the <h3> doesn't render as a
   big underlined link-colored heading (the "everything is a link" look). */
.blog-card > .blog-card-link { display: block; text-decoration: none; color: inherit; }
.blog-card-link h2, .blog-card-link h3 { text-decoration: none; }
.blog-card .blog-card-cover { width: 100%; display: block; }
.blog-card :where(h2, h3) { padding: 1.15rem 1.35rem 0; margin: 0; }
.blog-card .post-meta { padding: .3rem 1.35rem 0; margin: 0; }
.blog-card > p { padding: .5rem 1.35rem 0; }
.blog-card .blog-card-more { display: inline-block; padding: .6rem 1.35rem 1.35rem; }

/* Blog index grid — a post card carries real content (title, date, excerpt, link),
   so four-up crushes it. Override the generic .feature-cards flex flow with a grid
   that caps at THREE columns: 1-up on phones, 2-up on tablets, 3-up at most. */
.blog-list { display: grid; gap: calc(var(--space) * 1.5); grid-template-columns: 1fr; }
@media (min-width: 40rem) { .blog-list { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 62rem) { .blog-list { grid-template-columns: repeat(3, 1fr); } }
.blog-list > * { flex: none; }

/* --- Collections (component('collection') / [collection] shortcode) ------ */

.collection { display: grid; gap: calc(var(--space) * 1.5); }
/* A collection embedded as a TOP-LEVEL page section (component('collection') /
   [collection] echoed straight into <main>, not nested inside a .section) has no
   section wrapper, so it carried no vertical rhythm — its heading jammed flush
   against the band above it (the recurring "no top margin" bug). Give a top-level
   collection the same block padding a .section has. Nested collections (inside a
   .container within a .section) are unaffected. */
main > .collection { padding-block: clamp(calc(var(--space) * 2.25), 5.5vw, calc(var(--space) * 4.5)); }
.collection-heading { margin: 0; }
/* A trailing note dropped straight after a collection in a plain .container (e.g.
   "want the deeper version?" under a blog grid) has no margin of its own and hugs
   the grid. Give whatever follows a collection real separation. */
.collection + *,
.carousel + *,
[data-carousel] + * { margin-top: clamp(calc(var(--space) * 1.5), 4vw, calc(var(--space) * 2.5)); }
/* Cards that link to a record's own page (cms_generic_card_html / a linked
   testimonial). The title link inherits the heading's own colour so a linked card
   reads identically to an unlinked one until you hover it — a collection whose
   linking option is off must not look like a different component. */
.collection-card-link { color: inherit; text-decoration: none; }
.collection-card-link:hover { text-decoration: underline; }
.collection-card-img { display: block; width: 100%; height: auto; }
.collection-card-meta { font-size: .85rem; color: var(--color-muted, #667); }
.collection-card-more { display: inline-block; margin-top: auto; font-weight: 600; }
.testimonial .collection-card-more { margin-top: var(--space); }

/* The generic detail page (cms_detail_html) — deliberately the same reading column
   as a blog post, so a type invented in the CMS gets the layout that was already
   designed rather than one of its own. The article is a bare <article>, not a
   .section, so it carries no band padding of its own: without the padding-block
   below, the eyebrow jams against the header and the share row against whatever
   band follows. Uses the .section rhythm verbatim so the article sits on the same
   vertical grid as every other band on the page (including its own "More" band). */
.collection-detail > .prose {
    margin-inline: auto;
    padding-inline: 1.5rem;
    padding-block: clamp(calc(var(--space) * 2.25), 5.5vw, calc(var(--space) * 4.5));
}
.collection-detail .post-cover { width: 100%; height: auto; border-radius: var(--radius); }

/* Stacked "list" layout — one item per row, full width. */
.collection-list { display: grid; gap: var(--space); }
.collection-list .testimonial,
.collection-list .card { width: 100%; }

/* --- Carousel (scroll-snap track + prev/next) --------------------------- */

.carousel { position: relative; }
.carousel-track {
    display: flex;
    gap: calc(var(--space) * 1.25);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    /* An overflow-x:auto box also clips on the block axis, so a card's drop shadow
       gets sheared off top and bottom. Give the track real room above and (more)
       below the cards so the shadows render in full instead of a hard cut line. */
    padding-block: .5rem 1.5rem;
    -webkit-overflow-scrolling: touch;
    /* No visible scroll bar — navigation is the arrows + click-and-drag (main.js).
       The track still scrolls (wheel/trackpad/touch/drag); the bar is just hidden. */
    cursor: grab;
    scrollbar-width: none;              /* Firefox */
    -ms-overflow-style: none;           /* old Edge */
}
.carousel-track::-webkit-scrollbar { width: 0; height: 0; display: none; }  /* WebKit */
/* While click-dragging: free scrolling (snap off so the drag isn't sticky), a
   grabbing cursor, and no text/image selection. main.js restores snap on release
   so it settles onto the nearest item. */
.carousel-track.is-dragging { cursor: grabbing; scroll-behavior: auto; scroll-snap-type: none; user-select: none; }
.carousel-track.is-dragging a,
.carousel-track.is-dragging img { pointer-events: none; }
.carousel-item {
    scroll-snap-align: start;
    flex: 0 0 min(100%, 22rem);
}
.carousel-item > * { height: 100%; }
.carousel-prev,
.carousel-next {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
    border: 1px solid rgba(0, 0, 0, .12);
    border-radius: 999px;
    background: var(--color-background, #fff);
    box-shadow: var(--shadow, 0 1px 3px rgba(0, 0, 0, .12));
    color: var(--color-text, #1a1a1a);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
}
.carousel-prev { left: -.5rem; }
.carousel-next { right: -.5rem; }
.carousel-prev[hidden],
.carousel-next[hidden] { display: none; }
@media (hover: none) {
    /* Touch devices swipe the track; hide the arrows. */
    .carousel-prev, .carousel-next { display: none; }
}
/* Slides mode — one FULL-WIDTH item per view (a content carousel you page through:
   quote slides, feature slides, or one message with several images), vs the default
   track that peeks the next card. Add class="carousel carousel--slides"; main.js
   still wires the arrows, drag and (if present) the dots. */
.carousel--slides .carousel-track { gap: 0; }
.carousel--slides .carousel-item { flex-basis: 100%; }
.carousel--slides .carousel-item > * { width: 100%; }
/* Centre a width-capped slide (e.g. a measure'd quote card) within the full-width
   item, so it isn't stranded at the left edge with its side shadow clipped by the
   track's overflow. A full-width child (a .split, a figure) is unaffected. */
.carousel--slides .carousel-item > [data-options~="measure"],
.carousel--slides .carousel-item > .measure { margin-inline: auto; }
/* When a dot pager sits below the track it adds height, so the arrows' top:50%
   (measured against the whole .carousel) drops below the images. Lift them by half
   the dots block so they centre on the TRACK/images instead. */
.carousel:has(.carousel-dots) :is(.carousel-prev, .carousel-next) { top: calc(50% - (var(--space) * 1.25 + .6rem) / 2); }
/* Dots — one per slide. main.js fills an EMPTY .carousel-dots with a .carousel-dot
   button per item and toggles .is-active as the track scrolls. Affordance only; the
   track is the source of truth, so with no JS the slides still scroll/swipe. */
.carousel-dots { display: flex; justify-content: center; gap: .5rem; margin-top: calc(var(--space) * 1.25); }
/* No-JS, or a single-slide carousel: main.js never fills the dots, so an empty
   .carousel-dots must not leave a stray gap below the track. */
.carousel-dots:empty { display: none; }
/* Pause/play for an autoplaying carousel — main.js adds it only when autoplay runs.
   Pause bars by default; a play triangle once .is-paused. Floating (no dots) it carries
   its own page-colored chip so it reads over a photo slide; in the dot row it is drawn
   in currentColor so it follows the section's foreground on any band. */
.carousel-pause {
    position: absolute; top: .5rem; right: .5rem; z-index: 2;
    display: grid; place-items: center;
    width: 2rem; height: 2rem; padding: 0;
    border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, var(--color-background, #fff) 88%, transparent);
    color: var(--color-text, #1a1a1a); cursor: pointer;
}
.carousel-dots .carousel-pause { position: static; width: 1.5rem; height: 1.5rem; margin-left: .5rem; background: transparent; color: inherit; }
.carousel-pause::before {
    content: ""; width: .6rem; height: .7rem;
    border-inline: .2rem solid currentColor;
}
.carousel-pause.is-paused::before {
    width: 0; height: 0; margin-left: .15rem;
    border-block: .35rem solid transparent; border-inline: .6rem solid currentColor; border-inline-end: 0;
}
.carousel-dot {
    appearance: none; border: 0; padding: 0; cursor: pointer;
    width: .6rem; height: .6rem; border-radius: 999px;
    background: color-mix(in srgb, var(--color-text, #1a1a1a) 22%, transparent);
    transition: background .2s ease, transform .2s ease;
}
.carousel-dot.is-active { background: var(--color-primary, #0066cc); transform: scale(1.3); }
/* On a colored/photo band, re-derive the dots from the band's own foreground so they
   stay visible on any surface (mirrors the chip-kicker/on-band pattern). */
:is(.scrim, .section-invert, [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"], [data-options~="background-photo"]) .carousel-dot { background: color-mix(in srgb, currentColor 30%, transparent); }
:is(.scrim, .section-invert, [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"], [data-options~="background-photo"]) .carousel-dot.is-active { background: currentColor; }

/* --- Modals (from the 'modals' CMS type) -------------------------------- */

.modal {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: none;
    align-items: center;
    justify-content: center;
    padding: var(--space);
}
.modal.is-open { display: flex; }
.modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, .55);
}
.modal-box {
    position: relative;
    width: min(30rem, 100%);
    max-height: calc(100vh - 2 * var(--space));
    overflow-y: auto;
    display: grid;
    gap: var(--space);
    padding: calc(var(--space) * 1.75);
    background: var(--color-background, #fff);
    color: var(--color-text, #1a1a1a);
    border-radius: var(--radius, 8px);
    box-shadow: 0 20px 60px rgba(0, 0, 0, .3);
    animation: modal-in .2s ease;
}
@keyframes modal-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.modal-heading { margin: 0; }
.modal-body { color: var(--color-muted, #444); }
.modal-body > :first-child { margin-top: 0; }
.modal-image { width: 100%; height: auto; border-radius: calc(var(--radius, 8px) * .75); object-fit: cover; }
.modal-cta { justify-self: start; }
.modal-close {
    position: absolute;
    top: .5rem;
    right: .5rem;
    width: 2rem;
    height: 2rem;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 999px;
    background: rgba(0, 0, 0, .06);
    color: inherit;
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
}
body.modal-open { overflow: hidden; }
@media (prefers-reduced-motion: reduce) {
    .modal-box { animation: none; }
    .carousel-track { scroll-behavior: auto; }
}

/* ======================================================================
   MOTION & POLISH LAYER (2026) — shared interaction bones.
   The runtime (main.js) toggles .is-visible / .is-scrolled and sets stagger
   indices; these classes give the look. All motion is gated by
   prefers-reduced-motion at the bottom. A site's style.css adds brand flavor
   (timing, distance, accent) on top — never re-implement the mechanism.
   ====================================================================== */

/* Scroll-progress bar — a sleek line at the very top that fills as the page
   scrolls. main.js injects .scroll-progress and drives its width. Brand primary
   with a subtle lighter-toward-the-tip gradient, a soft primary glow and a rounded
   leading cap. Purely decorative and reduced-motion aware (main.js skips it), so it
   never blocks content. Retune or hide it per-site in style.css. */
.scroll-progress {
    position: fixed;
    top: 0; left: 0;
    height: 3px;
    width: 0;
    max-width: 100%;
    border-radius: 0 3px 3px 0;
    background: linear-gradient(90deg,
        var(--color-primary, #0066cc),
        color-mix(in srgb, var(--color-primary, #0066cc) 62%, #fff));
    box-shadow:
        0 0 8px color-mix(in srgb, var(--color-primary, #0066cc) 60%, transparent),
        0 1px 2px color-mix(in srgb, var(--color-primary, #0066cc) 45%, transparent);
    z-index: 200;
    pointer-events: none;
    will-change: width;
}

/* Custom browser scrollbar — sleek, brand-primary. Only on fine pointers (desktop),
   so touch devices keep their clean overlay scrollbars. Firefox uses scrollbar-color;
   WebKit/Blink gets a thin, lightly-rounded thumb flush against the window edge.
   NB: since Chrome 121, setting scrollbar-width/scrollbar-color on an element makes
   Chromium IGNORE every ::-webkit-scrollbar rule for it — so the standard props are
   gated to Firefox (no ::-webkit-scrollbar support) to keep WebKit/Blink on the
   pseudo-element path below (which controls width, radius, and the flush edge). */
@media (pointer: fine) {
    html,
    body {
        scrollbar-color: var(--color-primary, #0066cc) transparent;
        scrollbar-width: thin;
    }

    ::-webkit-scrollbar {
        width: 6px;
        height: 6px;
    }

    ::-webkit-scrollbar-track,
    ::-webkit-scrollbar-track-piece,
    ::-webkit-scrollbar-corner {
        background-color: transparent;
        border: 0;
        box-shadow: none;
    }

    ::-webkit-scrollbar-thumb {
        background: linear-gradient(
            var(--color-primary, #0066cc),
            color-mix(in srgb, var(--color-primary, #0066cc) 78%, #000)
        );
        border: 0;
        border-radius: 0 !important;
        background-clip: padding-box;
    }

    ::-webkit-scrollbar-thumb:hover {
        background: color-mix(
            in srgb,
            var(--color-primary, #0066cc) 85%,
            #000
        );
    }
}

/* Reveal-on-scroll. main.js adds .reveal + --reveal-i, then .is-visible in view.
   The hidden start-state is gated on html.js (set synchronously in <head>) and
   main.js always force-reveals any straggler on load — so content can NEVER stay
   invisible if JS is disabled, errors, or a screenshot tool captures without
   scrolling. Only the animation is progressive; the content is not. */
html.js .reveal {
    opacity: 0;
    transform: translateY(18px);
    transition: opacity .7s cubic-bezier(.16, 1, .3, 1), transform .7s cubic-bezier(.16, 1, .3, 1);
    transition-delay: calc(var(--reveal-i, 0) * 80ms);
    will-change: opacity, transform;
}
html.js .reveal.is-visible { opacity: 1; transform: none; }
/* Direction variants (set data-reveal on an element you tag yourself). */
html.js [data-reveal="left"]  { transform: translateX(-28px); }
html.js [data-reveal="right"] { transform: translateX(28px); }
html.js [data-reveal="scale"] { transform: scale(.94); }
html.js [data-reveal="fade"]  { transform: none; }
html.js [data-reveal].is-visible { transform: none; }

/* Sticky header polish: a soft shadow / tighter feel once the page scrolls. */
.site-header { transition: box-shadow .3s ease, padding .3s ease, background-color .3s ease; }
.site-header.is-scrolled { box-shadow: 0 6px 24px -14px rgba(0, 0, 0, .22); }

/* Media hover-zoom for image-beside-text rows (masked by overflow). Covers the
   split hero and the asymmetric/zig-zag archetypes so the interaction is
   consistent wherever a photo sits beside copy. */
.split .media,
.split-wide .media, .split-narrow .media,
.feature-row .media, .offset-media { overflow: hidden; }
/* Media images compose a hover ZOOM (--mz) and a scroll PARALLAX drift (--my)
   through CSS vars so the two never fight. At rest both are 0 (identity). */
.split .media img, .split > img,
.split-wide .media img, .split-narrow .media img,
.feature-row .media img, .offset-media img {
    transform: scale(calc(1 + var(--mz, 0))) translate3d(0, var(--my, 0), 0);
    transition: transform .8s cubic-bezier(.2, .8, .2, 1);
}
.split .media:hover img, .split > img:hover,
.split-wide .media:hover img, .split-narrow .media:hover img,
.feature-row .media:hover img, .offset-media:hover img { --mz: .05; }

/* Contained parallax: main.js adds .is-parallax to select large editorial media
   and drives --my each frame. The base zoom gives the image overscan room inside
   its clipped, cover-fit frame, so the vertical drift NEVER reveals an edge. A
   quicker transition lets the drift track the scroll instead of easing behind it. */
img.is-parallax { --mz: .14; transition: transform .12s linear; }
img.is-parallax:hover { --mz: .17; }

/* Animated link underline — wipe in on hover/focus. */
.link-underline {
    background-image: linear-gradient(currentColor, currentColor);
    background-size: 0% 2px;
    background-position: 0 100%;
    background-repeat: no-repeat;
    text-decoration: none;
    transition: background-size .3s ease;
}
.link-underline:hover,
.link-underline:focus-visible { background-size: 100% 2px; }
/* Arrow affordance — a trailing icon in a "read more" link glides forward on hover
   (also when the link's card is hovered). A small, consistent premium touch. */
.link-underline .icon { transition: transform .25s cubic-bezier(.2, .8, .2, 1); }
.link-underline:hover .icon,
.link-underline:focus-visible .icon,
a.card:hover .link-underline .icon { transform: translateX(4px); }

/* --- Section tone bands: alternating full-bleed surfaces for rhythm ------
   Put on a .section to give it a distinct, edge-to-edge background (the classic
   v0 "banded" cadence). They self-bleed to the viewport, so keep the inner
   content in a .container. Colors come from tokens, so they read intentionally
   under any palette. Alternate plain / .section-tint / .section-invert down a
   page for depth instead of one flat scroll. */
/* A .section already spans main's full width, so a tone band just PAINTS a
   background — no width:100vw / negative-margin hack (which is what caused the
   sliver of horizontal scroll). Content inside stays in its own .container. */
.section-tint { background: color-mix(in srgb, var(--color-primary, #0066cc) 6%, var(--color-background, #fff)); }
.section-band { background: color-mix(in srgb, var(--color-accent, #33aaff) 8%, var(--color-background, #fff)); }
.section-invert {
    background: var(--color-band-primary, var(--color-primary, #0066cc));
    color: var(--color-on-band-primary, var(--color-on-primary, #fff));
}
.section-invert :is(h1, h2, h3, h4) { color: inherit; }
.section-invert :is(p, .lede, .stat-label) { color: inherit; }
.section-invert .eyebrow { color: color-mix(in srgb, currentColor 90%, transparent); }
.section-invert .card {
    background: color-mix(in srgb, #fff 8%, transparent);
    border-color: color-mix(in srgb, #fff 16%, transparent);
    color: var(--color-on-primary, #fff);
}
.section-invert .stat-num { color: var(--color-on-primary, #fff); }

/* Band-contrast safeguard: --color-muted is gated to pass AA on the PAGE
   background, but a tinted/banded surface shifts that background. Inside a
   tint/band, render secondary text as a fixed fraction of --color-text over
   the band (not the free muted token), so it stays comfortably readable
   whatever the palette while still reading as secondary. (section-invert
   handles its own on-primary text above.) */
.section-tint :is(.lede, .stat-label, .step p, .feature p, .feature-row-body p, .timeline-item p, .prose),
.section-band :is(.lede, .stat-label, .step p, .feature p, .feature-row-body p, .timeline-item p, .prose) {
    color: color-mix(in srgb, var(--color-text, #1a1a1a) 74%, transparent);
}

/* --- Bento grid: mixed-size feature cells (asymmetric, modern) ----------- */
.bento {
    display: grid;
    gap: var(--space);
    grid-template-columns: repeat(6, 1fr);
    grid-auto-rows: minmax(11rem, auto);
}
.bento > * { grid-column: span 2; }              /* default: thirds */
.bento > .bento-wide  { grid-column: span 4; }
.bento > .bento-tall  { grid-row: span 2; }
.bento > .bento-full  { grid-column: span 6; }
/* A bento only reads as designed when its cells carry size modifiers. When the model
   emits all-equal cells with none, the fixed 6-col span leaves orphan empty columns
   (e.g. 4 items → a lone half-width cell + 4 empty columns). With no modifier present,
   degrade to a clean auto-fit grid so every cell fills. */
.bento:not(:has(> :is(.bento-wide, .bento-tall, .bento-full))) {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
    grid-auto-rows: auto;
}
.bento:not(:has(> :is(.bento-wide, .bento-tall, .bento-full))) > * { grid-column: auto; }
@media (max-width: 720px) {
    .bento { grid-template-columns: 1fr; }
    .bento > * { grid-column: auto !important; grid-row: auto !important; }
}

/* --- Marquee: seamless auto-scrolling strip (JS clones the track) -------- */
.marquee { overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
.marquee-track { display: flex; gap: calc(var(--space) * 2.5); width: max-content; animation: marquee 32s linear infinite; }
.marquee:hover .marquee-track { animation-play-state: paused; }
@keyframes marquee { to { transform: translateX(calc(-50% - var(--space) * 1.25)); } }
.eyebrow + .marquee { margin-top: 2rem; }

/* --- Texture utilities: mesh gradient + subtle grain -------------------- */
.gradient-mesh,
[data-options~="background-mesh"] {
    background:
        radial-gradient(60% 60% at 15% 15%, color-mix(in srgb, var(--color-primary, #0066cc) 22%, transparent), transparent 70%),
        radial-gradient(55% 55% at 85% 25%, color-mix(in srgb, var(--color-accent, #33aaff) 22%, transparent), transparent 70%),
        radial-gradient(65% 65% at 50% 100%, color-mix(in srgb, var(--color-primary, #0066cc) 14%, transparent), transparent 70%),
        var(--color-background, #fff);
}
.grain, [data-options~="grain"] { position: relative; isolation: isolate; }
.grain::after, [data-options~="grain"]::after {
    content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; opacity: .5; mix-blend-mode: overlay;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.32'/%3E%3C/svg%3E");
}

/* --- Material textures ([data-options~="texture-*"]) --------------------------
   A tactile SURFACE character (paper / canvas / concrete / carbon) for a band —
   the material cousin of grain/pattern-*. Inspired by transparenttextures.com but
   authored natively (self-contained data-URI / gradient overlays, no external
   requests, resolution-independent) so they tint to any band tone.

   Meant to be applied SPARINGLY — the art-direction pass stamps a look's signature
   texture onto its COLORED bands (tint/surface/contrast/brand) to give a flat fill
   material depth; a plain white section should stay clean. Use ONE surface treatment
   per band: texture-* is an ALTERNATIVE to grain, not a companion.

   The noise-based textures (paper/stucco/concrete/brushed) blend so they read on
   light AND dark bands; the weave-based ones (canvas/linen/carbon/crosshatch/stripes)
   draw from currentColor so they carry the band's own foreground and adapt.

   INTENSITY is a separate axis: --tex-strength scales every texture (1 = default).
   Add data-options="texture-soft" for a whisper or "texture-strong" to let a bold
   look carry more — so the SAME family can read barely-there on luxe and assertive
   on brutalist. Families are ordered subtle → bold roughly:
     linen · paper · canvas · brushed · carbon · stucco · crosshatch · concrete · stripes */
[data-options~="texture-paper"],   [data-options~="texture-stucco"],
[data-options~="texture-concrete"],[data-options~="texture-brushed"],
[data-options~="texture-canvas"],  [data-options~="texture-linen"],
[data-options~="texture-carbon"],  [data-options~="texture-crosshatch"],
[data-options~="texture-stripes"] { position: relative; isolation: isolate; --tex-strength: 1; }
[data-options~="texture-paper"]::after,   [data-options~="texture-stucco"]::after,
[data-options~="texture-concrete"]::after,[data-options~="texture-brushed"]::after,
[data-options~="texture-canvas"]::after,  [data-options~="texture-linen"]::after,
[data-options~="texture-carbon"]::after,  [data-options~="texture-crosshatch"]::after,
[data-options~="texture-stripes"]::after {
    content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; border-radius: inherit;
    background-repeat: repeat;
}
/* Intensity modifiers — scale any family up or down. */
[data-options~="texture-soft"]   { --tex-strength: .58; }
[data-options~="texture-strong"] { --tex-strength: 1.7; }

/* --- Noise-based (blended, tone-agnostic) --- */
/* Paper — fine fibrous grain (printed / editorial feel). */
[data-options~="texture-paper"]::after {
    opacity: calc(var(--tex-strength, 1) * .5); mix-blend-mode: soft-light; background-size: 160px 160px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23p)'/%3E%3C/svg%3E");
}
/* Stucco — soft cloudy plaster mottle (earthy / warm feel). */
[data-options~="texture-stucco"]::after {
    opacity: calc(var(--tex-strength, 1) * .42); mix-blend-mode: soft-light; background-size: 240px 240px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='s'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.05' numOctaves='5' seed='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23s)'/%3E%3C/svg%3E");
}
/* Concrete — coarse mottled stone (raw / industrial feel). */
[data-options~="texture-concrete"]::after {
    opacity: calc(var(--tex-strength, 1) * .4); mix-blend-mode: overlay; background-size: 300px 300px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='c'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.014 0.018' numOctaves='5' seed='8' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23c)'/%3E%3C/svg%3E");
}
/* Brushed — fine directional streaks (brushed metal / premium tech feel). */
[data-options~="texture-brushed"]::after {
    opacity: calc(var(--tex-strength, 1) * .5); mix-blend-mode: soft-light; background-size: 220px 220px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='b'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8 0.012' numOctaves='3' seed='5' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23b)'/%3E%3C/svg%3E");
}

/* --- Weave-based (currentColor, adapts to the band's own foreground) --- */
/* Canvas — orthogonal woven crosshatch (soft / handmade feel). */
[data-options~="texture-canvas"]::after {
    background-image:
        repeating-linear-gradient(0deg,  color-mix(in srgb, currentColor calc(var(--tex-strength, 1) * 8%), transparent) 0 1px, transparent 1px 4px),
        repeating-linear-gradient(90deg, color-mix(in srgb, currentColor calc(var(--tex-strength, 1) * 8%), transparent) 0 1px, transparent 1px 4px);
}
/* Linen — very fine orthogonal weave (quiet, premium fabric). */
[data-options~="texture-linen"]::after {
    background-image:
        repeating-linear-gradient(0deg,  color-mix(in srgb, currentColor calc(var(--tex-strength, 1) * 5%), transparent) 0 1px, transparent 1px 3px),
        repeating-linear-gradient(90deg, color-mix(in srgb, currentColor calc(var(--tex-strength, 1) * 5%), transparent) 0 1px, transparent 1px 3px);
}
/* Carbon — tight diagonal twill (engineered / technical feel). */
[data-options~="texture-carbon"]::after {
    background-image:
        repeating-linear-gradient(45deg,  color-mix(in srgb, currentColor calc(var(--tex-strength, 1) * 7%), transparent) 0 1px, transparent 1px 3px),
        repeating-linear-gradient(-45deg, color-mix(in srgb, currentColor calc(var(--tex-strength, 1) * 7%), transparent) 0 1px, transparent 1px 3px);
}
/* Crosshatch — single-direction etched hatching (editorial ink / print feel). */
[data-options~="texture-crosshatch"]::after {
    background-image:
        repeating-linear-gradient(45deg, color-mix(in srgb, currentColor calc(var(--tex-strength, 1) * 10%), transparent) 0 1px, transparent 1px 6px);
}
/* Stripes — bold wide diagonal bands (playful / assertive; least subtle by default). */
[data-options~="texture-stripes"]::after {
    background-image:
        repeating-linear-gradient(45deg, color-mix(in srgb, currentColor calc(var(--tex-strength, 1) * 9%), transparent) 0 8px, transparent 8px 16px);
}

/* --- Before/after compare slider ([data-compare], JS-driven --compare) --- */
.compare { position: relative; overflow: hidden; border-radius: var(--radius); --compare: 50%; user-select: none; touch-action: none; aspect-ratio: 16 / 10; }
.compare img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The two photos frequently have DIFFERENT intrinsic ratios. Without a fixed frame
   the base image sets an unpredictable height and the absolutely-positioned "after"
   image stretches to match it — so the pair looks scaled/distorted and jumps on
   resize. Pinning .compare to one aspect-ratio and letting BOTH images cover it
   crops them identically and resizes as a single, stable unit. */
.compare > img { width: 100%; height: 100%; }
/* The after layer stays the FULL width of .compare and is revealed by a clip-path
   driven by --compare — so the image itself never changes width (no scale/squish as
   you drag the handle); only the reveal edge moves. (Sizing it to var(--compare)
   would shrink its box and scale the photo with it — the bug that made the two
   sides look different sizes mid-drag.) */
.compare-after { position: absolute; inset: 0; width: 100%; overflow: hidden; clip-path: inset(0 calc(100% - var(--compare)) 0 0); }
.compare-after img { position: absolute; inset: 0; width: 100%; height: 100%; }
.compare-range { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: ew-resize; }
.compare-handle {
    position: absolute; top: 0; bottom: 0; left: var(--compare); width: 2px; transform: translateX(-50%);
    background: #fff; box-shadow: 0 0 0 1px rgba(0, 0, 0, .15); pointer-events: none;
}
.compare-handle::after {
    content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: 2.5rem; height: 2.5rem; border-radius: 999px; background: #fff;
    box-shadow: 0 2px 10px rgba(0, 0, 0, .25);
}

/* --- Photo treatments: brand duotone / tint (cohesion from stock photos) - */
.duotone, .img-tint { position: relative; overflow: hidden; border-radius: var(--radius); isolation: isolate; }
.duotone img, .img-tint img { display: block; width: 100%; height: 100%; object-fit: cover; }
.duotone img { filter: grayscale(1) contrast(1.08); }
/* Overlays are purely decorative — never let them intercept pointer events, or
   they swallow clicks meant for the image/link beneath (incl. the editor's
   element picker). */
.duotone::before { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; mix-blend-mode: multiply; background: var(--color-primary, #0066cc); }
/* Lighter wash — keeps photo detail while unifying it to the palette. */
.img-tint::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: color-mix(in srgb, var(--color-primary, #0066cc) 30%, transparent); mix-blend-mode: multiply; }

/* SITE-WIDE PHOTO COHESION — the art-direction pass tags <main> with the site's chosen
   photo grade (ad-duotone / ad-tint) so EVERY content photo shares one treatment and
   the imagery reads as a single art-directed set, not a random stock collage. It reaches
   bare <img> (which .duotone/.img-tint, being wrapper effects, can't). A wrapped
   .duotone/.img-tint still layers its branded color overlay ON TOP of this grade.
   Opt an image out with class="no-treat" (logos, diagrams, screenshots that need
   their real colors); the header/footer are outside <main> so logos there are safe. */
main.ad-duotone :is(img, .media):not(.no-treat) { filter: grayscale(1) contrast(1.04); }
main.ad-tint    :is(img, .media):not(.no-treat) { filter: saturate(.86) contrast(1.02); }

/* --- 2026 reference components ------------------------------------------------
   Distilled from a set of high-end business / SaaS / agency sites so every look
   can reach for the same current vocabulary: a chip kicker, a soft brand "aura"
   glow behind product/hero imagery, a device frame, a stat pinned on a photo, and
   a two-tone headline highlight. All token-driven, so they inherit each brand. */

/* Chip kicker — a rounded label pill in place of the plain .eyebrow (add
   class="eyebrow chip-kicker"; optionally a leading <span class="dot"> for an
   availability/status cue like "Accepting projects"). */
.eyebrow.chip-kicker {
    display: inline-flex; align-items: center; gap: .5em;
    /* Hug the text — a .section-head is a column flex (align-items:stretch), which
       would otherwise blow this pill out to the full container width. fit-content
       pins it to its content; the head's align-items (start by default, center when
       the section is centered) then places the pill correctly with no override. */
    width: fit-content;
    padding: .42em .9em .2em; border-radius: 999px;
    background: color-mix(in srgb, var(--color-primary, #0066cc) 8%, var(--color-background, #fff));
    border: 1px solid color-mix(in srgb, var(--color-primary, #0066cc) 16%, transparent);
    color: var(--color-accent-text, var(--color-primary, #0066cc));
    letter-spacing: .04em;
}
.eyebrow.chip-kicker::after { display: none; }
/* A chip-kicker is width:fit-content, so text-align can't centre it — when the chip
   itself is told to centre (its own .text-center / data-options="text-center", as in
   a centered band with no .section-head wrapper), centre the BOX. Without this the
   pill strands at the left under a centered heading (the "centered title, left
   eyebrow" tell). Inside a centered .section-head, align-items already handles it. */
.eyebrow.chip-kicker:is(.text-center, [data-options~="text-center"]) { margin-inline: auto; }
/* On a colored/dark band the chip-kicker's light fill + brand-tinted text washes out to
   a near-invisible pale-on-pale pill. Re-derive it from the band's own foreground: a
   translucent-white fill, matching border, and inherited (white) text — legible on any
   band. */
:is(.scrim, .section-invert,
    [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"],
    [data-options~="background-photo"]) .eyebrow.chip-kicker {
    background: color-mix(in srgb, currentColor 14%, transparent);
    border-color: color-mix(in srgb, currentColor 32%, transparent);
    color: inherit;
}
.chip-kicker .dot {
    width: .5em; height: .5em; border-radius: 999px; flex: none;
    margin: 0 5px 0 0;
    background: var(--color-accent, #16a34a);
    box-shadow: 0 0 0 .28em color-mix(in srgb, var(--color-accent, #16a34a) 25%, transparent);
}

/* Aura — a soft, blurred brand glow behind an element (a product screenshot, a
   hero image, a stat band). Purely decorative; sits behind the content. The
   signature 2026 "cloud" under an app shot. */
.aura { position: relative; isolation: isolate; }
.aura::before {
    content: ""; position: absolute; z-index: -1; inset: -10% -8%;
    background:
        radial-gradient(45% 55% at 28% 32%, color-mix(in srgb, var(--color-primary, #0066cc) 55%, transparent), transparent 70%),
        radial-gradient(50% 55% at 75% 58%, color-mix(in srgb, var(--color-accent, #33aaff) 45%, transparent), transparent 70%);
    filter: blur(50px); opacity: .7; pointer-events: none;
}

/* Device — wrap a product screenshot so it reads as a real app window: rounded,
   bordered, lifted. Pair with .aura for the floating-app hero. */
.device {
    border-radius: calc(var(--radius, 8px) + 6px);
    border: 1px solid var(--border);
    background: var(--color-background, #fff);
    box-shadow: var(--shadow-lg, 0 30px 60px -20px rgba(0, 0, 0, .25));
    overflow: hidden; isolation: isolate;
}
.device img { display: block; width: 100%; height: auto; }

/* Stat on a photo — a frosted figure pinned to a media corner (a ".media"
   wrapper with a child ".media-stat"). One proof number over an image. */
.media:has(> .media-stat) { position: relative; }
.media-stat {
    position: absolute; left: 1rem; bottom: 1rem; z-index: 3;
    padding: .75rem 1.05rem; border-radius: var(--radius, 8px);
    background: color-mix(in srgb, #000 42%, transparent);
    -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
    color: #fff; border: 1px solid rgba(255, 255, 255, .18);
}
.media-stat .stat-num { display: block; font-size: 1.8rem; line-height: 1; }
.media-stat .stat-label { font-size: .78rem; opacity: .85; }

/* Two-tone headline — <span class="hl"> inside a display/heading paints one word
   in the brand accent (the "one word in color" emphasis the references favor).
   Neutralized on colored/photo surfaces, where the head is already inverted. */
:is(.display, .display-xl, h1, h2, h3) .hl { color: var(--color-accent-text, var(--color-accent, var(--color-primary, #0066cc))); }
:is([data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"],
    .scrim, [data-options~="background-photo"], .section-invert) :is(.display, .display-xl, h1, h2, h3) .hl { color: inherit; }

/* --- Business components (team, checklist, contact, embed, announcement) ------
   Common on professional business sites; token-driven so they inherit each brand. */

/* Team / people grid — headshot + name + role (+ optional bio & links). */
/* auto-fit + 1fr makes a lone last-row member stretch to the full row width — its
   4/5 media then balloons to a huge portrait next to normal-sized ones. Cap the
   column (18rem) and center the tracks so every member keeps the same width → the
   media keeps a uniform height regardless of how the count fills the last row. */
.team { display: grid; gap: calc(var(--space) * 1.5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 18rem)); justify-content: center; }
.member { display: grid; gap: .35rem; }
.member img, .member .media { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; border-radius: var(--radius); background: var(--surface-2); margin-bottom: .5rem; }
/* When a .media wraps a real headshot, the image often carries its OWN ratio (the
   img() helper stamps e.g. 1x1 inline). Left alone, a square image inside the 4/5
   media box leaves a gray strip of the box's background below it. Make the box the
   frame and let the image FILL it (cropped to 4/5), so every headshot is uniform
   with no exposed background bar — regardless of the image's intrinsic ratio. */
.member .media { position: relative; overflow: hidden; }
.member .media > :is(img, picture, figure) {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; aspect-ratio: auto; border-radius: 0; margin: 0;
}
/* An empty .media (no headshot supplied — we never fabricate faces) should read as a
   deliberate avatar placeholder, not a broken gradient box: a soft brand-tinted panel
   with a centered person glyph. */
.member .media:empty {
    display: grid; place-items: center;
    background:
        radial-gradient(120% 90% at 50% -10%, color-mix(in srgb, var(--color-primary, #0066cc) 12%, var(--surface-2)) 0%, var(--surface-2) 70%);
}
.member .media:empty::after {
    content: ""; width: 42%; aspect-ratio: 1;
    background: color-mix(in srgb, var(--color-primary, #0066cc) 30%, var(--color-muted, #8a8f98));
    -webkit-mask: center/contain no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5Zm0 2c-4.4 0-8 2.5-8 5.6V22h16v-2.4c0-3.1-3.6-5.6-8-5.6Z'/%3E%3C/svg%3E");
            mask: center/contain no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5Zm0 2c-4.4 0-8 2.5-8 5.6V22h16v-2.4c0-3.1-3.6-5.6-8-5.6Z'/%3E%3C/svg%3E");
}
.member h3 { margin: 0; font-size: 1.05rem; }
.member .role { color: var(--color-accent-text, var(--color-primary, #0066cc)); font-size: .9rem; font-weight: 600; }
.member p { color: var(--color-muted, #555); font-size: .92rem; margin: .15rem 0 0; }
.member .socials { display: flex; gap: .6rem; margin-top: .35rem; }
.member .socials a { color: var(--color-muted, #666); }
.member .socials a:hover { color: var(--color-primary, #0066cc); }

/* Checklist — a scannable list of included features / benefits, each with a check. */
.checklist { display: grid; gap: .75rem; list-style: none; padding: 0; margin: 0; }
.checklist.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: calc(var(--space) * 1.5); }
/* Hanging indent, NOT a 2-column grid: an item is often inline content (a <strong>
   label followed by a text node), which a grid would scatter into separate cells —
   the check in col 1, the label in col 2, the trailing text on a new row. Absolutely
   positioning the marker lets any inline content flow naturally in the padded gutter. */
.checklist li { position: relative; padding-left: 2.3rem; min-height: 1.5rem; }
.checklist li::before {
    content: "\2713"; position: absolute; left: 0; top: .1rem; display: grid; place-items: center;
    width: 1.5rem; height: 1.5rem; border-radius: 999px; font-size: .82rem; font-weight: 700; line-height: 1;
    background: color-mix(in srgb, var(--color-primary, #0066cc) 14%, transparent);
    color: var(--color-accent-text, var(--color-primary, #0066cc));
}
/* If a checklist item supplies its OWN check mark (an icon/svg/image), suppress the
   base ::before check so the item doesn't show two ticks side by side. */
.checklist li:has(> svg, > .icon, > img, > .service-icon) { padding-left: 0; }
.checklist li:has(> svg, > .icon, > img, > .service-icon)::before { content: none; }
/* A list (checklist, or a plain ul/ol in a hand-built text column) that is FOLLOWED
   by more copy or a CTA should not butt straight against it. The flow rhythm spaces
   generic block siblings, but a list frequently sits as a loose sibling outside a
   .flow/.prose column; give whatever follows any list clear separation regardless. */
:is(.checklist, .flow > ul, .flow > ol, .prose > ul, .prose > ol) + :not(ul):not(ol) { margin-top: calc(var(--space) * 1.25); }

/* Contact panel — labelled info rows (icon + label + value) for address / hours /
   phone / email, to sit beside a form or map. */
.contact-panel { display: grid; gap: var(--space); }
.contact-item { display: grid; grid-template-columns: auto 1fr; gap: .8rem; align-items: start; }
.contact-item .icon { color: var(--color-primary, #0066cc); margin-top: .1rem; }
.contact-item .label { display: block; font-size: .74rem; text-transform: uppercase; letter-spacing: .05em; color: var(--color-muted, #667); }
.contact-item .value { display: block; font-weight: 500; }
.contact-item .value a { color: inherit; }
/* Robustness: the model often writes icon + label + value as three FLAT children
   (no wrapper), which a 2-col grid scatters (value lands under the icon). Pin the
   text parts to column 2 so they always stack beside the icon; and when there's no
   icon at all, drop the icon column so label/value don't sit side-by-side. */
.contact-item :is(.label, .value) { grid-column: 2; }
.contact-item:not(:has(> .icon, > svg, > img)) { grid-template-columns: 1fr; }
.contact-item:not(:has(> .icon, > svg, > img)) :is(.label, .value) { grid-column: 1; }

/* Embed — a responsive frame for third-party embeds (form, map, calendar, video).
   .embed keeps a 16:9 iframe fluid; .embed-slot is a labelled placeholder the
   operator drops an embed into. */
.embed { position: relative; width: 100%; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border); background: var(--surface); }
.embed > iframe, .embed > video { display: block; width: 100%; border: 0; }
.embed[data-ratio] { aspect-ratio: var(--embed-ratio, 16 / 9); }
.embed[data-ratio] > iframe { position: absolute; inset: 0; height: 100%; }
.embed-slot { min-height: 20rem; border-radius: var(--radius); border: 1px dashed color-mix(in srgb, var(--color-text, #111) 22%, transparent); display: grid; place-items: center; text-align: center; padding: var(--space); color: var(--color-muted, #667); background: var(--surface); }
/* Settings embeds: a pasted third-party form or map from [contact-form]/[newsletter]/[map]
   (also swapped in for the template's demo forms). The provider's markup drives its own
   height, so allow it to grow (no fixed ratio). */
.embed-form, .embed-map { min-height: 3rem; }
.embed-map { min-height: 18rem; }
.embed-form > iframe, .embed-map > iframe { min-height: inherit; }
/* Deferred embeds (site_lazy_embeds → base.js): the parked iframe holds the layout
   while it waits for its src, and stays see-through so it never flashes a white
   panel over a dark band. With JS off it's hidden outright and the <noscript> twin
   rendered beside it takes over, so a pasted embed always works either way. */
iframe[data-lazy-embed]:not([src]) { background: transparent; }
html:not(.js) iframe[data-lazy-embed] { display: none; }
/* Social profile row from the [social] shortcode. */
.social-row { display: flex; flex-wrap: wrap; gap: .5rem .9rem; align-items: center; }
.social-link { font-size: .9rem; text-decoration: none; color: inherit; opacity: .8; transition: opacity .2s; }
.social-link:hover { opacity: 1; text-decoration: underline; text-underline-offset: 2px; }

/* Announcement bar — a slim full-width strip above the header (promo / notice /
   phone). Rendered by the header when a site sets an announcement; keep it short. */
.announcement { background: var(--color-band-primary, var(--color-primary, #0066cc)); color: var(--color-on-band-primary, var(--color-on-primary, #fff)); text-align: center; font-size: .88rem; line-height: 1.35; padding: .55rem 1rem; }
.announcement a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* Utility ribbon — a thin bar at the very top (above the header): a short message on one
   side, small links (hours / service area / careers / a tel: number / social) on the
   other. Brand-tinted; scrolls away above the sticky header. A site's style.css can
   restyle it (e.g. a dark or neutral ribbon). */
.ribbon { background: var(--color-band-primary, var(--color-primary, #0066cc)); color: var(--color-on-band-primary, var(--color-on-primary, #fff)); font-size: .82rem; line-height: 1.3; }
.ribbon-inner { max-width: var(--container); margin-inline: auto; padding: .45rem 1.5rem; display: flex; align-items: center; justify-content: flex-end; gap: 1rem 1.5rem; flex-wrap: wrap; }
.ribbon-msg { opacity: .95; }
.ribbon-msg a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.ribbon-links { display: flex; align-items: center; gap: 1.15rem; margin-left: auto; }
.ribbon-links a { color: inherit; text-decoration: none; opacity: .9; white-space: nowrap; transition: opacity .2s; }
.ribbon-links a:hover { opacity: 1; text-decoration: underline; text-underline-offset: 2px; }
@media (max-width: 640px) {
    .ribbon-inner { justify-content: center; }
    .ribbon-msg { flex-basis: 100%; text-align: center; }
    .ribbon-links { margin-inline: auto; }
}

/* --- Background patterns (subtle, brand-tinted texture for a band) ------- */
.pattern-dots, [data-options~="pattern-dots"] { background-image: radial-gradient(color-mix(in srgb, var(--color-text, #1a1a1a) 14%, transparent) 1.5px, transparent 1.5px); background-size: 22px 22px; }
.pattern-grid, [data-options~="pattern-grid"] { background-image: linear-gradient(color-mix(in srgb, var(--color-text, #1a1a1a) 8%, transparent) 1px, transparent 1px), linear-gradient(90deg, color-mix(in srgb, var(--color-text, #1a1a1a) 8%, transparent) 1px, transparent 1px); background-size: 28px 28px; }
.pattern-waves, [data-options~="pattern-waves"] { background-image: radial-gradient(120% 60% at 50% 120%, color-mix(in srgb, var(--color-primary, #0066cc) 12%, transparent), transparent 70%); }

/* --- Timeline (vertical, dotted spine) ---------------------------------- */
.timeline { display: grid; }
.timeline-item { position: relative; padding: 0 0 calc(var(--space) * 2) calc(var(--space) * 2.25); border-left: 2px solid color-mix(in srgb, var(--color-primary, #0066cc) 25%, transparent); }
.timeline-item:last-child { border-left-color: transparent; padding-bottom: 0; }
.timeline-item::before { content: ""; position: absolute; left: -0.5rem; top: .15rem; width: .9rem; height: .9rem; border-radius: 999px; background: var(--color-primary, #0066cc); box-shadow: 0 0 0 4px var(--color-background, #fff); }
.timeline-item h3 { margin: 0 0 .25rem; }
.timeline-item p { margin: 0; color: var(--color-muted, #555); }
.timeline-date { font-size: .85rem; font-weight: 600; color: var(--color-accent-text, var(--color-accent, var(--color-primary, #0066cc))); }

/* --- RICH SECTION BLOCKS (2026 expansion) ------------------------------------
   A wider vocabulary of premium section compositions so builds differ page-to-page
   and site-to-site. All token-driven + band-aware like the rest. Deliberately
   GENERIC/multi-purpose — the names describe SHAPE, not a single content label:
     .accolades  — a badge/label row: awards · certifications · press · values · facts
     .metric-split — one oversized number beside supporting copy
     .spotlight  — a large single-subject showcase (feature · case study · result)
     .resource-list — download/guide/doc/resource cards
     .listing    — stacked rows: open roles · programs · directory · service lines
     .callout-band — a slim in-page promo/notice/highlight band
     .logo-grid  — a bordered logo/integration grid (vs the animated .marquee)
     .price-toggle — a monthly/annual switch (main.js flips every .price) */

/* Accolades — a centered wrap-row of small badge cards. Each .accolade is a figure
   (an icon/logo/emoji or a short number) + a title (+ optional meta). Multi-purpose:
   award badges, certifications, "as seen in" press, core values, quick facts, seals. */
.accolades { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space); }
.accolade {
    flex: 0 1 auto; display: grid; gap: .35rem; justify-items: center; text-align: center;
    min-width: min(100%, 9.5rem); padding: calc(var(--space) * 1.15) calc(var(--space) * 1.35);
    border-radius: var(--radius); border: 1px solid var(--border); background: var(--surface);
}
.accolade .accolade-figure { font-size: 1.9rem; line-height: 1; color: var(--color-accent-text, var(--color-primary, #0066cc)); }
/* If a build omits the icon/figure, don't reserve its space (avoids a top gap). */
.accolade .accolade-figure:empty { display: none; }
.accolade .accolade-figure :is(img, svg) { width: 2.4rem; height: 2.4rem; object-fit: contain; }
.accolade .accolade-title { font-weight: 600; font-family: var(--font-heading, inherit); line-height: 1.2; }
.accolade .accolade-meta { font-size: .82rem; color: var(--color-muted, #667); }
:is(.scrim, .section-invert, [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"], [data-options~="background-photo"]) .accolade {
    background: color-mix(in srgb, currentColor 8%, transparent);
    border-color: color-mix(in srgb, currentColor 22%, transparent);
}

/* Metric split — one OVERSIZED figure beside a supporting copy column. An editorial
   alternative to a stat grid: a single headline number that carries the section.
   Reuses .stat-num/data-count so the figure counts up on view. */
.metric-split { display: grid; gap: calc(var(--space) * 1.5); align-items: center; grid-template-columns: 1fr; }
@media (min-width: 48rem) { .metric-split { grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); gap: calc(var(--space) * 3); } }
.metric-split .metric-figure, .metric-split .metric-figure .stat-num {
    font-family: var(--font-heading, inherit); font-weight: 800; line-height: .92; letter-spacing: -0.03em;
    font-size: clamp(3.2rem, 1.4rem + 7vw, 6.5rem); color: var(--color-primary, #0066cc);
}
/* Mute only the PARAGRAPH copy — NOT the whole column. Setting color on .metric-cap
   itself makes the heading (which on a colored band is set to color:inherit) inherit
   this muted grey instead of the band's foreground → a low-contrast headline on a
   dark band. Scoping to <p> keeps the heading bright. */
.metric-split .metric-cap p { color: var(--color-muted, #555); }
:is([data-options~="background-primary"], [data-options~="background-accent"], [data-options~="background-contrast"],
    [data-options~="background-gradient"], .scrim, .section-invert) .metric-split .metric-cap p { color: inherit; }
:is([data-options~="background-primary"], [data-options~="background-accent"], [data-options~="background-contrast"],
    [data-options~="background-gradient"], .scrim, .section-invert) .metric-split :is(.metric-figure, .metric-figure .stat-num) { color: inherit; }

/* Spotlight — a large single-subject showcase. Built on a .split-wide inside (so it
   inherits the two-column responsive/reverse behavior + fragility guards); this
   marker just gives that split extra breathing room and an optional sticky media
   column while the copy reads. Multi-purpose: hero feature, case study, featured
   project/result. Pin the media with data-options="sticky-media" on the .spotlight. */
.spotlight .split-wide, .spotlight .split-narrow { gap: clamp(calc(var(--space) * 2), 5vw, calc(var(--space) * 4)); }
@media (min-width: 62rem) {
    .spotlight[data-options~="sticky-media"] .media { position: sticky; top: calc(var(--space) * 5); align-self: start; }
}

/* Resource list — cards for downloads / guides / docs / resources: an icon chip, a
   title, a line of meta, and an open/download link. A robust vertical card (like
   .feature-cards) so flat children never scatter. Multi-purpose. */
.resource-list { display: grid; gap: var(--space); grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); }
.resource { display: grid; gap: .5rem; align-content: start; padding: calc(var(--space) * 1.35); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
.resource .resource-icon { display: inline-grid; place-items: center; width: 2.6rem; height: 2.6rem; border-radius: var(--radius); background: color-mix(in srgb, var(--color-primary, #0066cc) 12%, transparent); color: var(--color-accent-text, var(--color-primary, #0066cc)); }
.resource .resource-icon :is(svg, img) { width: 1.4rem; height: 1.4rem; }
.resource h3 { margin: 0; font-size: 1.05rem; }
.resource .resource-meta { color: var(--color-muted, #667); font-size: .85rem; }
.resource .resource-link { font-weight: 600; }
:is(.scrim, .section-invert, [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"], [data-options~="background-photo"]) .resource {
    background: color-mix(in srgb, currentColor 8%, transparent); border-color: color-mix(in srgb, currentColor 22%, transparent);
}

/* Listing — stacked full-width rows for structured items. Each .listing-row is a
   title + meta (+ optional .listing-tag) and a trailing .listing-action; the whole
   row reads as one card (can be a card-as-link). Multi-purpose: open roles, programs,
   a directory, service lines, a changelog. */
.listing { display: grid; gap: .75rem; }
.listing-row { display: grid; grid-template-columns: 1fr auto; gap: .3rem 1.2rem; align-items: center; padding: calc(var(--space) * 1.15) calc(var(--space) * 1.4); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
.listing-row h3 { margin: 0; font-size: 1.08rem; grid-column: 1; }
.listing-row .listing-meta { grid-column: 1; display: flex; flex-wrap: wrap; gap: .3rem .9rem; color: var(--color-muted, #667); font-size: .88rem; }
.listing-row .listing-action { grid-column: 2; grid-row: 1 / span 2; align-self: center; justify-self: end; white-space: nowrap; }
.listing-tag { display: inline-block; padding: .12rem .55rem; border-radius: 999px; font-size: .72rem; font-weight: 600; letter-spacing: .03em; background: color-mix(in srgb, var(--color-primary, #0066cc) 12%, transparent); color: var(--color-accent-text, var(--color-primary, #0066cc)); }
@media (max-width: 40rem) {
    .listing-row { grid-template-columns: 1fr; }
    .listing-row .listing-action { grid-column: 1; grid-row: auto; justify-self: start; margin-top: .4rem; }
}
:is(.scrim, .section-invert, [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"], [data-options~="background-photo"]) .listing-row {
    background: color-mix(in srgb, currentColor 8%, transparent); border-color: color-mix(in srgb, currentColor 22%, transparent);
}

/* Callout band — a slim horizontal in-page band: a short message + one action, side
   by side (a mid-page promo / notice / highlight, lighter than a full .cta section).
   Gets a surface panel by default; inside a colored/photo band it dissolves and
   inherits the band's foreground. Multi-purpose. */
.callout-band { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space) calc(var(--space) * 1.5); padding: calc(var(--space) * 1.4) calc(var(--space) * 1.75); border-radius: var(--radius-lg, 14px); background: var(--surface-2); border: 1px solid var(--border); }
/* align-content:start pins the copy rows to the top. Without it, when a per-site
   style.css gives .callout-copy a tall flex-basis (e.g. `flex:1 1 24rem` on a
   text-center band that stacks the band into a column), the grid's default
   stretch balloons every auto row to fill that height — inflating the chip-kicker
   pill into a giant circle and scattering the copy. start keeps rows content-sized. */
.callout-band .callout-copy { display: grid; gap: .25rem; align-content: start; }
/* Belt-and-suspenders: a pill must never stretch to its grid cell's height. */
.callout-band .eyebrow.chip-kicker { align-self: start; }
/* A centered callout stacks into a column; center its copy items (incl. the
   fit-content chip) instead of stranding them at the left. */
.callout-band[data-options~="text-center"] .callout-copy,
.callout-band.text-center .callout-copy { justify-items: center; }
.callout-band .callout-copy :is(h2, h3) { margin: 0; }
.callout-band .callout-copy p { margin: 0; color: var(--color-muted, #555); }
.callout-band .callout-actions { display: flex; flex-wrap: wrap; gap: .6rem; }
:is(.scrim, .section-invert, [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"], [data-options~="background-photo"]) .callout-band {
    background: color-mix(in srgb, currentColor 10%, transparent); border-color: color-mix(in srgb, currentColor 24%, transparent);
}
:is(.scrim, .section-invert, [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"], [data-options~="background-photo"]) .callout-band .callout-copy p { color: inherit; opacity: .9; }

/* Logo/integration grid — a BORDERED cell grid of partner/integration/client marks,
   a structured alternative to the animated .marquee. Each .logo-cell frames one mark;
   an optional .logo-cell-label names it (great for an integrations directory). */
/* auto-FILL (not auto-fit): a partial last row keeps its cells at the SAME width
   as the rows above and leaves the empty tracks blank, instead of stretching a
   lone trailing cell across the row (the recurring bordered-grid "4 then 2 giant
   cells" tell). The :has() count rules below refine common counts to balanced
   grids and — being higher-specificity than a plain `.logo-grid` — also override
   a per-site style.css that re-declares this with auto-fit. */
.logo-grid { display: grid; gap: 0; grid-template-columns: repeat(auto-fill, minmax(min(50%, 11rem), 1fr)); overflow: hidden; }
/* Balanced columns so the cell count divides evenly — no orphan row. Scoped to a
   min-width so phones still stack 1–2 up via the auto-fill default above. */
@media (min-width: 48rem) {
    .logo-grid:has(> .logo-cell:nth-child(4):last-child)  { grid-template-columns: repeat(2, 1fr); } /* 4  → 2×2 */
    .logo-grid:has(> .logo-cell:nth-child(6):last-child)  { grid-template-columns: repeat(3, 1fr); } /* 6  → 3×2 */
    .logo-grid:has(> .logo-cell:nth-child(8):last-child)  { grid-template-columns: repeat(4, 1fr); } /* 8  → 4×2 */
    .logo-grid:has(> .logo-cell:nth-child(9):last-child)  { grid-template-columns: repeat(3, 1fr); } /* 9  → 3×3 */
    .logo-grid:has(> .logo-cell:nth-child(10):last-child) { grid-template-columns: repeat(5, 1fr); } /* 10 → 5×2 */
    .logo-grid:has(> .logo-cell:nth-child(12):last-child) { grid-template-columns: repeat(4, 1fr); } /* 12 → 4×3 */
}
.logo-cell { display: grid; place-items: center; gap: .4rem; text-align: center; min-height: 6rem; padding: calc(var(--space) * 1.25); border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.logo-cell img { max-height: 2.25rem; width: auto; opacity: .7; filter: grayscale(1); transition: opacity .25s, filter .25s; }
.logo-cell:hover img { opacity: 1; filter: none; }
.logo-cell-label { font-size: .82rem; color: var(--color-muted, #667); }

/* Pricing toggle — a monthly/annual switch. main.js flips every .price in the
   section between its data-monthly and data-annual text via [data-price-toggle].
   Progressive enhancement: no JS ⇒ the monthly price (element text) stays. */
.price-toggle { display: inline-flex; align-items: center; gap: .6rem; justify-content: center; font-size: .92rem; color: var(--color-muted, #555); }
.price-toggle .switch { appearance: none; cursor: pointer; position: relative; width: 3rem; height: 1.6rem; border-radius: 999px; border: 1px solid var(--border); background: var(--surface-2); transition: background .2s ease; }
.price-toggle .switch::after { content: ""; position: absolute; top: 50%; left: .2rem; transform: translateY(-50%); width: 1.15rem; height: 1.15rem; border-radius: 999px; background: var(--color-background, #fff); box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, .15)); transition: left .2s ease; }
.price-toggle .switch[aria-checked="true"] { background: var(--color-primary, #0066cc); }
.price-toggle .switch[aria-checked="true"]::after { left: calc(100% - 1.35rem); }
.price-toggle .switch:focus-visible { outline: 2px solid var(--color-primary, #0066cc); outline-offset: 2px; }
.price-toggle .is-current { font-weight: 700; color: var(--color-text, #14161c); }
.price-save { font-size: .78rem; font-weight: 600; padding: .1rem .5rem; border-radius: 999px; background: color-mix(in srgb, var(--color-accent, #16a34a) 16%, transparent); color: var(--color-accent-text, var(--color-accent, #16a34a)); }

/* Two-panel comparison — a NARRATIVE side-by-side (old way / new way, problem /
   solution, us / them), vs the tabular .comparison matrix. Tag the weak side
   .is-con (muted, dash markers) and the strong side .is-pro (brand, check markers).
   MULTI-PURPOSE. */
.compare-cols { display: grid; gap: var(--space); grid-template-columns: 1fr; align-items: stretch; }
@media (min-width: 44rem) { .compare-cols { grid-template-columns: 1fr 1fr; gap: calc(var(--space) * 1.5); } }
.compare-col { display: grid; gap: .85rem; align-content: start; padding: calc(var(--space) * 1.75); border: 1px solid var(--border); border-radius: var(--radius-lg, 14px); background: var(--surface); }
.compare-col h3 { margin: 0; }
.compare-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .6rem; }
.compare-col li { position: relative; padding-left: 1.9rem; color: var(--color-muted, #555); }
.compare-col li::before { content: ""; position: absolute; left: 0; top: .1rem; width: 1.3rem; height: 1.3rem; border-radius: 999px; display: grid; place-items: center; font-size: .8rem; font-weight: 700; line-height: 1; }
.compare-col.is-con { background: var(--surface-2); }
.compare-col.is-con li::before { content: "\2013"; color: var(--color-muted, #99a); background: color-mix(in srgb, var(--color-text, #111) 8%, transparent); }
.compare-col.is-pro { border-color: color-mix(in srgb, var(--color-primary, #0066cc) 40%, transparent); box-shadow: var(--shadow); }
.compare-col.is-pro li { color: var(--color-text, #1a1a1a); }
.compare-col.is-pro li::before { content: "\2713"; color: var(--color-accent-text, var(--color-primary, #0066cc)); background: color-mix(in srgb, var(--color-primary, #0066cc) 14%, transparent); }

/* Inline signup — a compact one-row email capture (newsletter / waitlist / early
   access / book-a-demo). Reuses the .field/.btn form primitives; this just lays them
   on one line and stacks on narrow screens. Demo forms are intercepted by main.js —
   swap for a real embed before launch. MULTI-PURPOSE. */
/* align-items:end so the button bottom-aligns with the INPUT, not floated up to
   the top of a field that carries a label above its input (the "button stranded
   above the field" look). */
.signup-form { display: flex; flex-wrap: wrap; gap: .6rem; align-items: end; }
.signup-form .field { flex: 1 1 14rem; margin-bottom: 0; }
.signup-form .btn { flex: 0 0 auto; }
.signup-form .form-demo-note { flex-basis: 100%; }

/* Single-offer pricing — one plan / quote panel instead of a tier row (for the many
   service businesses with a single package or a "custom quote"). Two columns on
   desktop: the summary + price + CTA beside the included list. */
.price-solo { display: grid; gap: calc(var(--space) * 1.5); align-content: start; padding: calc(var(--space) * 2); border: 1px solid var(--border); border-radius: var(--radius-lg, 14px); background: var(--surface); grid-template-columns: 1fr; }
@media (min-width: 46rem) { .price-solo { grid-template-columns: 1fr 1.1fr; align-items: center; column-gap: calc(var(--space) * 2.5); } }
.price-solo .price { font-family: var(--font-heading, inherit); font-size: clamp(2.2rem, 1.5rem + 2.5vw, 3.25rem); font-weight: 800; line-height: 1; letter-spacing: -0.02em; }
.price-solo .price small { font-size: .9rem; font-weight: 500; color: var(--color-muted, #555); }

/* Testimonial wall — a dense "wall of love": many short quotes flowed into masonry
   columns so cards of different heights pack naturally. Holds existing .testimonial
   cards. MULTI-PURPOSE: reviews, press one-liners, social shout-outs. */
.testimonial-wall { columns: 1; column-gap: var(--space); }
@media (min-width: 40rem) { .testimonial-wall { columns: 2; } }
@media (min-width: 60rem) { .testimonial-wall { columns: 3; } }
.testimonial-wall > * { break-inside: avoid; margin-bottom: var(--space); }

/* Meters — labeled progress / percentage bars for capability, capacity, coverage or
   goal progress. Set the fill via style="--val:82%" on .meter-fill (a custom
   property, not a width literal). MULTI-PURPOSE; use only for real 0–100% values. */
.meters { display: grid; gap: calc(var(--space) * 1.1); }
.meter { display: grid; gap: .4rem; }
.meter-head { display: flex; justify-content: space-between; gap: 1rem; align-items: baseline; }
.meter-label { font-weight: 600; }
.meter-value { color: var(--color-muted, #667); font-variant-numeric: tabular-nums; }
.meter-track { height: .6rem; border-radius: 999px; background: color-mix(in srgb, var(--color-text, #111) 10%, transparent); overflow: hidden; }
.meter-fill { display: block; height: 100%; width: var(--val, 0%); border-radius: inherit; background: linear-gradient(90deg, var(--color-primary, #0066cc), var(--color-accent, #33aaff)); }
:is(.scrim, .section-invert, [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"]) .meter-track { background: color-mix(in srgb, currentColor 20%, transparent); }
:is(.scrim, .section-invert, [data-options~="background-primary"], [data-options~="background-accent"],
    [data-options~="background-contrast"], [data-options~="background-gradient"]) .meter-fill { background: currentColor; }

/* --- Comparison / data table -------------------------------------------- */
.table-wrap { overflow-x: auto; }
/* A verdict line / CTA (or anything) placed right after a table hugs its last row.
   Give whatever follows the scroll wrapper real separation. */
.table-wrap + * { margin-top: calc(var(--space) * 2.25); }
.data-table, .comparison { width: 100%; border-collapse: collapse; }
.data-table th, .data-table td, .comparison th, .comparison td { padding: calc(var(--space) * .7) var(--space); text-align: left; border-bottom: 1px solid rgba(0, 0, 0, .08); }
.data-table thead th, .comparison thead th { font-family: var(--font-heading, inherit); font-size: .92rem; letter-spacing: .01em; }
.comparison th:not(:first-child), .comparison td:not(:first-child) { text-align: center; }
.data-table tbody tr:hover, .comparison tbody tr:hover { background: color-mix(in srgb, var(--color-primary, #0066cc) 4%, transparent); }
/* Comparison MATRIX cells — a feature/plan/option/us-vs-them grid. A yes/no cell can
   carry an icon (icon('check')) or the .is-yes/.is-no helper, which draws its own
   glyph so the model needn't. Highlight one column (your plan / your offer) with
   .is-featured on its <th>+<td>s to make it the visual answer. Multi-purpose. */
.comparison .is-yes, .comparison .is-no { font-weight: 600; }
.comparison td.is-yes::before { content: "\2713"; color: var(--color-accent-text, var(--color-primary, #0066cc)); }
.comparison td.is-no::before { content: "\2013"; color: var(--color-muted, #99a); }
.comparison td.is-yes :is(svg, .icon) { color: var(--color-accent-text, var(--color-primary, #0066cc)); }
.comparison :is(th, td).is-featured { background: color-mix(in srgb, var(--color-primary, #0066cc) 7%, transparent); }
.comparison thead th.is-featured { border-bottom: 2px solid var(--color-primary, #0066cc); }
.comparison tbody tr:last-child td.is-featured { border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius); }

/* --- Tabs (JS-enhanced, accessible; see main.js) ------------------------ */
.tabs { display: grid; gap: var(--space); }
.tab-list { display: flex; flex-wrap: wrap; gap: .25rem; border-bottom: 1px solid rgba(0, 0, 0, .1); }
.tab-btn { appearance: none; border: 0; background: none; font: inherit; cursor: pointer; padding: .7em 1.1em; color: var(--color-muted, #555); border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tab-btn:hover { color: var(--color-primary, #0066cc); }
.tab-btn[aria-selected="true"] { color: var(--color-primary, #0066cc); border-bottom-color: var(--color-primary, #0066cc); font-weight: 600; }
.tab-panel[hidden] { display: none; }
/* A tab panel stacks a heading, copy and often a checklist; base resets element
   margins, so without this the paragraph and the list that follows it hug (no gap
   between the intro copy and the checks). Give the panel a consistent flow rhythm. */
.tab-panel > * + * { margin-top: var(--space); }

/* --- Forms ----------------------------------------------------------------
   Token-driven baseline so a generated (demo) form looks designed out of the
   box; style.css may refine. Fields read the brand tokens like everything else. */
.field { display: flex; flex-direction: column; gap: .4em; margin-bottom: .9rem; }
/* The field owns its OWN vertical rhythm. When it also sits inside a .stack, the
   stack's `> * + * { margin-top: --space }` would compound with this margin — and
   they only collapse in plain block flow, so a skin that makes the form flex/grid
   yields a doubled gap (the "too much space between fields" bug). Cancel the stack's
   contribution on fields so the field margin is the single source, and drop the
   trailing margin so the control group ends flush. */
.stack > .field { margin-top: 0; }
.field:last-child { margin-bottom: 0; }
.field > label, .form label { font-weight: 600; font-size: .95em; }
input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=button]):not([type=range]),
select, textarea {
    width: 100%;
    box-sizing: border-box;
    padding: .7em .85em;
    background: var(--color-background, #fff);
    color: var(--color-text, #1a1a1a);
    border: 1px solid color-mix(in srgb, var(--color-text, #1a1a1a) 22%, transparent);
    border-radius: var(--radius, 8px);
    transition: border-color .15s ease, box-shadow .15s ease;
}
textarea { min-height: 8em; resize: vertical; }
input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--color-primary, #0066cc);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary, #0066cc) 22%, transparent);
}

/* Demo-form notice (shown by main.js when a backend-less form is submitted). */
.form-demo-note {
    margin-top: var(--space, 1rem);
    padding: .75em 1em;
    border-radius: var(--radius, 8px);
    background: color-mix(in srgb, var(--color-primary, #0066cc) 9%, var(--color-background, #fff));
    border: 1px solid color-mix(in srgb, var(--color-primary, #0066cc) 28%, transparent);
    color: var(--color-text, #1a1a1a);
    font-size: .95em;
}

/* Hosted-form result (cms_form_html / base.js). A success replaces the form with
   .form-done; the redirect back lands on #form-<id>, so leave room for a sticky header. */
.form[id^="form-"] { scroll-margin-top: 6rem; }
.form-status {
    margin-top: var(--space, 1rem);
    padding: .75em 1em;
    border-radius: var(--radius, 8px);
    background: color-mix(in srgb, var(--color-primary, #0066cc) 9%, var(--color-background, #fff));
    border: 1px solid color-mix(in srgb, var(--color-primary, #0066cc) 28%, transparent);
    color: var(--color-text, #1a1a1a);
    font-size: .95em;
}
.form-status[hidden] { display: none; }
.form-status.is-error {
    background: color-mix(in srgb, #c0392b 8%, var(--color-background, #fff));
    border-color: color-mix(in srgb, #c0392b 35%, transparent);
}
.form-done .form-status { margin-top: 0; }

@media (prefers-reduced-motion: reduce) {
    .reveal, [data-reveal] { opacity: 1 !important; transform: none !important; transition: none !important; }
    .split .media img, .split > img,
    .split-wide .media img, .split-narrow .media img,
    .feature-row .media img, .offset-media img { transition: none; }
    .marquee-track { animation: none; }
}

/* ======================================================================
   HARDENING LAYER (2026) — defends the primitives against the realistic
   ways a generated page misuses them. Appended last so it overrides the
   base rules above; a site's style.css still loads AFTER base.css and can
   override anything here. Every rule below fixes a class of bug found in
   real generated output, NOT a style preference — keep it token-driven.
   ====================================================================== */

/* --- Dark-surface awareness -------------------------------------------------
   .section-invert / .cta paint themselves the brand color, but base.css only
   recolored headings/paragraphs/cards/stats — buttons, chips, code, hr and
   hairline borders stayed light-surface values and vanished on the band. Make
   every component the model might drop into a dark band legible. */

/* Primary button on a primary-colored band would be same-color-on-same-color
   (only the label shows). Invert its fill so it reads as a real button. */
.section-invert .btn:not(.btn-outline) {
    background: var(--color-on-primary, #fff);
    color: var(--color-primary, #0066cc);
    box-shadow: none;
}
/* Outline buttons on ANY colored band: without this an outline button keeps its
   default brand-color text (e.g. cyan on a cyan band, or a faint tint on a dark
   band) and goes nearly invisible — the "ghost button" bug. Force a real
   transparent outline in the band's OWN on-color (inherited), so it's always
   legible whatever the band. Covers .cta / .section-invert and every data-options
   brand band. */
.cta .btn-outline,
.section-invert .btn-outline,
[data-options~="background-primary"] .btn-outline,
[data-options~="background-accent"] .btn-outline,
[data-options~="background-contrast"] .btn-outline,
[data-options~="background-gradient"] .btn-outline {
    background: transparent;
    color: inherit;
    border-color: currentColor;
}
/* Chips, inline code, hr and any component hairline read `currentColor`/token
   so they invert with the band instead of disappearing into it. */
.section-invert .chip { background: color-mix(in srgb, currentColor 14%, transparent); color: inherit; }
.section-invert code  { background: color-mix(in srgb, currentColor 16%, transparent); color: inherit; }
.section-invert hr    { border-top-color: color-mix(in srgb, currentColor 24%, transparent); }
.section-invert .link-underline { color: var(--color-on-primary, #fff); }
/* Logo images: never leave them permanently dimmed; restore on hover, and on a
   dark band knock them to white so dark-inked marks don't vanish. */
.logos img:hover { opacity: 1; filter: none; }
.section-invert .logos img { filter: grayscale(1) brightness(0) invert(1); }

/* --- Overflow / layout blowouts --------------------------------------------
   The tone bands bleed with width:100vw, which is wider than the content area
   when a vertical scrollbar is present → a sliver of horizontal scroll on every
   banded page. Clip it at the root (safer than overflow:hidden — keeps sticky
   working). Also center the FAQ so it doesn't strand in a wide container, keep
   the marquee track on one line even if .logos (flex-wrap:wrap) is merged onto
   it, let asymmetric grids backfill partial rows, and pull the carousel arrows
   inside the container so they don't cause their own overflow. */
body { overflow-x: clip; }
.faq { margin-inline: auto; }
.marquee-track { flex-wrap: nowrap; }

/* --- Mobile robustness safeguards -------------------------------------------
   The grids already collapse; these catch the remaining mobile failure modes. */
/* Long unbroken strings (URLs, emails, IDs) wrap instead of forcing a scroll. */
:where(p, li, h1, h2, h3, h4, .display, .lede, blockquote, td, .contact-item .value) { overflow-wrap: break-word; }
/* Keep form fields >=16px so iOS doesn't auto-zoom the whole page on focus. */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), select, textarea { font-size: max(16px, 1em); }
/* Comfortable tap targets on touch devices (44px is the platform guideline). */
@media (pointer: coarse) {
    .btn, .btn-outline, .tab-btn, .nav-toggle { min-height: 44px; }
    .btn, .btn-outline { display: inline-flex; align-items: center; justify-content: center; }
    .nav-dropdown .nav-link, .site-nav.is-open .nav-link { min-height: 44px; display: flex; align-items: center; }
}
.bento, .editorial-grid { grid-auto-flow: dense; }
.carousel-prev { left: .25rem; }
.carousel-next { right: .25rem; }

/* --- Full-bleed hero photo cover -------------------------------------------
   A hero background image must COVER its layer. The reset forces img{height:auto},
   so a photo in an absolutely-positioned hero layer letterboxes and the brand
   color shows underneath. Cover it whatever class the page invented, and cover
   any image inside an inset:0 hero layer (the pattern generated pages emit). */
.hero-bg-img, .scrim-img, .hero-bg-photo,
.hero > img,
.hero [style*="inset"] > img,
.hero [style*="inset"] > picture > img {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    object-fit: cover;
}

/* --- Testimonial author name/role ------------------------------------------
   The name and role render as adjacent inline spans; without this they collide
   into one run of text ("Jane DoeCEO, Acme"). Stack them into a small column so
   the name reads as the name and the role as a quieter line beneath it. */
.testimonial .author-meta { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.testimonial .author-name { display: block; }
.testimonial .author-role { display: block; font-size: .9rem; color: var(--color-muted, #667); }
.section-invert .testimonial .author-role { color: color-mix(in srgb, currentColor 72%, transparent); }

/* ======================================================================
   OPTIONS SYSTEM (2026) — the data-options modifier vocabulary.

   `class`         = what a thing IS (structure): .section, .container, .hero,
                     .split, .card, .cta, .stats … (the primitives above).
   `data-options`  = how it's DRESSED: a space-separated bag of modifiers,
                     matched with [data-options~="token"].

   Two properties make this worth more than plain classes:
     1. Tokens COMPOSE — data-options="background-primary text-center flush-bottom".
     2. Tokens are CONTEXT-AWARE — a token can mean one thing on a <section>
        (a full-bleed band) and another on a .card (a filled panel), and can
        target descendants (section[data-options~="background-primary"] .container p).

   These are a PALETTE the generator draws from PER SECTION — used sometimes,
   varied section-to-section and site-to-site. Nothing here should fire on every
   section. Legacy modifier classes (.section-invert/-tint/-band, .text-center)
   are kept as ALIASES so already-generated sites keep working unchanged.

   Everything reads the theme tokens, so it re-skins with the palette. A site's
   style.css still loads AFTER base.css and overrides anything here.
   ====================================================================== */

/* --- SECTION HEAD: one consistent section intro ----------------------------
   Every section header (eyebrow → heading → lede) in ONE block with uniform
   spacing, instead of each section improvising margins. Centering is driven
   from HERE (or the host's text-center option), so the eyebrow can never strand
   left above a centered heading — the bug where .eyebrow.text-center did nothing
   because an inline-block only centers its own text, not its box. */
.section-head {
    /* A plain BLOCK stack, deliberately NOT a flex column. When a section-head is
       centered — whether by data-options="text-center" OR by a per-site skin that
       makes .section-head text-align:center its default look — text-align must govern
       EVERY child. In a flex column it doesn't: a fit-content .eyebrow/.chip-kicker
       pill is placed by align-items (start), so the heading centers while the pill
       strands at the left edge (the recurring "centered title, left eyebrow" tell).
       In a block, the inline-level eyebrow/pill and the block heading all follow
       text-align together. Rhythm comes from margins instead of the flex gap. */
    display: block;
    max-width: 64ch;
    margin-bottom: clamp(calc(var(--space) * 2), 5vw, calc(var(--space) * 3.5));
}
.section-head > * { margin: 0; }
.section-head > * + * { margin-top: .7rem; }
.section-head .lede { margin-top: .35rem; }
/* text-center placed DIRECTLY on the head (not only via an ancestor) must center
   the BOX and its inline children too — otherwise a lone head with nothing beside
   it strands at the left of a wide container (text centered inside a left-hugging
   64ch box reads as "left-aligned and weird"). */
.section-head.text-center, .section-head[data-options~="text-center"] {
    align-items: center;
    text-align: center;
    margin-inline: auto;
}

/* Intro heading placed DIRECTLY in a .container (the eyebrow → heading → content
   pattern where there's no .section-head wrapper) gets real breathing room before
   the content below. Vertical margins collapse with any following block, so this
   never double-spaces a section that already sets its own top margin. */
.container > :is(h1, h2, h3) { margin-bottom: calc(var(--space) * .9); }

/* --- FLOW: automatic vertical rhythm for a text column ---------------------
   The global reset zeroes margins, so a plain text column (the copy beside an
   image, an .offset-card, any hand-built block) collapses its heading and
   paragraphs together — the "squished" look. .flow restores a consistent,
   tunable rhythm (retune with --flow). It is applied by hand AND auto-applied
   to the editorial text columns that ship with no spacing of their own. */
.flow > * ,
.split       > :not(.media):not(img) > * ,
.split-wide  > :not(.media):not(img) > * ,
.split-narrow > :not(.media):not(img) > * ,
.offset-card > * ,
.measure > * { margin: 0; }

.flow > * + * ,
.split       > :not(.media):not(img) > * + * ,
.split-wide  > :not(.media):not(img) > * + * ,
.split-narrow > :not(.media):not(img) > * + * ,
.offset-card > * + * ,
.measure > * + * { margin-top: var(--flow, calc(var(--space) * .95)); }
/* A media block following a text intro (e.g. a centered hero over a wide image)
   needs real breathing room so the picture doesn't crowd the copy. */
.flow + .media, .section-head + .media, [data-options~="measure"] + .media { margin-top: calc(var(--space) * 2.5); }
/* Avoid ugly one-word orphans on a centered lede. */
.lede { text-wrap: pretty; }
/* A trailing CTA link/button as a flow child needs a box for its top margin to
   apply (vertical margin is ignored on an inline element) — so it gets real space
   above. These content columns are grid/flex (e.g. .feature-row-body is display:grid),
   so a plain child STRETCHES to the full column width by default: `display:inline-*`
   is blockified and ignored. That makes a "read more" underline span the whole
   column and a lone .btn go full-width. Pin them to their content width and pack
   them to the start so they hug their text, not the cell. */
.flow > .link-underline,
.feature-row-body > .link-underline,
.offset-card > .link-underline,
.flow > .btn, .flow > .btn-outline,
.feature-row-body > .btn, .feature-row-body > .btn-outline,
.offset-card > .btn, .offset-card > .btn-outline {
    display: inline-block;
    width: fit-content;
    max-width: 100%;
    justify-self: start;
    align-self: start;
}
/* A centered content column keeps the CTA centered, not stranded at the left. */
:is(.text-center, [data-options~="text-center"]) :is(.flow, .feature-row-body, .offset-card)
    > :is(.link-underline, .btn, .btn-outline) { justify-self: center; align-self: center; margin-inline: auto; }

/* An eyebrow and the heading it labels are a UNIT — pull them together. */
.flow > .eyebrow + * ,
.split       > :not(.media):not(img) > .eyebrow + * ,
.split-wide  > :not(.media):not(img) > .eyebrow + * ,
.split-narrow > :not(.media):not(img) > .eyebrow + * ,
.offset-card > .eyebrow + * { margin-top: 1rem; }

/* A row of buttons gets a little more air above it. */
.flow > * + .hero-actions ,
.offset-card > * + .hero-actions { margin-top: calc(var(--space) * 1.4); }

/* --- BLOG --------------------- */
main.blog-post > article { margin: var(--space) 0 calc(var(--space) * 3); }

/* ======================================================================
   AXIS A — SURFACE / BACKGROUND
   On a <section> these are full-bleed BANDS; on a .card / panel they FILL that
   element. Brand surfaces set their own readable foreground from the matching
   on-token; a shared "inverted surface" group below makes descendants legible
   relative to that foreground (currentColor), covering legacy .section-invert
   and .cta too so there is ONE inversion behavior everywhere.
   ====================================================================== */

[data-options~="background-primary"]  { background: var(--color-band-primary, var(--color-primary, #0066cc)); color: var(--color-on-band-primary, var(--color-on-primary, #fff)); }
[data-options~="background-accent"]   { background: var(--color-band-accent, var(--color-accent, #33aaff));  color: var(--color-on-band-accent, var(--color-on-accent, #fff)); }
/* Maximum-contrast band — swaps the page's own text/background, so it is dark on
   a light site and light on a dark site (always high-contrast by construction). */
[data-options~="background-contrast"] { background: var(--color-text, #14161c);    color: var(--color-background, #fff); }
[data-options~="background-tint"]     { background: color-mix(in srgb, var(--color-primary, #0066cc) 6%, var(--color-background, #fff)); }
[data-options~="background-band"]     { background: color-mix(in srgb, var(--color-accent, #33aaff) 8%, var(--color-background, #fff)); }
[data-options~="background-surface"]  { background: var(--surface); }
[data-options~="background-surface-2"]{ background: var(--surface-2); }
[data-options~="background-gradient"] { background: linear-gradient(135deg, var(--color-band-primary, var(--color-primary, #0066cc)), color-mix(in srgb, var(--color-band-primary, var(--color-primary, #0066cc)) 40%, var(--color-accent, #33aaff))); color: var(--color-on-band-primary, var(--color-on-primary, #fff)); }
/* Photo band: the element carries an inline background-image; this adds the
   sanctioned scrim + white text (same contract as .scrim). */
[data-options~="background-photo"] { position: relative; color: #fff; background-size: cover; background-position: center; }
[data-options~="background-photo"]::before { content: ""; position: absolute; inset: 0; z-index: 0; background: linear-gradient(180deg, rgba(0,0,0,.25), rgba(0,0,0,.6)); }
[data-options~="background-photo"] > * { position: relative; z-index: 1; }
[data-options~="background-photo"] :is(h1,h2,h3,h4,p) { color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,.5); }

/* Give a flat colored band a HINT of depth so it doesn't read as a dead rectangle
   of color (the "elementary" look). A very faint top-left light — replace it with a
   background-gradient/-mesh/pattern-* option or style.css for something richer. */
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    .section-invert,
    .cta) {
    background-image:
        radial-gradient(120% 120% at 15% -10%, color-mix(in srgb, #fff 16%, transparent), transparent 55%),
        linear-gradient(160deg, transparent 40%, color-mix(in srgb, #000 18%, transparent));
}

/* Inverted / colored SURFACE group — descendants read relative to the surface's
   own foreground so text, secondary text, eyebrows, hairlines, chips, code and
   buttons all stay legible on ANY of the colored surfaces (and on the legacy
   classes). Kept token/currentColor-driven so it works under any palette. */
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"],
    .section-invert, .cta) {
    --border: color-mix(in srgb, currentColor 22%, transparent);
}
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"]) :is(h1, h2, h3, h4) { color: inherit; }
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"]) :is(p, li, .lede, .stat-label, .feature p, .step p, .feature-row-body p, .timeline-item p) {
    /* Full-strength on a SOLID brand band: softening body text here (e.g. 82%)
       drops a saturated band like crimson below AA. Hierarchy still reads from
       size/weight; legibility wins on a colored surface. */
    color: inherit;
}
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"]) .eyebrow { color: color-mix(in srgb, currentColor 90%, transparent); }
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"]) .stat-num { color: inherit; }
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"]) .card {
    background: color-mix(in srgb, currentColor 8%, transparent);
    border-color: color-mix(in srgb, currentColor 16%, transparent);
    color: inherit;
}
/* Cards that carry their OWN opaque light surface (a testimonial, price or offset
   card keeps its white --color-background, unlike the translucent .card above) must
   also reset their FOREGROUND on a colored band. Without this they inherit the
   band's white text and render invisibly — white text on a white card. Pair the
   opaque light surface with the page's normal dark text + muted tokens. */
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"]) :is(.testimonial, .price-card, .offset-card) {
    color: var(--color-text, #14161c);
    --border: color-mix(in srgb, var(--color-text, #14161c) 12%, transparent);
}
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"])
    :is(.testimonial, .price-card, .offset-card) :is(h1, h2, h3, h4, p, li, blockquote) {
    color: var(--color-text, #14161c);
    text-shadow: none;
}
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"])
    :is(.testimonial, .price-card, .offset-card) :is(.author-role, .price small, .muted, .eyebrow) {
    color: var(--color-muted, #555);
}
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"]) .chip { background: color-mix(in srgb, currentColor 14%, transparent); color: inherit; }
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"]) hr { border-top-color: color-mix(in srgb, currentColor 24%, transparent); }
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"]) .link-underline { color: inherit; }

/* Buttons on a colored surface — per-surface because a solid button needs the
   band's own background as its text color (not derivable from currentColor). */
[data-options~="background-primary"]  .btn:not(.btn-outline) { background: var(--color-on-band-primary, var(--color-on-primary, #fff)); color: var(--color-band-primary, var(--color-primary, #0066cc)); box-shadow: none; }
[data-options~="background-accent"]   .btn:not(.btn-outline) { background: var(--color-on-band-accent, var(--color-on-accent, #fff));  color: var(--color-band-accent, var(--color-accent, #33aaff));  box-shadow: none; }
[data-options~="background-contrast"] .btn:not(.btn-outline) ,
[data-options~="background-gradient"] .btn:not(.btn-outline) { background: var(--color-background, #fff); color: var(--color-text, #14161c); box-shadow: none; }
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"]) .btn-outline {
    background: transparent; color: currentColor; border-color: currentColor;
}

/* A .cta placed INSIDE a colored band would double-box (a rounded primary card
   on a primary band). Flatten it so it reads as one surface. */
:is([data-options~="background-primary"],
    [data-options~="background-accent"],
    [data-options~="background-contrast"],
    [data-options~="background-gradient"],
    .section-invert) .cta {
    background: transparent; color: inherit; box-shadow: none; padding-inline: 0;
}

/* ======================================================================
   AXIS B — ALIGNMENT   (host: section / .section-head / any block)
   ====================================================================== */
:is(.text-center, [data-options~="text-center"]) { text-align: center; }
/* A centered host must also center its CONTENT BLOCK, not just the text inside it.
   The generator's standard centered-hero pattern is <div class="flow" data-options="measure">
   — a width-capped block. Match the option form too (not only the .measure class), plus
   narrow/wide, or the block keeps its width cap but hugs the left with centered text inside
   it (the "centered text in a left-stranded block" bug). */
:is(.text-center, [data-options~="text-center"]) :is(.lede, .measure, [data-options~="measure"], [data-options~="narrow"], [data-options~="wide"], .flow, .section-head, .hero-actions) { margin-inline: auto; }
:is(.text-center, [data-options~="text-center"]) .hero-actions { justify-content: center; }
:is(.text-center, [data-options~="text-center"]) .section-head { align-items: center; }
/* A centered section-head above a width-capped body COLUMN (.measure/narrow) must
   center that column too — otherwise the copy hugs the left under a centered header
   and the section reads lopsided (centered title, left-stranded body). The block is
   centered; its paragraph text stays left-aligned for readability. */
:is(.section-head.text-center, .section-head[data-options~="text-center"])
    ~ :is(.measure, [data-options~="measure"], [data-options~="narrow"]) { margin-inline: auto; }
[data-options~="text-right"] { text-align: right; }
/* If an eyebrow is tagged directly (not via the host), make it a block so its
   own text-align can center it. */
.eyebrow.text-center, .eyebrow[data-options~="text-center"] { display: block; }
[data-options~="items-center"] { align-items: center; }
[data-options~="justify-end"]     { justify-content: flex-end; }
[data-options~="justify-between"] { justify-content: space-between; }
[data-options~="items-start"]  { align-items: flex-start; }
[data-options~="items-end"]    { align-items: flex-end; }

/* ======================================================================
   AXIS C — DENSITY / RHYTHM   (host: section)
   flush-bottom on one section + flush-top on the next (same background) = the
   two bleed together into a single uninterrupted surface.
   ====================================================================== */
/* Density is SECTION rhythm — scope it to section-level hosts. Dropped on a bounded
   component that carries its own surface (a .contact-panel with hairline dividers, a
   card, a form), the band padding just inflates the component and reveals its own
   background as empty gray bars above/below the content. Restricting the selector to
   sections/containers makes that misuse a no-op instead of a visible defect. */
:is(section, .hero, .cta, .container, [class*="section"])[data-options~="tight"] { padding-block: clamp(calc(var(--space) * 1.25), 3vw, calc(var(--space) * 2.5)); }
:is(section, .hero, .cta, .container, [class*="section"])[data-options~="roomy"] { padding-block: clamp(calc(var(--space) * 4), 8vw, calc(var(--space) * 7)); }
[data-options~="flush"]        { padding-block: 0; }
/* flush merges two same-tone bands into one surface. Keep a small residual pad so
   the two sections' CONTENT still has breathing room and never collides at the
   seam (a full-zero gap made a flushed section's head butt the section above). */
[data-options~="flush-top"]    { padding-top: calc(var(--space) * 1.25); }
[data-options~="flush-bottom"] { padding-bottom: calc(var(--space) * 1.25); }
/* UNPAIRED flush guard. flush only reads as a deliberate bleed when BOTH sides opt in
   (flush-bottom above + flush-top below). Generated pages often stamp one side alone,
   which just collapses a section against a differently-styled band — the "this section
   has no margin" tell. Restore standard section padding whenever the partner is absent. */
:where(section):not([data-options~="flush-bottom"]) + [data-options~="flush-top"] {
    padding-top: clamp(calc(var(--space) * 2.25), 5.5vw, calc(var(--space) * 4.5));
}
:where(section)[data-options~="flush-bottom"]:not(:has(+ [data-options~="flush-top"])) {
    padding-bottom: clamp(calc(var(--space) * 2.25), 5.5vw, calc(var(--space) * 4.5));
}

/* ======================================================================
   AXIS D — CONTENT WIDTH   (host: .container / a content block)
   ====================================================================== */
/* These four are mutually exclusive widths. The model sometimes emits two at once
   (e.g. "measure wide" — a narrow reading measure AND a wide container), which
   used to resolve by source order and strand a listing in a too-narrow column.
   Guard each so the WIDER intent always wins deterministically: full > wide >
   narrow > measure, whatever order they're written in. */
[data-options~="narrow"]:not([data-options~="wide"]):not([data-options~="full"])  { max-width: 46rem; }
[data-options~="wide"]:not([data-options~="full"])    { max-width: calc(var(--container) + 12rem); }
[data-options~="full"]    { max-width: none; }
[data-options~="measure"]:not([data-options~="narrow"]):not([data-options~="wide"]):not([data-options~="full"]) { max-width: 68ch; }

/* ======================================================================
   AXIS E — CONNECTIVE MOTIFS   (host: section — used SOMETIMES, for rhythm)
   The cohesion layer: a hairline between bands, an accent tick on the eyebrow,
   a colored top edge, a numbered index. Pick one occasionally; don't stack them.
   ====================================================================== */
/* Accent tick before the eyebrow (a small recurring brand mark). */
[data-options~="rule-eyebrow"] .eyebrow::before ,
.eyebrow[data-options~="rule"]::before {
    content: ""; display: inline-block; width: 1.75rem; height: 2px;
    margin-right: .6rem; vertical-align: middle;
    background: currentColor; opacity: .6;
}
[data-options~="divider-top"]    { border-top: 1px solid var(--border); }
[data-options~="divider-bottom"] { border-bottom: 1px solid var(--border); }
[data-options~="accent-edge"]    { border-top: 3px solid var(--color-accent, #33aaff); }
/* An accent-edge on a content block (a .flow quote, a note) draws a border right up
   against the copy — and a skin often adds a left border too, so the text ends up
   jammed into the corner of the accent line. Give it even inset from the top and both
   sides so the border reads as a frame with breathing room, not a cramped bracket.
   Cards / sections own their own padding, so leave those to it. */
[data-options~="accent-edge"]:not(.card):not(.section):not(.price-card):not(.offset-card) {
    padding-block-start: calc(var(--space) * 1.1);
    padding-inline: calc(var(--space) * 1.25);
}
/* Numbered index — SAFE on simple repeating children (steps / features / cards /
   list items); not the media-grid rows, so it never breaks a grid. */
[data-options~="index"] { counter-reset: idx; }
[data-options~="index"] > :is(.step, .feature, .card, li) { position: relative; }
[data-options~="index"] > :is(.step, .feature, .card, li)::before {
    counter-increment: idx;
    content: counter(idx, decimal-leading-zero);
    display: block;
    margin-bottom: .5rem;
    font: 700 .8rem/1 var(--font-heading, inherit);
    letter-spacing: .12em;
    color: var(--color-accent-text, var(--color-accent, #33aaff));
}

/* ======================================================================
   AXIS F — SHAPE / ELEVATION   (host: .card / .media / panel / button)
   ====================================================================== */
[data-options~="raised"]   { box-shadow: var(--shadow-lg); }
[data-options~="flat"]     { box-shadow: none; }
[data-options~="bordered"] { border: 1px solid var(--border); }
[data-options~="round"]    { border-radius: var(--radius-lg); }
[data-options~="sharp"]    { border-radius: 0; }
[data-options~="pill"]     { border-radius: 999px; }
/* Pull a block up so it overlaps the previous section (the "designed, not
   stacked" seam). Pair with a raised/bordered surface so the overlap reads. */
[data-options~="overlap"]  { margin-top: calc(var(--space) * -3.5); position: relative; z-index: 2; }

/* ======================================================================
   AXIS G — TYPE TREATMENT   (host: section / heading)
   ====================================================================== */
[data-options~="heading-tight"] :is(h1, h2, h3, .display) { letter-spacing: -0.03em; }
[data-options~="heading-wide"]  :is(h1, h2, h3) { letter-spacing: .02em; }
[data-options~="balance"] :is(h1, h2, h3, h4, .display, .lede) { text-wrap: balance; }
[data-options~="uppercase"] :is(h1, h2, h3) { text-transform: uppercase; letter-spacing: .03em; }
[data-options~="display-xl"] .display ,
.display[data-options~="display-xl"] { font-size: clamp(2.8rem, 1.6rem + 5vw, 5rem); }

/* ======================================================================
   AXIS H — MEDIA TREATMENT   (host: img / media wrapper)
   (.duotone / .img-tint stay as classes — they need wrapper ::before/::after.)
   ====================================================================== */
[data-options~="grayscale"] img , img[data-options~="grayscale"] { filter: grayscale(1); transition: filter .4s ease; }
[data-options~="grayscale"]:hover img , img[data-options~="grayscale"]:hover { filter: none; }
[data-options~="rounded"] :is(img, .media) , img[data-options~="rounded"] { border-radius: var(--radius-lg); overflow: hidden; }

/* --- Hosted form field grid (native [form] renderer) ---------------------- */
/* Scoped to the .form-grid wrapper cms_form_html() emits, so it never affects
   other .form / .field markup. Fields are full-width by default; .field-half
   pairs two per row and collapses to one column on narrow screens. */
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: .9rem 1rem; }
.form-grid > .field { grid-column: 1 / -1; margin-bottom: 0; }
.form-grid > .field-half { grid-column: span 1; }
@media (max-width: 560px) { .form-grid > .field-half { grid-column: 1 / -1; } }
/* button_position 'inline': the last field + the submit button share one row.
   The field takes the room; the button keeps its size and drops under the field
   only when the row is too narrow for both. */
.form-grid > .form-submit-row { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: flex-end; gap: .75rem 1rem; }
.form-grid > .form-submit-row.is-half { grid-column: span 1; }
.form-submit-row > .field { flex: 1 1 12rem; min-width: 0; margin: 0; }
.form-submit-row > .form-submit { flex: none; margin: 0; }
.form-submit-row > .field-check + .form-submit { align-self: center; }
@media (max-width: 560px) { .form-grid > .form-submit-row.is-half { grid-column: 1 / -1; } }
/* Submit-button icon (button_icon): text-sized, not the 1.5em default .icon. An
   icon-only button is squared up so it doesn't read as an empty pill. */
.form-submit .icon { width: 1.15em; height: 1.15em; }
.form-submit.is-icon-only { padding-inline: .85em; }

/* ============================================================================
   AMBIENT ATMOSPHERE (Track A) — a subtle, token-driven decorative layer behind
   the page. One fixed, non-interactive element at z-index:-1, so it sits behind
   all content WITHOUT touching any existing z-index (sticky header, modals, etc.
   are untouched). Plain/transparent sections reveal it; banded sections cover it.
   Generic + gentle by default; flavored per look via <body data-look="…">. Kept
   deliberately restrained — atmosphere, not structure. Inert if the .site-ambient
   element or the data-look attribute is absent (older sites degrade cleanly).
   ========================================================================== */
.site-ambient { position: fixed; inset: 0; z-index: -1; pointer-events: none; overflow: hidden; }
.site-ambient::before, .site-ambient::after { content: ""; position: absolute; inset: 0; }
/* Generic: a whisper of warmth from the top. Works under any palette. */
.site-ambient::before { background: radial-gradient(120% 55% at 50% -12%, color-mix(in srgb, var(--color-accent) 5%, transparent), transparent 62%); }

/* Brutalist — a quiet structural grid in the open areas + one bold accent block
   off the corner (a single graphic moment). */
body[data-look="brutalist"] .site-ambient::before {
    background:
        repeating-linear-gradient(to right, color-mix(in srgb, var(--color-text) 7%, transparent) 0 1px, transparent 1px calc(100% / 12)),
        repeating-linear-gradient(to bottom, color-mix(in srgb, var(--color-text) 5%, transparent) 0 1px, transparent 1px 8rem);
}
body[data-look="brutalist"] .site-ambient::after {
    inset: auto; top: -8rem; right: -8rem; width: 24rem; height: 24rem;
    background: var(--color-accent); opacity: .1; transform: rotate(45deg);
}

/* Noir — a warm spotlight glow + a soft vignette (a lit room). */
body[data-look="noir"] .site-ambient::before { background: radial-gradient(120% 60% at 50% -8%, color-mix(in srgb, var(--color-accent) 16%, transparent), transparent 62%); }
body[data-look="noir"] .site-ambient::after { background: radial-gradient(ellipse at center, transparent 52%, rgba(0, 0, 0, .42) 130%); }

/* Coastal — a soft sea-to-sand wash. */
body[data-look="coastal"] .site-ambient::before { background: linear-gradient(to bottom, color-mix(in srgb, var(--color-primary) 8%, transparent), transparent 42%, transparent 72%, color-mix(in srgb, var(--color-accent) 8%, transparent)); }

/* Gradient — a soft luminous mesh glow drifting behind the page. */
body[data-look="gradient"] .site-ambient::before { background: radial-gradient(50% 60% at 12% 8%, color-mix(in srgb, var(--color-accent) 10%, transparent), transparent 60%), radial-gradient(55% 60% at 88% 14%, color-mix(in srgb, var(--color-tertiary) 9%, transparent), transparent 60%); }

/* Mono — a fine technical hairline grid, very quiet. */
body[data-look="mono"] .site-ambient::before {
    background:
        repeating-linear-gradient(to right, color-mix(in srgb, var(--color-text) 5%, transparent) 0 1px, transparent 1px 6rem),
        repeating-linear-gradient(to bottom, color-mix(in srgb, var(--color-text) 4%, transparent) 0 1px, transparent 1px 6rem);
}

/* Industrial — a coarse steel hairline grid. */
body[data-look="industrial"] .site-ambient::before { background: repeating-linear-gradient(to right, color-mix(in srgb, var(--color-text) 6%, transparent) 0 1px, transparent 1px 7rem), repeating-linear-gradient(to bottom, color-mix(in srgb, var(--color-text) 5%, transparent) 0 1px, transparent 1px 7rem); }

/* Aura SaaS — a soft brand aura glow from the top. */
body[data-look="aurasaas"] .site-ambient::before { background: radial-gradient(60% 55% at 50% -6%, color-mix(in srgb, var(--color-primary) 12%, transparent), transparent 62%); }

/* Dark studio — a faint accent glow drifting up from a corner. */
body[data-look="studio"] .site-ambient::before { background: radial-gradient(45% 45% at 88% 92%, color-mix(in srgb, var(--color-accent) 10%, transparent), transparent 62%); }

/* Friendly — a soft green glow from the top. */
body[data-look="friendly"] .site-ambient::before { background: radial-gradient(55% 55% at 50% -8%, color-mix(in srgb, var(--color-primary) 8%, transparent), transparent 62%); }
/* Vibrant — two playful colour glows. */
body[data-look="vibrant"] .site-ambient::before { background: radial-gradient(45% 50% at 12% 6%, color-mix(in srgb, var(--color-primary) 9%, transparent), transparent 60%), radial-gradient(50% 55% at 90% 12%, color-mix(in srgb, var(--color-accent) 9%, transparent), transparent 60%); }
/* Warm — a broad sunny glow. */
body[data-look="warm"] .site-ambient::before { background: radial-gradient(70% 55% at 50% -8%, color-mix(in srgb, var(--color-primary) 9%, transparent), transparent 62%); }
/* Technical — a fine dark grid + a corner accent glow. */
body[data-look="technical"] .site-ambient::before { background: repeating-linear-gradient(to right, color-mix(in srgb, var(--color-text) 5%, transparent) 0 1px, transparent 1px 6rem), repeating-linear-gradient(to bottom, color-mix(in srgb, var(--color-text) 4%, transparent) 0 1px, transparent 1px 6rem), radial-gradient(50% 50% at 85% 8%, color-mix(in srgb, var(--color-accent) 10%, transparent), transparent 60%); }
/* Bold — vivid brand glows from the corners. */
body[data-look="bold"] .site-ambient::before { background: radial-gradient(50% 55% at 15% 8%, color-mix(in srgb, var(--color-primary) 9%, transparent), transparent 60%), radial-gradient(50% 55% at 88% 14%, color-mix(in srgb, var(--color-accent) 8%, transparent), transparent 60%); }
/* Quiet — barely-there greige. Restraint. */
body[data-look="quiet"] .site-ambient::before { background: radial-gradient(80% 50% at 50% -14%, color-mix(in srgb, var(--color-text) 3%, transparent), transparent 60%); }
/* Organic — a soft sage wash. */
body[data-look="organic"] .site-ambient::before { background: radial-gradient(75% 55% at 50% -10%, color-mix(in srgb, var(--color-primary) 8%, transparent), transparent 62%); }

/* Clean SaaS — a crisp indigo glow from the top. */
body[data-look="saas"] .site-ambient::before { background: radial-gradient(60% 55% at 50% -8%, color-mix(in srgb, var(--color-primary) 8%, transparent), transparent 62%); }

/* Hero spacing: when a hero stacks a second full-bleed element after its content
   container — a product screenshot in its own wider container, or a full-bleed
   marquee band — give it room to breathe from the CTA above (the .hero-actions +
   sibling rule can't reach across containers). */
.hero > .container + .container,
.hero > .container + .marquee { margin-top: clamp(2rem, 5vw, calc(var(--space) * 3.5)); }

/* ============================================================================
   VIEW TRANSITIONS (Track B) — app-like motion between real page navigations.
   Cross-document (MPA) transitions: every page opts in here, so same-origin
   navigations cross-fade instead of hard-cutting. Pure progressive enhancement —
   browsers without support just navigate normally — and fully gated by
   prefers-reduced-motion.
   ========================================================================== */
@view-transition { navigation: auto; }
::view-transition-old(root),
::view-transition-new(root) { animation-duration: .32s; animation-timing-function: cubic-bezier(.4, 0, .2, 1); }
/* The sticky header holds its place across the transition instead of fading. */
.site-header { view-transition-name: site-header; }
@media (prefers-reduced-motion: reduce) {
    @view-transition { navigation: none; }
}

/* --- Filterable grid ([data-filter-group]) ------------------------------- */
.filter-bar { display: flex; flex-wrap: wrap; gap: .5rem; justify-content: center; margin-bottom: calc(var(--space) * 1.6); }
.filter-btn { font: inherit; font-size: .85rem; cursor: pointer; padding: .42rem .95rem; border: 1px solid var(--border); border-radius: 999px; background: transparent; color: var(--color-muted, #555); transition: background-color .15s ease, color .15s ease, border-color .15s ease; }
.filter-btn:hover { color: var(--color-text, #111); border-color: color-mix(in srgb, var(--color-text) 30%, transparent); }
.filter-btn[aria-pressed="true"] { background: var(--color-primary); color: var(--color-on-primary, #fff); border-color: transparent; }
[data-filter-group] [data-tags][hidden] { display: none; }
/* Screen-reader-only utility (used by the filter count live region). */
.visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }

/* --- Lightbox (native <dialog>, built on demand by base.js) --------------- */
.lightbox { max-width: min(94vw, 1100px); max-height: 94vh; width: auto; padding: 0; border: 0; background: transparent; overflow: visible; color: #fff; margin: 0; }
.lightbox::backdrop { background: rgba(0, 0, 0, .84); }
.lightbox-figure { margin: 0; display: grid; gap: .7rem; justify-items: center; }
.lightbox-img { display: block; max-width: 100%; max-height: 84vh; width: auto; height: auto; border-radius: var(--radius, 8px); box-shadow: 0 24px 70px rgba(0, 0, 0, .5); }
.lightbox-cap { margin: 0; font-size: .9rem; text-align: center; opacity: .85; }
.lightbox-close, .lightbox-nav { position: fixed; z-index: 1; display: grid; place-items: center; border: 0; cursor: pointer; color: #fff; background: rgba(0, 0, 0, .5); border-radius: 999px; line-height: 1; }
.lightbox-close:hover, .lightbox-nav:hover { background: rgba(0, 0, 0, .78); }
.lightbox-close { top: 1rem; right: 1rem; width: 2.6rem; height: 2.6rem; font-size: 1.7rem; }
.lightbox-nav { top: 50%; transform: translateY(-50%); width: 3rem; height: 3rem; font-size: 2rem; }
.lightbox-prev { left: 1rem; }
.lightbox-next { right: 1rem; }
.gallery-item[data-lightbox] { cursor: zoom-in; }
@media (prefers-reduced-motion: no-preference) {
    .lightbox[open] { animation: lightbox-in .2s ease; }
}
@keyframes lightbox-in { from { opacity: 0; } to { opacity: 1; } }

/* ============================================================================
   SCROLL-DRIVEN MOTION (Track B) — native, GPU, JS-free scroll-LINKED motion via
   animation-timeline: view(). Unlike the one-shot .reveal (which just toggles a
   class once in view), these track scroll PROGRESS continuously. OPT-IN only via
   [data-scroll] so nothing changes on existing sites unless a builder asks for it.
   Doubly gated: the animation-name is set ONLY inside @supports, so browsers
   without support never animate (the element keeps its natural, visible state),
   and the whole block is off under prefers-reduced-motion. Use sparingly — one
   signature moment per page, not on everything.
   ========================================================================== */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        [data-scroll] { animation-timeline: view(); animation-fill-mode: both; animation-timing-function: linear; }
        /* Ranges start a little AFTER the element enters and finish as it nears the
           middle of the viewport (into the 'cover' phase), so the motion plays where
           the reader is looking rather than completing down at the fold edge. */
        /* Enters: fade + rise. */
        [data-scroll="fade-up"] { animation-name: sd-fade-up; animation-range: entry 12% cover 42%; }
        /* Enters: settle up from a subtle scale. */
        [data-scroll="zoom"]    { animation-name: sd-zoom; animation-range: entry 12% cover 45%; }
        /* Continuous: gentle parallax drift across the whole time it's on screen. */
        [data-scroll="drift"]   { animation-name: sd-drift; animation-range: cover 0% cover 100%; }
        /* Enters: a clip-path wipe reveal — reads as a section-to-section transition. */
        [data-scroll="wipe"]    { animation-name: sd-wipe; animation-range: entry 18% cover 50%; }
        /* A whole section that lifts + fades in as its top crosses in. */
        [data-scroll="settle"]  { animation-name: sd-settle; animation-range: entry 12% cover 40%; }
    }
}
@keyframes sd-fade-up { from { opacity: 0; transform: translateY(2.2rem); } to { opacity: 1; transform: none; } }
@keyframes sd-zoom    { from { opacity: 0; transform: scale(.94); } to { opacity: 1; transform: none; } }
@keyframes sd-drift   { from { transform: translateY(2.4rem); } to { transform: translateY(-2.4rem); } }
@keyframes sd-wipe    { from { opacity: 0; clip-path: inset(0 0 100% 0); } to { opacity: 1; clip-path: inset(0 0 0 0); } }
@keyframes sd-settle  { from { opacity: 0; transform: translateY(3rem); } to { opacity: 1; transform: none; } }

/* --- Member pages ---------------------------------------------------------
   Sign-in, password and no-access pages (pages/login.php and friends). Narrow,
   quiet, entirely token-driven so they inherit the site's brand without any
   per-site styling — these pages are template-owned and a client never designs
   them, so they have to look deliberate out of the box on every palette. */
.auth-card { max-width: 26rem; }
.auth-card .form { margin-top: var(--space, 1rem); }
.auth-card .section-head { margin-bottom: var(--space, 1rem); }

.auth-note {
    margin-top: var(--space, 1rem);
    font-size: .95em;
    color: var(--color-muted, #666);
}

.auth-links {
    margin-top: var(--space, 1rem);
    font-size: .95em;
}

/* Errors read as a problem without importing a red that fights the brand: the
   page's own text color at full strength on a tinted ground derived from it. */
.auth-error {
    margin-top: var(--space, 1rem);
    padding: .7em 1em;
    border-radius: var(--radius, 8px);
    background: color-mix(in srgb, #d92b2b 10%, var(--color-background, #fff));
    border: 1px solid color-mix(in srgb, #d92b2b 38%, transparent);
    color: var(--color-text, #1a1a1a);
    font-size: .95em;
}

/* --- Signed-in member bar -------------------------------------------------
   A small floating "you are signed in / sign out" control, emitted by the router
   for signed-in members. Deliberately corner-anchored and quiet: it appears on
   every page of a members-only area, so it has to stay out of the way of the
   design it sits on top of. Token-driven, so it inherits each site's brand. */
.member-bar {
    position: fixed;
    right: max(1rem, env(safe-area-inset-right));
    bottom: max(1rem, env(safe-area-inset-bottom));
    z-index: 60;
    display: flex;
    align-items: center;
    gap: .6rem;
    max-width: calc(100vw - 2rem);
    padding: .5rem .55rem .5rem .85rem;
    border-radius: 999px;
    background: var(--color-background, #fff);
    border: 1px solid color-mix(in srgb, var(--color-text, #1a1a1a) 14%, transparent);
    box-shadow: var(--shadow, 0 1px 3px rgba(0,0,0,.12)), 0 6px 20px rgba(0,0,0,.08);
    font-size: .85rem;
    line-height: 1.2;
}
.member-bar-who {
    color: var(--color-muted, #666);
    max-width: 14rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.member-bar-out {
    flex: none;
    padding: .35em .9em;
    border-radius: 999px;
    background: var(--color-primary, #0066cc);
    color: var(--color-on-primary, #fff);
    text-decoration: none;
    font-weight: 600;
}
.member-bar-out:hover, .member-bar-out:focus-visible { filter: brightness(.94); }

/* On a phone the identity line is the first thing to go — the action is what
   matters, and the member already knows who they are. */
@media (max-width: 480px) {
    .member-bar-who { display: none; }
    .member-bar { padding: .5rem .55rem; }
}
@media print { .member-bar { display: none; } }
