/* Hozio Studio combined stylesheet — generated, do not edit. sig 2568ac03c3905840 */

/* --- _base/a11y.css --- */
/* ─── Global accessibility safety net — a11y.css ────────────────────────
   Sanctioned SPEC §3.7 accessibility exception to the §2 engine `!important`
   ban. This is the ONLY reason `!important` is permitted in the engine layer
   outside of the third-party integrations exception (choices.js / Fluent
   Forms overrides): here it enforces a MOTION-SAFETY override that users who
   set `prefers-reduced-motion: reduce` are entitled to, and which must beat
   any component's own animation/transition durations regardless of specificity.

   STATE-SCOPED — this rule only applies inside the reduced-motion media
   query, so it does NOT change any element's at-rest computed style and keeps
   the byte-identical fingerprint gate at 0 diffs. The per-component
   reduced-motion guards that already exist remain in place and are harmless;
   this simply covers every component (present and future) in one place. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}


/* --- _base/bleed.css --- */
/* ─── Bleed — escape the container, once ───────────────────────────────
   Wave S spatial vocabulary. The canonical container-escape math, declared
   ONE time so it stops being re-derived (usually wrong) in per-client CSS.

   THE CONTRACT. The design system's skeleton is
   `<section class="hzo-section"><div class="hzo-container">…</div></section>`:
   the section spans the viewport, the container holds the max-width. That is
   correct for 95% of content and it is also why an image or panel can never
   touch the viewport edge — the single most common "why does this look like a
   document and not like a site" symptom. .hzo-bleed is the sanctioned way for
   ONE element inside a container to reach the edge without unwrapping the
   section.

     <section class="hzo-section">
       <div class="hzo-container">
         <p>Normal measure copy…</p>
         <figure class="hzo-bleed"><img src="…" alt="…"></figure>
       </div>
     </section>

   ── The math ────────────────────────────────────────────────────────────
   `margin-inline: calc(50% - 50vw)` on a block-level child pulls each side out
   to the viewport edge: 50% is half the container's own width, 50vw is half
   the viewport, so the negative margin is exactly the gutter. No `width: 100vw`
   is needed (and it is deliberately NOT set — width:100vw double-counts the
   scrollbar and is the usual source of the phantom horizontal scrollbar).

   ⚠ OVERFLOW GUARD — READ THIS, IT IS THE ONE FOOTGUN.
   `vw` units include the vertical scrollbar's width. On a page WITH a vertical
   scrollbar, 50vw is ~8px wider than half the visible area, so a bleed element
   overhangs by about one scrollbar width and the page gains a horizontal
   scrollbar. The fix belongs on the page root, not here (a guard on .hzo-bleed
   itself cannot clip its own overhang, and a guard on .hzo-section would clip
   every deliberate overlap from _base/overlap.css). Add ONE line to the
   client's custom.css when the site uses bleed:

     body { overflow-x: clip; }

   Use `clip`, not `hidden`: `hidden` on an ancestor silently kills
   `position: sticky` inside it (the sticky header, the sticky-cta rail),
   `clip` does not. `.hzo-bleed-guard` below is that rule as a class for the
   cases where the guard needs to sit on a wrapper instead of on body.

   ── Half bleed ──────────────────────────────────────────────────────────
   --left / --right escape one side only and keep the container edge on the
   other, so an image can run off the page while its copy stays on the grid.
   This is the asymmetric-split move; it is the reason this file exists.

   ── Inner padding ───────────────────────────────────────────────────────
   A bled element is edge-to-edge, so any TEXT inside it needs its own gutter
   back. .hzo-bleed__inner re-applies the container's responsive padding
   without re-applying the max-width. Never put a bare paragraph directly in a
   .hzo-bleed — it will touch the glass on mobile.
   ────────────────────────────────────────────────────────────────────────── */

.hzo-bleed {
    margin-inline: calc(50% - 50vw);
}
.hzo-bleed--left {
    margin-inline-start: calc(50% - 50vw);
    margin-inline-end: 0;
}
.hzo-bleed--right {
    margin-inline-start: 0;
    margin-inline-end: calc(50% - 50vw);
}

/* Below 768px a half-bleed reads as a mistake rather than as art direction —
   the copy column is already full width, so an image escaping one side only
   looks misaligned. Both halves flatten to a full bleed on mobile. */
@media (max-width: 767.98px) {
    .hzo-bleed--left,
    .hzo-bleed--right { margin-inline: calc(50% - 50vw); }
}

/* Opt-in overflow guard (see the note above). Put it on body via custom.css,
   or on the nearest wrapper that owns the horizontal axis. */
.hzo-bleed-guard { overflow-x: clip; }

/* Gutter restore for content inside a bled element. Mirrors _base/container.css
   padding exactly — same five zones, same tokens — minus the max-width. */
.hzo-bleed__inner { padding: 0 var(--space-md); }
@media (min-width: 480px)  { .hzo-bleed__inner { padding: 0 var(--space-lg); } }
@media (min-width: 768px)  { .hzo-bleed__inner { padding: 0 var(--space-lg-2); } }
@media (min-width: 1024px) { .hzo-bleed__inner { padding: 0 var(--space-xl); } }
@media (min-width: 1250px) { .hzo-bleed__inner { padding: 0 var(--space-2xl); } }


/* --- _base/close.css --- */
/* ─── Shared close control — .hzo-close ────────────────────────────────
   The CANONICAL close button for overlays/dismissibles (modal, drawer,
   toast, mini-cart, banner, lightbox, mega-menu, mobile-menu). Distilled
   from the DOMINANT pattern shared by the modal / drawer / toast close
   buttons: a transparent square control, centered child svg, muted rest
   color, hover fill on --bg-alt with --text-heading foreground, a
   focus-visible ring on --focus-ring, and a color/background transition
   guarded by prefers-reduced-motion.

   NO-OP ON ARRIVAL (Wave 1): this class is intentionally NOT referenced by
   any existing markup, so it matches no DOM element and cannot change the
   computed-style fingerprint. It DEFINES the shared contract only.

   LATER WAVES: overlay/control components add `.hzo-close` to their close
   elements and DROP the duplicated declarations below, keeping only the
   per-component DELTAS that legitimately differ, e.g.:
     • size          — modal/drawer 44px · toast 28px · mini-cart 24px
     • border-radius — square (--radius-control) here · toast/mini-cart pill
     • position      — lightbox absolute top/right · banner flex-shrink
     • child svg size — 24 / 16 / 14 per component
     • hover model   — banner's color-mix currentColor + lightbox's white-on
                       -scrim are DELIBERATE deltas, not part of this base
   Tokens only — no hardcoded aesthetic literals. */
.hzo-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: var(--radius-control);
    cursor: pointer;
    transition: background var(--transition-base) ease, color var(--transition-base) ease;
}
.hzo-close:hover { background: var(--bg-alt); color: var(--text-heading); }
.hzo-close:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 1px; }
.hzo-close > svg { display: block; }

@media (prefers-reduced-motion: reduce) {
    .hzo-close { transition: none; }
}


/* --- _base/container.css --- */
/* ─── Container ───────────────────────────────────────────────────────
   Horizontal padding scales across five zones via the QA breakpoints
   (480, 768, 1024, 1250) — 16 / 24 / 32 / 48 / 64. Caps at 64px from
   1250 onward since wider screens don't need wider gutters. */
.hzo-container {
    max-width: var(--container-max-width);
    margin: 0 auto;
    padding: 0 var(--space-md);
}
@media (min-width: 480px)  { .hzo-container { padding: 0 var(--space-lg); } }
@media (min-width: 768px)  { .hzo-container { padding: 0 var(--space-lg-2); } }
@media (min-width: 1024px) { .hzo-container { padding: 0 var(--space-xl); } }
@media (min-width: 1250px) { .hzo-container { padding: 0 var(--space-2xl); } }


/* --- _base/core-markup-normalize.css --- */
/* ─── Core-markup normalization (always-loaded) ─────────────────────────────
   WordPress emits a handful of class names into content that IT does not
   fully style — it either leaves them to the active theme, or (in the emoji
   case) ships the rule from a hook a plugin can unhook. This engine replaces
   the theme, and hozio-studio.php's "Frontend cleanup" section unhooks core
   defaults. Between those two facts sits a gap: core-generated markup sitting
   in client post_content with no CSS owner at all.

   That gap is not theoretical. On 2026-08-28 (v1.11.1) eight pasted
   `<img class="emoji">` tags rendered at 667 px square on a live client page
   because the engine had unhooked print_emoji_styles and nothing else claimed
   the rule. This sheet claims them, under our own handle, so the fleet stops
   depending on core winning a race against every other plugin on the site.

   SCOPE — only markup that (a) core generates, (b) is broken or invisible
   without CSS, and (c) is not already owned by an .hzo-* component. Core BLOCK
   styles (wp-block-*) are deliberately NOT re-implemented here: per-block CSS
   still loads correctly for anything rendered through render_block(), and
   guessing at core's block output would calcify that guess into every client
   site — the failure mode CLAUDE.md's "Before touching CSS" section is about.
   ────────────────────────────────────────────────────────────────────────── */

/* ─── WP emoji / smiley images ──────────────────────────────────────────────
   Byte-for-byte the rule core's wp_enqueue_emoji_styles() emits, re-declared
   under our handle. Deliberate duplication: core's copy covers a client whose
   components/ folder is missing (the primitives.css fallback path in
   hzostudio_enqueue_components(), where no _base sheet enqueues at all), ours
   covers any other plugin on the site unhooking core's. Either alone is a
   single point of failure; together, neither is.

   `!important` is core's own choice here and is kept for the same reason it
   exists there: these tags land inside arbitrary content, and the rule has to
   outrank whatever generic `img` rule a theme, page builder or client
   custom.css has already declared. It is scoped to two exact class tokens on
   one element type, so it cannot leak.

   The matching PHP (includes/core-markup-normalize.php) removes these tags
   from rendered output entirely — this sheet is what catches the ones it
   deliberately declines to touch (an emoji <img> with an empty alt), plus
   anything reaching the browser through a path that filter does not run on. */
img.wp-smiley,
img.emoji {
    display: inline !important;
    border: none !important;
    box-shadow: none !important;
    height: 1em !important;
    width: 1em !important;
    margin: 0 0.07em !important;
    vertical-align: -0.1em !important;
    background: none !important;
    padding: 0 !important;
}

/* ─── .screen-reader-text ───────────────────────────────────────────────────
   Core's visually-hidden class — emitted by get_search_form(), comment forms,
   nav markup and pagination, and by this engine at
   includes/wc-shortcodes.php:275 (the WooCommerce quantity label). Themes are
   expected to style it; unstyled it does not degrade quietly, it renders the
   hidden label as visible body copy in the middle of the page.

   Same clip technique as .hzo-sr-only in sr-only.css — kept in sync with it
   deliberately rather than aliased, because that file documents the utility we
   author against and this one documents markup we merely receive. If the
   technique changes there, change it here too. */
.screen-reader-text {
    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;
}

/* Focus reveal — core ships .screen-reader-text on skip links, which become a
   silent keyboard trap if they stay hidden while focused. */
.screen-reader-text:focus,
.screen-reader-text:focus-within {
    position: static !important;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    clip-path: none;
    white-space: normal;
}

/* ─── Classic caption + alignment classes ───────────────────────────────────
   [caption] shortcode output and the classic alignment classes. This fleet
   forces the raw-text Classic editor, so both still appear in authored content
   and in anything pasted from an older site. Core emits the markup and leaves
   the styling to the theme; with no theme in that role, an uncaptioned float
   collapses to full width and caption text renders as an unstyled paragraph.

   Tokens only, no fixed sizes: the caption inherits the type ladder and steps
   down relatively, so it tracks whatever context it lands in. */
.wp-caption {
    max-width: 100%;
    margin-bottom: var(--space-md);
}

.wp-caption img {
    display: block;
    max-width: 100%;
    height: auto;
}

.wp-caption-text,
.wp-caption .wp-caption-text {
    margin-top: var(--space-xs);
    margin-bottom: 0;
    font-size: 0.875em;
    line-height: var(--lh-snug);
    color: var(--text-muted);
}

.alignnone {
    max-width: 100%;
    height: auto;
}

.aligncenter {
    display: block;
    margin-inline: auto;
    max-width: 100%;
    height: auto;
}

.alignleft {
    float: left;
    max-width: 100%;
    height: auto;
    margin: 0 var(--space-md) var(--space-sm) 0;
}

.alignright {
    float: right;
    max-width: 100%;
    height: auto;
    margin: 0 0 var(--space-sm) var(--space-md);
}

/* Floats are a content-flow primitive with no owner further down the cascade —
   without a clear, the next section's heading rides up beside a floated image.
   Scoped to the engine's section skeleton so it cannot affect component
   internals that use float deliberately. */
.hzo-section::after {
    content: "";
    display: table;
    clear: both;
}

/* Narrow viewports: a floated image beside body copy leaves neither enough
   measure to read. Drop both floats to full-width blocks at the same
   breakpoint long-form-content.css uses for its table reflow. */
@media (max-width: 767px) {
    .alignleft,
    .alignright {
        float: none;
        display: block;
        margin-inline: auto;
        margin-bottom: var(--space-md);
    }
}


/* --- _base/dark-section.css --- */
/* ─── Dark section (class-scoped; only activates via .hzo-section--dark) ── */
/* Sections stack flush: cancel WordPress's default block-gap margin between
   stacked top-level sections. Each .hzo-section owns its vertical rhythm via
   padding, so sections must meet with NO gap; without this the 24px block-gap
   inserts a margin and the page background shows through as a seam between
   sections. Specificity (0,2,0) beats WP's `.is-layout-flow > * + *` (0,1,0),
   so it wins regardless of stylesheet order. */
:is(.is-layout-flow, .is-layout-constrained) > .hzo-section { margin-block-start: 0; }

.hzo-section--dark { background: var(--bg-dark-section); color: #fff; }
.hzo-section--dark h1, .hzo-section--dark h2, .hzo-section--dark h3, .hzo-section--dark h4, .hzo-section--dark h5, .hzo-section--dark h6 { color: var(--heading-color-on-dark, #fff); }
.hzo-section--dark p { color: rgba(255,255,255,0.85); }
.hzo-section--dark a { color: #fff; }
.hzo-section--dark .hzo-eyebrow,
.hzo-section--dark h1.hzo-eyebrow,
.hzo-section--dark h2.hzo-eyebrow,
.hzo-section--dark h3.hzo-eyebrow,
.hzo-section--dark h4.hzo-eyebrow { color: var(--accent) !important; }

/* ─── Dark-surface button auto-treatment (token remap) ──────────────────
   BARE .hzo-btn--outline / --ghost / --secondary on a brand-colored dark field
   render brand-on-brand and effectively vanish (~1:1): the base rules resolve
   --btn-outline-color / --btn-ghost-color / --btn-secondary-bg to var(--brand),
   which equals the section's own --bg-dark-section. A correct fix already
   exists as the opt-in `.hzo-btn--on-dark.*` rules (buttons.css:171-195), but
   authors must remember the class and shipped patterns (rate-menu, contact-
   split, Fluent Forms prev/upload, WooCommerce) don't.
   Remapping the *component tokens* here makes those base rules resolve to the
   on-dark values with NO .hzo-btn--on-dark needed — text/border go white,
   secondary flips to a white fill with brand text, hovers stay readable.
   Values read the SAME --btn-on-dark-* family the explicit on-dark rules use,
   so the bare and opt-in paths share one source of truth and can't visually
   diverge (a client theming --btn-on-dark-* themes both).
   Scope note (specificity/order): single class (0,1,0) is deliberate. Custom
   properties inherit from the nearest ancestor, so this surface-level
   declaration shadows any :root / tokens.overrides token for its whole subtree
   — no specificity boost is needed for it to win inside dark surfaces, and
   keeping it low lets a client still re-theme by targeting the surface.
   Folded onto :is(.hzo-section--dark, .hzo-surface--dark) so the opt-in
   surface-dark utility (surface-dark.css remaps text tokens but not these) is
   fixed at the same time. Only outline/ghost/secondary read these tokens —
   primary (accent/green) stays green, and danger/link are untouched. */
:is(.hzo-section--dark, .hzo-surface--dark) {
    --btn-outline-color:   var(--btn-on-dark-accent, #fff);          /* outline: white text + white border (rest); white fill on hover */
    --btn-ghost-color:     var(--btn-on-dark-accent, #fff);          /* ghost: white text (rest + hover) */
    --btn-ghost-hover:     rgba(255, 255, 255, 0.2);                 /* ghost hover: translucent white wash (matches on-dark ghost hover) */
    --btn-secondary-bg:    var(--btn-on-dark-bg, #fff);              /* secondary: white fill + white border */
    --btn-secondary-text:  var(--btn-on-dark-text, var(--brand));    /* secondary + outline-hover text: brand on white */
    --btn-secondary-hover: color-mix(in srgb, var(--btn-on-dark-bg, #fff) 75%, var(--btn-on-dark-text, var(--brand))); /* secondary hover: light brand tint (matches on-dark) */
}


/* --- _base/doc-layout-toc.css --- */
/* ─── Doc layout + sticky TOC sidebar (legal pages, long-form articles) ──
   Two-column grid: TOC sidebar on the left, document body on the right.
   The sidebar is sticky on desktop, stacks above the body on mobile.
   The active section is highlighted via IntersectionObserver scrollspy
   in main.js. Section numbers (.hzo-section-num) sit next to each <h2>
   in a flex container, aria-hidden so screen readers read only the
   heading. Both the inline number and the matching TOC number are
   hand-authored to keep the markup static + crawlable without JS. */
.hzo-doc-layout {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-xl);
    align-items: start;
}
@media (min-width: 1024px) {
    .hzo-doc-layout {
        grid-template-columns: 240px 1fr;
        gap: var(--space-3xl);
    }
}

.hzo-doc-toc { position: static; }
@media (min-width: 1024px) {
    .hzo-doc-toc {
        position: sticky;
        top: 104px;
        max-height: calc(100vh - 120px);
        overflow-y: auto;
        padding-right: 8px;
    }
}
.hzo-doc-toc::-webkit-scrollbar { width: 4px; }
.hzo-doc-toc::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }

.hzo-doc-toc__label {
    font-family: var(--font-body);
    font-size: var(--fs-50);
    font-weight: var(--fw-semibold, 600);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
    margin: 0 0 var(--space-md);
    padding-bottom: var(--space-sm);
    border-bottom: 1px solid var(--border);
}

.hzo-doc-toc__list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
}

.hzo-doc-toc__item { position: relative; }

/* !important on color: fights a:visited from per-client main.css.
   Matching :visited rule below preserves the active/idle colors. */
.hzo-doc-toc__link {
    display: flex;
    align-items: baseline;
    gap: 12px;
    padding: 8px 0 8px 14px;
    color: var(--text-muted) !important;
    text-decoration: none;
    font-family: var(--font-body);
    font-size: var(--fs-100);
    line-height: 1.4;
    border-left: 2px solid var(--border);
    transition: color 0.15s, border-color 0.15s;
}
.hzo-doc-toc__link:hover { color: var(--text-body) !important; }
.hzo-doc-toc__link.is-active {
    color: var(--brand-text) !important;
    border-left-color: var(--brand-text);
    font-weight: var(--fw-semibold, 600);
}
.hzo-doc-toc__link:visited { color: var(--text-muted) !important; }
.hzo-doc-toc__link:visited:hover { color: var(--text-body) !important; }
.hzo-doc-toc__link.is-active:visited { color: var(--brand-text) !important; }

.hzo-doc-toc__num {
    font-family: var(--font-body);
    font-size: var(--fs-50);
    font-weight: var(--fw-medium, 500);
    color: var(--text-muted);
    min-width: 22px;
    flex-shrink: 0;
}
.hzo-doc-toc__link.is-active .hzo-doc-toc__num { color: var(--brand); }

/* Section heading row inside the doc body — flex container that holds
   the decorative number + the real heading tag, baseline-aligned. The
   number is aria-hidden so screen readers read only the h2. */
.hzo-doc-section-head {
    display: flex;
    align-items: baseline;
    gap: var(--space-md);
    margin-bottom: var(--space-md);
}
.hzo-section-num {
    font-family: var(--font-body);
    font-size: var(--fs-100);
    font-weight: var(--fw-semibold, 600);
    color: var(--text-muted);
    flex-shrink: 0;
    line-height: 1.4;
}
/* Reset the eyebrow's default bottom margin when it sits inside a
   section-head flex row — the row owns spacing, not the eyebrow. */
.hzo-doc-section-head > .hzo-eyebrow,
.hzo-doc-section-head > h1.hzo-eyebrow,
.hzo-doc-section-head > h2.hzo-eyebrow,
.hzo-doc-section-head > h3.hzo-eyebrow,
.hzo-doc-section-head > h4.hzo-eyebrow {
    margin: 0 !important;
}

/* Section adjacency rule — when [hzo_final_cta] (or any .hzo-section)
   follows another .hzo-section, collapse the doubled padding (80+80=160
   on desktop, 48+48=96 on mobile) by stripping the second section's
   top padding. The preceding section's bottom padding becomes the sole
   gap, scaling responsively via the .hzo-section ladder (48/64/80).
   System-level fix replacing per-page padding-bottom:0 hacks. */
.hzo-section + .hzo-section,
.hzo-section + .hzo-final-cta {
    padding-top: 0;
}
/* …EXCEPT a colored surface band (--pale/--tint/--dark) or a hero must ALWAYS
   keep its own top padding, even when it follows another section — otherwise
   its background sits flush against the content (the "no top padding on
   colored sections" bug). Only plain white-on-white sections collapse.
   Restored responsively to match the .hzo-section ladder (48 / 64 / 80). */
.hzo-section + .hzo-section--pale,
.hzo-section + .hzo-section--tint,
.hzo-section + .hzo-section--dark,
.hzo-section + .hzo-hero {
    padding-top: var(--space-xl);
}
@media (min-width: 768px) {
    .hzo-section + .hzo-section--pale,
    .hzo-section + .hzo-section--tint,
    .hzo-section + .hzo-section--dark,
    .hzo-section + .hzo-hero { padding-top: var(--space-2xl); }
}
@media (min-width: 1250px) {
    .hzo-section + .hzo-section--pale,
    .hzo-section + .hzo-section--tint,
    .hzo-section + .hzo-section--dark,
    .hzo-section + .hzo-hero { padding-top: var(--space-3xl); }
}
/* …and the mirror case: a PLAIN section that FOLLOWS a colored surface (or
   hero) must also keep its top padding. The collapse above only holds for true
   white-on-white runs; when the background CHANGES at the boundary
   (colored → plain), zeroing the plain section's top padding jams its content
   against the color edge (the "What customers say follows the dark band" bug).
   Restore responsively to match the ladder (48 / 64 / 80). */
.hzo-section--pale + .hzo-section,
.hzo-section--tint + .hzo-section,
.hzo-section--dark + .hzo-section,
.hzo-hero + .hzo-section {
    padding-top: var(--space-xl);
}
@media (min-width: 768px) {
    .hzo-section--pale + .hzo-section,
    .hzo-section--tint + .hzo-section,
    .hzo-section--dark + .hzo-section,
    .hzo-hero + .hzo-section { padding-top: var(--space-2xl); }
}
@media (min-width: 1250px) {
    .hzo-section--pale + .hzo-section,
    .hzo-section--tint + .hzo-section,
    .hzo-section--dark + .hzo-section,
    .hzo-hero + .hzo-section { padding-top: var(--space-3xl); }
}

/* Doc page card — contained card with bg-alt + radius. Used for the
   hero and closing CTA on long-form doc pages (privacy, terms). Inner
   padding scales across five zones via QA breakpoints (480, 768, 1024,
   1250). Tightens on small phones, opens up on desktop, caps at
   64/48 from 1250 onward. */
.hzo-doc-card {
    background: var(--bg-alt);
    border-radius: var(--radius-card);
    padding: var(--space-lg-2) var(--space-md);
    text-align: center;
}
@media (min-width: 480px)  { .hzo-doc-card { padding: var(--space-xl-2) var(--space-lg); } }
@media (min-width: 768px)  { .hzo-doc-card { padding: var(--space-xl) var(--space-lg-2); } }
@media (min-width: 1024px) { .hzo-doc-card { padding: var(--space-2xl-2) var(--space-xl-2); } }
@media (min-width: 1250px) { .hzo-doc-card { padding: var(--space-2xl) var(--space-xl); } }

/* Vertical rhythm between adjacent doc-page siblings (.hzo-doc-card,
   .hzo-doc-layout) inside a single .hzo-section > .hzo-container.
   Same five-zone scale as the card padding. First sibling has no top
   gap — section padding-top handles that breathing room. */
.hzo-doc-card + .hzo-doc-card,
.hzo-doc-card + .hzo-doc-layout,
.hzo-doc-layout + .hzo-doc-card {
    margin-top: var(--space-lg-2);
}
@media (min-width: 480px) {
    .hzo-doc-card + .hzo-doc-card,
    .hzo-doc-card + .hzo-doc-layout,
    .hzo-doc-layout + .hzo-doc-card { margin-top: var(--space-xl-2); }
}
@media (min-width: 768px) {
    .hzo-doc-card + .hzo-doc-card,
    .hzo-doc-card + .hzo-doc-layout,
    .hzo-doc-layout + .hzo-doc-card { margin-top: var(--space-xl); }
}
@media (min-width: 1024px) {
    .hzo-doc-card + .hzo-doc-card,
    .hzo-doc-card + .hzo-doc-layout,
    .hzo-doc-layout + .hzo-doc-card { margin-top: var(--space-2xl-2); }
}
@media (min-width: 1250px) {
    .hzo-doc-card + .hzo-doc-card,
    .hzo-doc-card + .hzo-doc-layout,
    .hzo-doc-layout + .hzo-doc-card { margin-top: var(--space-2xl); }
}

/* CTA variant of .hzo-doc-card — used as the closing "Get in touch"
   block on long-form doc pages. Identical to the base card on desktop;
   on mobile (<768) the card flips to left-aligned and stacks the email
   and phone contact links onto separate lines so longer email addresses
   don't get awkwardly wrapped or centered on narrow screens. */
@media (max-width: 767px) {
    .hzo-doc-card--cta {
        text-align: left;
    }
    .hzo-doc-card--cta .hzo-doc-card__contact a {
        display: block;
    }
    .hzo-doc-card--cta .hzo-doc-card__sep {
        display: none;
    }
}

/* Body sections — separated by top rules + generous vertical rhythm.
   scroll-margin-top offsets the anchor jump so the section title lands
   below the sticky header (which is ~80px). */
.hzo-doc-body > section {
    margin-top: var(--space-3xl);
    padding-top: var(--space-3xl);
    border-top: 1px solid var(--border);
    scroll-margin-top: 100px;
}
.hzo-doc-body > section:first-child {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
}
.hzo-doc-body h2:not(.hzo-eyebrow):first-child { margin-top: 0; }


/* --- _base/emphasis-decoration.css --- */
/* ─── Emphasis / decoration utilities ────────────────────────────────────
   Opt-in inline decorations keyed to --emphasis (a client's decorative accent;
   falls back to --accent if a client hasn't defined one). Re-key --emphasis per
   client in main.css for a distinct decorative hue. */

/* Keyword underline — clean rule beneath one/two key words; wraps per line and
   sits behind descenders. */
.hzo-mark {
    background-image: linear-gradient(var(--emphasis, var(--accent)), var(--emphasis, var(--accent)));
    background-repeat: no-repeat;
    background-position: 0 92%;
    background-size: 100% 0.12em;
    padding-bottom: 0.02em;
}

/* Stat brackets — [ ] framing a standout number. Wrap a number; the brackets
   carry the emphasis color, the number keeps its own. */
.hzo-bracket {
    position: relative;
    display: inline-block;
    padding: 0 0.4em;
}
.hzo-bracket::before,
.hzo-bracket::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    width: 0.2em;
    border: 3px solid var(--emphasis, var(--accent));
}
.hzo-bracket::before { left: 0;  border-right: 0; }
.hzo-bracket::after  { right: 0; border-left: 0; }

/* Pull-quote — editorial moment, emphasis left rule, heading font. */
.hzo-pullquote {
    font-family: var(--font-heading);
    font-size: clamp(1.25rem, 2.2vw, 1.6rem);
    font-weight: var(--fw-bold, 700);
    line-height: 1.3;
    color: var(--text-heading);
    border-left: 3px solid var(--emphasis, var(--accent));
    padding-left: var(--space-lg);
    margin: var(--space-xl) 0;
}
.hzo-section--dark .hzo-pullquote { color: #fff; }

/* ═══════════════════════════════════════════════════════════════════════
   SITE HEADER · NAV · DROPDOWN · HAMBURGER · MOBILE MENU
   Structure ported from the TGA production header. Uses hzo-* classes
   throughout. Breakpoints: hamburger below 1250px, horizontal nav above.
   ═══════════════════════════════════════════════════════════════════════ */

/* Cancel WP's default block-gap between direct children of .wp-site-blocks.
   WP emits `:where(.wp-site-blocks) > * { margin-block-start: 24px; }` from
   the block-layout system (driven by --wp--style--block-gap, default 24px).
   For block templates that compose utility-bar + header + main + sections +
   footer as direct siblings, this creates visible 24px white bands between
   every section (page background showing through between brand-colored
   regions). Our header/main/section/footer carry their own padding/spacing
   for whitespace control — we don't want WP's auto-gap on top of that. */
.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }

.hzo-site-header {
    background: var(--nav-header-bg, #fff);
    box-shadow: var(--nav-header-shadow, 0 1px 4px rgba(0, 0, 0, 0.08));
    position: sticky;
    top: 0;
    z-index: var(--z-header);
}
.hzo-site-header__inner {
    display: flex;
    align-items: center;
    min-height: var(--header-height-mobile, 72px);
    gap: 16px;
}
@media (min-width: 1250px) {
    .hzo-site-header__inner { min-height: var(--header-height-desktop, 80px); }
}


/* --- _base/gradient-scrims.css --- */
/* ─── Directional gradient scrims (opt-in on --image surfaces) ─────────
   Art-directed alternative to the flat wash: full strength on the text
   side, fading out so the photo breathes on the far side. Add alongside
   .hzo-hero--image / .hzo-section--image:
     .hzo-scrim--left     copy sits left  → dark anchors left
     .hzo-scrim--right    copy sits right → dark anchors right
     .hzo-scrim--bottom   dark rises from the bottom (tiles, captions)
     .hzo-scrim--top      dark descends from the top
   Strength rides the same knob as the flat wash (--hzo-section-scrim,
   default 60%); --scrim-color re-tints (default --brand-deep). Below 768px
   the side scrims flatten to a stronger uniform wash — copy spans the full
   width there, and a side gradient would leave its right half unreadable.
   No modifier = the original flat wash (back-compat; nothing shifts on
   existing pages).

   STRENGTH is the second axis and composes with all four directions:
     .hzo-scrim--weak     35%  the photo is the subject; short, large copy
     .hzo-scrim--base     60%  the shipped default — a no-op by construction
     .hzo-scrim--strong   82%  copy-heavy band, or a busy/light photo
   These set --hzo-section-scrim from the --scrim-* ladder in tokens.css, so the
   strength is a class on the section instead of a percentage in an inline style.
   They work on the flat wash too (no direction modifier). AA is NOT implied by
   any of them — contrast depends on the photograph, so measure the rendered
   band. */

/* Strength stops. Declared on the SECTION (the pseudo inherits) and before the
   direction rules so nothing here depends on source order among them. */
.hzo-hero--image.hzo-scrim--weak,
.hzo-section--image.hzo-scrim--weak   { --hzo-section-scrim: var(--scrim-weak, 35%); }
.hzo-hero--image.hzo-scrim--base,
.hzo-section--image.hzo-scrim--base   { --hzo-section-scrim: var(--scrim-base, 60%); }
.hzo-hero--image.hzo-scrim--strong,
.hzo-section--image.hzo-scrim--strong { --hzo-section-scrim: var(--scrim-strong, 82%); }

.hzo-hero--image.hzo-scrim--left::before,
.hzo-section--image.hzo-scrim--left::before {
    background: linear-gradient(to right,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) min(calc(var(--hzo-section-scrim, 60%) * 1.5), 95%), transparent) 0%,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) var(--hzo-section-scrim, 60%), transparent) 38%,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) calc(var(--hzo-section-scrim, 60%) * 0.35), transparent) 68%,
        transparent 100%);
}
.hzo-hero--image.hzo-scrim--right::before,
.hzo-section--image.hzo-scrim--right::before {
    background: linear-gradient(to left,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) min(calc(var(--hzo-section-scrim, 60%) * 1.5), 95%), transparent) 0%,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) var(--hzo-section-scrim, 60%), transparent) 38%,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) calc(var(--hzo-section-scrim, 60%) * 0.35), transparent) 68%,
        transparent 100%);
}
.hzo-hero--image.hzo-scrim--bottom::before,
.hzo-section--image.hzo-scrim--bottom::before {
    background: linear-gradient(to top,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) min(calc(var(--hzo-section-scrim, 60%) * 1.5), 95%), transparent) 0%,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) calc(var(--hzo-section-scrim, 60%) * 0.6), transparent) 45%,
        transparent 85%);
}
.hzo-hero--image.hzo-scrim--top::before,
.hzo-section--image.hzo-scrim--top::before {
    background: linear-gradient(to bottom,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) min(calc(var(--hzo-section-scrim, 60%) * 1.5), 95%), transparent) 0%,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) calc(var(--hzo-section-scrim, 60%) * 0.6), transparent) 45%,
        transparent 85%);
}
@media (max-width: 767.98px) {
    .hzo-hero--image.hzo-scrim--left::before,
    .hzo-section--image.hzo-scrim--left::before,
    .hzo-hero--image.hzo-scrim--right::before,
    .hzo-section--image.hzo-scrim--right::before {
        background: color-mix(in srgb, var(--scrim-color, var(--brand-deep)) min(calc(var(--hzo-section-scrim, 60%) * 1.25), 90%), transparent);
    }
}


/* --- _base/icon-sizing.css --- */
/* ─── Icon sizing scale ────────────────────────────────────────────────
   One icon family per site (art-direction §3) — this governs SIZE only, never
   family mixing. Icons inherit text color (currentColor) and track font-size via
   em, so an icon beside text scales + aligns with that text by default. Zero new
   tokens. a11y: decorative => aria-hidden="true"; meaningful => role="img" +
   aria-label. Wrap an inline SVG: <span class="hzo-icon hzo-icon--lg">…svg…</span>. */
.hzo-icon {
    display: inline-flex;
    flex-shrink: 0;
    width: 1em;
    height: 1em;
    vertical-align: -0.125em;   /* sit on the adjacent text baseline */
    line-height: var(--lh-none);
}
.hzo-icon > svg { width: 100%; height: 100%; display: block; fill: none; stroke: currentColor; }
.hzo-icon--filled > svg { fill: currentColor; stroke: none; }  /* solid (non-stroke) icons */
.hzo-icon--xs { font-size: 0.75em; }
.hzo-icon--sm { font-size: 0.875em; }
.hzo-icon--md { font-size: 1em; }      /* default */
.hzo-icon--lg { font-size: 1.5em; }
.hzo-icon--xl { font-size: 2em; }


/* --- _base/layout-primitives.css --- */
/* ─── Layout primitives — Stack / Cluster / Spacer ────────────────────
   Composition utilities so client builds compose spacing instead of sprinkling
   bespoke margins. Every gap/height maps to the --space ladder — zero new
   tokens. See art-direction.md "Compose spacing, don't hand-roll margins". */

/* Stack — vertical rhythm between direct children (gap-based flex column).
   Default gap = the base step (--space-md); modifiers step the ladder. */
.hzo-stack { display: flex; flex-direction: column; gap: var(--space-md); }
.hzo-stack--xs  { gap: var(--space-xs); }
.hzo-stack--sm  { gap: var(--space-sm); }
.hzo-stack--md  { gap: var(--space-md); }
.hzo-stack--lg  { gap: var(--space-lg); }
.hzo-stack--xl  { gap: var(--space-xl); }
.hzo-stack--2xl { gap: var(--space-2xl); }
.hzo-stack--3xl { gap: var(--space-3xl); }
/* --split: fill the parent and push the last child to the bottom
   (card footers, sidebar CTAs). Use on a stack inside a flex/grid cell. */
.hzo-stack--split { flex: 1 1 auto; }
.hzo-stack--split > :last-child { margin-top: auto; }

/* Cluster — horizontal group that wraps; gap + alignment. Default gap =
   --space-sm, centered cross-axis. */
.hzo-cluster { display: flex; flex-wrap: wrap; gap: var(--space-sm); align-items: center; }
.hzo-cluster--xs  { gap: var(--space-xs); }
.hzo-cluster--sm  { gap: var(--space-sm); }
.hzo-cluster--md  { gap: var(--space-md); }
.hzo-cluster--lg  { gap: var(--space-lg); }
.hzo-cluster--xl  { gap: var(--space-xl); }
.hzo-cluster--center   { justify-content: center; }
.hzo-cluster--between  { justify-content: space-between; }
.hzo-cluster--end      { justify-content: flex-end; }
.hzo-cluster--start    { align-items: flex-start; }
.hzo-cluster--baseline { align-items: baseline; }  /* icon + text rows */

/* Spacer — explicit vertical gap for the rare case a stack doesn't fit
   (e.g. between unrelated sibling blocks not in a stack). */
.hzo-spacer { display: block; flex-shrink: 0; height: var(--space-md); }
.hzo-spacer--xs  { height: var(--space-xs); }
.hzo-spacer--sm  { height: var(--space-sm); }
.hzo-spacer--md  { height: var(--space-md); }
.hzo-spacer--lg  { height: var(--space-lg); }
.hzo-spacer--xl  { height: var(--space-xl); }
.hzo-spacer--2xl { height: var(--space-2xl); }
.hzo-spacer--3xl { height: var(--space-3xl); }


/* --- _base/long-form-content.css --- */
/* ─── Long-form content (legal pages, articles) ────────────────────── */
/* Heading spacing inside hzo-sections — excludes the eyebrow pattern */
.hzo-section h2:not(.hzo-eyebrow) {
    margin-top: var(--space-xl);
    margin-bottom: var(--space-md);
}
.hzo-section h3:not(.hzo-eyebrow) {
    margin-top: var(--space-lg);
    margin-bottom: var(--space-sm);
}
.hzo-section h4:not(.hzo-eyebrow) {
    margin-top: var(--space-md);
    margin-bottom: var(--space-sm);
}
.hzo-section h2:not(.hzo-eyebrow):first-child,
.hzo-section h3:not(.hzo-eyebrow):first-child {
    margin-top: 0;
}

/* Block-table borders use the global border token (pale gray, not browser default) */
.hzo-section .wp-block-table table {
    border-collapse: collapse;
    width: 100%;
}
.hzo-section .wp-block-table thead tr {
    border-bottom: 2px solid var(--border);
}
.hzo-section .wp-block-table tbody tr {
    border-bottom: 1px solid var(--border);
}
.hzo-section .wp-block-table th,
.hzo-section .wp-block-table td {
    border: none;
    padding: 12px 16px;
    text-align: left;
    vertical-align: top;
}

/* Block-table mobile stacking — rows collapse to labeled-cell cards.
   data-label is injected at runtime by main.js from each table's thead. */
@media (max-width: 767px) {
    .hzo-section .wp-block-table table,
    .hzo-section .wp-block-table thead,
    .hzo-section .wp-block-table tbody,
    .hzo-section .wp-block-table tr,
    .hzo-section .wp-block-table th,
    .hzo-section .wp-block-table td {
        display: block;
        width: auto;
    }
    .hzo-section .wp-block-table thead {
        position: absolute;
        clip: rect(0, 0, 0, 0);
        height: 1px;
        width: 1px;
        overflow: hidden;
    }
    .hzo-section .wp-block-table tbody tr {
        margin-bottom: var(--space-md);
        padding: var(--space-md) 0;
        border-bottom: 1px solid var(--border);
    }
    .hzo-section .wp-block-table tbody tr:last-child {
        border-bottom: none;
    }
    .hzo-section .wp-block-table td {
        padding: var(--space-xs) 0;
        border: none;
    }
    .hzo-section .wp-block-table td::before {
        content: attr(data-label);
        display: block;
        font-weight: var(--fw-bold, 700);
        font-size: var(--fs-50);
        color: var(--text-heading);
        text-transform: uppercase;
        letter-spacing: 0.05em;
        margin-bottom: 4px;
    }
    .hzo-section .wp-block-table td:empty {
        display: none;
    }
}


/* --- _base/measure.css --- */
/* ─── Measure — the line-length ladder ─────────────────────────────────
   Wave S spatial vocabulary. Line length is a typographic decision with one
   right answer per role, and until now the system carried exactly one value
   (--prose-max-width: 65ch, consumed by .hzo-prose) plus a scattering of
   hand-written `max-width: 720px` inline styles in the block patterns.

   Three steps, because there are three jobs:
     --measure-45 / .hzo-measure-45   45ch  a headline or a pull-quote. Short
                                            lines are what make a big type size
                                            read as a statement instead of as a
                                            paragraph set in the wrong size.
     --measure-65 / .hzo-measure-65   65ch  body copy. The default, and the
                                            same value --prose-max-width has
                                            always carried.
     --measure-75 / .hzo-measure-75   75ch  dense reference text — tables,
                                            long-form legal, documentation.
   .hzo-measure with no step is 65ch. Add .hzo-measure--center to centre the
   block (margin-inline: auto), which is what a centred section almost always
   wants and what a bare `text-align: center` does NOT give you.

   ch UNITS, NOT px: a measure expressed in px is only correct at one font
   size, so it silently breaks the moment the design widget moves the type
   scale or a client sets a wider heading font. ch tracks the element's own
   font.

   These are max-widths only — no padding, no display change, nothing that
   could disturb a layout it is added to.
   ────────────────────────────────────────────────────────────────────────── */

.hzo-measure,
.hzo-measure-65 { max-width: var(--measure-65, 65ch); }
.hzo-measure-45 { max-width: var(--measure-45, 45ch); }
.hzo-measure-75 { max-width: var(--measure-75, 75ch); }
.hzo-measure--center { margin-inline: auto; }


/* --- _base/overlap.css --- */
/* ─── Overlap — pull utilities + the content z-index scale ─────────────
   Wave S spatial vocabulary. Boundary-breaking is the cheapest premium signal
   there is: one element that refuses to stay inside its section. Every band
   sitting neatly in its own box is what makes a page read as a stack of
   rectangles no matter how good the individual sections are.

   Pulling is a two-part move and BOTH parts are needed. A negative margin
   alone puts the element in the right place and then paints it underneath the
   neighbour's background, so the effect silently disappears. That is why
   every .hzo-pull-* here also opens a positioning context and raises the
   element. It is one class, not a recipe you have to remember.

     <section class="hzo-section hzo-section--dark">…</section>
     <section class="hzo-section">
       <div class="hzo-container">
         <div class="hzo-card hzo-pull-2">…</div>   <!-- rises into the dark band -->
       </div>
     </section>

   ── The scale ───────────────────────────────────────────────────────────
     .hzo-pull-1   48px   a nudge — a card breaking its section line
     .hzo-pull-2   64px   the default overlap; a panel straddling two surfaces
     .hzo-pull-3   80px   a hero card sitting deep in the band above
     .hzo-pull-4  120px   the statement move; use once per page at most
   Push variants pull the NEXT sibling up into THIS element instead:
     .hzo-push-1 … .hzo-push-4  (negative margin-bottom, same steps)

   RESPONSIVE BEHAVIOUR: below 768px every step clamps to --pull-1. A 120px
   overlap on a 375px screen is not a composition, it is two things on top of
   each other. The clamp lives in the CLASS, not in the token — tokens are not
   responsive, and a --pull-4 read from an inline style would hard-code 120px
   at every width.

   ── The content z-index scale ───────────────────────────────────────────
   The existing --z-100…700 / --z-header / --z-modal ladder governs CHROME
   (sticky bars, drawers, modals, toasts) and starts at 10. Content layering
   inside a section needs a vocabulary an order of magnitude below that, and
   it was missing — which is why component CSS reaches for bare `z-index: 1`.

     --z-behind  -1   decorative fill BEHIND the section's own content
     --z-base     0   the content plane
     --z-above    1   the element that must sit on top of its neighbours

   Utilities: .hzo-z-behind / .hzo-z-base / .hzo-z-above. Each opens a
   positioning context, because a z-index on a static element does nothing —
   the second-most-common silent no-op in this system after a bare modifier.

   ⚠ .hzo-z-behind needs a stacking context on the ancestor or it will slide
   behind the SECTION's background too, not just behind its content. Put
   .hzo-isolate on the section that owns the decoration.

   ⚠ AN ANCESTOR WITH overflow: hidden CLIPS EVERY PULL. If an overlap does
   not appear, that is the first thing to check — .hzo-media-band and several
   card variants set it deliberately. `overflow: clip` on the page root is
   safe (see _base/bleed.css); overflow on the section between the two is not.
   ────────────────────────────────────────────────────────────────────────── */

.hzo-pull-1, .hzo-pull-2, .hzo-pull-3, .hzo-pull-4,
.hzo-push-1, .hzo-push-2, .hzo-push-3, .hzo-push-4 {
    position: relative;
    z-index: var(--z-above, 1);
}

.hzo-pull-1 { margin-top: calc(-1 * var(--pull-1, 48px)); }
.hzo-pull-2,
.hzo-pull-3,
.hzo-pull-4 { margin-top: calc(-1 * var(--pull-1, 48px)); }  /* mobile clamp */
.hzo-push-1 { margin-bottom: calc(-1 * var(--pull-1, 48px)); }
.hzo-push-2,
.hzo-push-3,
.hzo-push-4 { margin-bottom: calc(-1 * var(--pull-1, 48px)); }  /* mobile clamp */

@media (min-width: 768px) {
    .hzo-pull-2 { margin-top: calc(-1 * var(--pull-2, 64px)); }
    .hzo-pull-3 { margin-top: calc(-1 * var(--pull-3, 80px)); }
    .hzo-pull-4 { margin-top: calc(-1 * var(--pull-4, 120px)); }
    .hzo-push-2 { margin-bottom: calc(-1 * var(--pull-2, 64px)); }
    .hzo-push-3 { margin-bottom: calc(-1 * var(--pull-3, 80px)); }
    .hzo-push-4 { margin-bottom: calc(-1 * var(--pull-4, 120px)); }
}

/* Content-layer utilities. Each opens a positioning context so the z-index
   is not a no-op. */
.hzo-z-behind { position: relative; z-index: var(--z-behind, -1); }
.hzo-z-base   { position: relative; z-index: var(--z-base, 0); }
.hzo-z-above  { position: relative; z-index: var(--z-above, 1); }

/* Stacking-context opener — the partner to .hzo-z-behind, and the correct
   answer whenever a section's decoration must not escape into the page's
   global stacking order. */
.hzo-isolate { isolation: isolate; }


/* --- _base/overlay-lock.css --- */
/* ─── Overlay scroll-lock (always-loaded; ungated) ─────────────────────────
   `body.hzo-overlay-open` is toggled by the SHARED overlay manager in main.js
   (hzo.lockOverlayBackground / unlockOverlayBackground) whenever ANY overlay is
   open — modal, drawer, mini-cart, lightbox, mobile-menu, or search. That JS is
   ungated (ships on every page), so its CSS MUST be ungated too, or a page that
   opens a drawer/mini-cart WITHOUT a modal present loses the scroll-lock (the
   modal component sheet — which used to hold this rule — isn't enqueued there).

   Audit fix: UI-audit gap G2 (scroll-lock CSS present only in the gated
   modal.css). Per interaction-standards.md §"class-set-by-ungated-JS ⇒
   CSS-in-ungated-sheet". Moved here 2026-07-15, byte-identical behavior. */
body.hzo-overlay-open { overflow: hidden; }


/* --- _base/print-styles.css --- */
/* ─── Print styles ─────────────────────────────────────────────────────
   Keep this block LAST in the file. Paper gets the content, not the chrome:
   nav/overlay/CTA chrome hidden, black-on-white text, accordions expanded
   (CSS-only best effort — ::details-content forces the content box open in
   supporting browsers; the sibling display rule covers older engines that
   honored it), external link URLs printed after main-content links, and no
   page breaks inside card-like units. */
@media print {
    /* Chrome that means nothing on paper */
    .hzo-skip-link,
    .hzo-utility-bar,
    .hzo-site-header,
    .hzo-mobile-menu,
    .hzo-mobile-trigger,
    .hzo-search-overlay,
    .hzo-sticky-bar,
    .hzo-back-to-top,
    .hzo-social-proof,
    .hzo-banner,
    .hzo-final-cta,
    .hzo-site-footer nav,
    .hzo-site-footer .hzo-footer-link,   /* [hzo_footer_nav] renders bare links, no <nav> wrapper */
    .hzo-footer-social,
    .hzo-carousel__btn,
    .hzo-carousel__dots,
    .hzo-carousel__toggle,
    [data-hzo-marquee-toggle],
    .hzo-scroll-row__btn,
    .hzo-video-toggle,          /* Batch-2 chrome */
    .hzo-map__load,
    .hzo-price-toggle,          /* the monthly (default) prices still print */
    .hzo-share-row,             /* pure share chrome — and its links would print their sharer URLs */
    .hzo-modal,
    .hzo-drawer,
    .hzo-lightbox,
    .hzo-toast-region { display: none !important; }
    /* Lite-video: hide the play badge but keep the poster (it's real content). */
    .hzo-lite-video__play { display: none !important; }
    /* Long below-fold sections marked content-visibility:auto must be forced
       visible or some Chromium versions print them blank. */
    .hzo-cv-auto { content-visibility: visible !important; }

    /* Black on white, no elevation. (!important is deliberate here — print
       must beat every component color, including the targeted !importants
       used against a:visited elsewhere in this file.) */
    * {
        background: transparent !important;
        color: #000 !important;
        box-shadow: none !important;
        text-shadow: none !important;
    }
    body { background: #fff !important; }

    /* Expand every <details> (accordions/FAQs) so answers print.
       ::details-content opens the content box where supported; the sibling
       rule is the legacy best-effort fallback. */
    details::details-content {
        content-visibility: visible !important;
        height: auto !important;
        opacity: 1 !important;
    }
    details > summary ~ * { display: block !important; }

    /* G13: force scroll-reveal content visible in print/PDF — an un-revealed
       element (opacity:0, scroll-triggered) would otherwise print blank. Covers
       the legacy .hzo-reveal path AND the .hzo-reveal--item orchestrator. */
    .hzo-reveal,
    .hzo-reveal.hzo-reveal--init,
    .hzo-reveal--item,
    .hzo-reveal--item.hzo-reveal--init,
    .hzo-reveal--item.hzo-reveal--init.hzo-reveal--in {
        opacity: 1 !important;
        transform: none !important;
        filter: none !important;
        animation: none !important;
    }

    /* Print destination URLs after external links in the content area only
       (nav/footer/buttons stay clean; same-page anchors carry no href^=http). */
    main a[href^="http"]::after {
        content: " (" attr(href) ")";
        font-size: 0.85em;
        font-weight: normal;
        word-break: break-all;
    }

    /* Keep card-like units on one page */
    .hzo-card,
    .hzo-accordion__item,
    .hzo-stat,
    .hzo-timeline__item,
    .hzo-feature__panel {
        break-inside: avoid;
        page-break-inside: avoid;
    }
}


/* --- _base/prose.css --- */
/* ─── Prose (.hzo-prose) ───────────────────────────────────────────────
   Long-form article body: measure + vertical rhythm for h2/h3/p/lists/
   blockquotes. Internals use :where() (zero specificity) so any component
   class used inside an article still wins — the same trick as the Batch 1
   text-wrap pass, which already targets .hzo-prose for `pretty` wrapping. */
.hzo-prose { max-width: var(--prose-max-width); }
.hzo-prose > :where(h2) { margin: var(--space-xl) 0 var(--space-md); }
.hzo-prose > :where(h3) { margin: var(--space-lg-2) 0 var(--space-sm); }
.hzo-prose > :where(h2:first-child, h3:first-child) { margin-top: 0; }
.hzo-prose > :where(p, ul, ol) { margin: 0 0 var(--space-md); }
.hzo-prose > :where(ul, ol) { padding-left: 1.4em; }
.hzo-prose :where(li + li) { margin-top: var(--space-xs); }
.hzo-prose > :where(blockquote) {
    margin: var(--space-lg) 0;
    padding: var(--space-xs) 0 var(--space-xs) var(--space-lg);
    border-left: 3px solid var(--emphasis, var(--accent));
    color: var(--text-heading);
    font-size: 1.1em;
}
.hzo-prose > :where(blockquote) :where(p:last-child) { margin-bottom: 0; }
/* Opt-in dropcap — the FIRST paragraph only, pure CSS (::first-letter on
   the first <p> child; no JS, no extra markup). Float + tuned line-height
   works everywhere; initial-letter isn't cross-browser yet. */
.hzo-prose--dropcap > p:first-of-type::first-letter {
    float: left;
    font-family: var(--font-heading);
    font-size: 3.1em;
    font-weight: var(--font-weight-heading, var(--h2-weight, 700));
    line-height: 0.82;
    padding: 0.08em 0.12em 0 0;
    color: var(--brand-text);
}


/* --- _base/rendering-hint.css --- */
/* ─── Rendering hint (.hzo-cv-auto) ────────────────────────────────────
   content-visibility:auto lets the browser skip layout/paint for offscreen
   sections. BELOW-FOLD SECTIONS ONLY — never the hero or anything that can
   start in the first viewport (the hint would delay its first paint), and
   never on containers whose in-page anchors must be measurable on load.
   contain-intrinsic-size reserves ~800px per skipped section so the
   scrollbar doesn't jump as they render in; `auto` remembers the real size
   after first render. */
.hzo-cv-auto {
    content-visibility: auto;
    contain-intrinsic-size: auto 800px;
}


/* --- _base/responsive-background.css --- */
/* ─── Responsive background utility ───────────────────────────────────
   Right-size a background photo per breakpoint via inline vars — an LCP win
   for every image hero (mobile stops downloading the desktop image):
     style="--hzo-bg-sm:url(...480w); --hzo-bg-md:url(...1024w);
            --hzo-bg-xl:url(...1920w); --hzo-bg-pos-sm: 70% center"
   Each zone falls back to the previous one, so setting only --hzo-bg-sm is
   valid. Position vars are optional (default center). Declared AFTER the
   --image surfaces so its background-position wins when composed with them. */
.hzo-bg-responsive {
    background-image: var(--hzo-bg-sm);
    background-position: var(--hzo-bg-pos-sm, center);
    background-size: cover;
}
@media (min-width: 768px) {
    .hzo-bg-responsive {
        background-image: var(--hzo-bg-md, var(--hzo-bg-sm));
        background-position: var(--hzo-bg-pos-md, var(--hzo-bg-pos-sm, center));
    }
}
@media (min-width: 1024px) {
    .hzo-bg-responsive {
        background-image: var(--hzo-bg-lg, var(--hzo-bg-md, var(--hzo-bg-sm)));
        background-position: var(--hzo-bg-pos-lg, var(--hzo-bg-pos-md, var(--hzo-bg-pos-sm, center)));
    }
}
@media (min-width: 1440px) {
    .hzo-bg-responsive {
        background-image: var(--hzo-bg-xl, var(--hzo-bg-lg, var(--hzo-bg-md, var(--hzo-bg-sm))));
        background-position: var(--hzo-bg-pos-xl, var(--hzo-bg-pos-lg, var(--hzo-bg-pos-md, var(--hzo-bg-pos-sm, center))));
    }
}


/* --- _base/responsive-content-grid.css --- */
/* ─── Responsive content grid — 2 / 3 / 4-up ──────────────────────────
   Mobile-first single column; expands at the QA breakpoints. Drives feature
   grids, stat rows, card rows, logo grids. */
.hzo-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-lg); }
@media (min-width: 768px) {
    .hzo-grid--2, .hzo-grid--3, .hzo-grid--4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
    .hzo-grid--3 { grid-template-columns: repeat(3, 1fr); }
    .hzo-grid--4 { grid-template-columns: repeat(4, 1fr); }
}

/* ─── Spatial vocabulary (Wave S) — spans, arbitrary template, gap ─────
   The 11 lines above are the frozen base: every existing .hzo-grid renders
   byte-identically. Everything below is ADDITIVE and opt-in by class.

   WHY THIS EXISTS. A uniform N-up grid is one composition. Premium layouts
   need ONE cell to carry more weight than its neighbours — the feature tile,
   the tall portrait, the full-width closer. That was possible before only by
   hand-writing grid-column in a client's custom.css, which is why it happened
   once and never became vocabulary.

   ── Column spans ────────────────────────────────────────────────────────
   RESPONSIVE-SAFE COLLAPSE is the whole design: a span is only declared at
   the breakpoint where the columns it asks for actually exist. Below 768px
   the base grid is a single column and NO span applies, so a spanned cell can
   never open an implicit extra track and blow the grid out horizontally.

     .hzo-span-2      2 columns from 768px  (in a --3/--4 grid at 768px the
                      grid is 2-up, so this reads as a full row — intended)
     .hzo-span-3      3 columns from 1024px (--3 / --4 grids)
     .hzo-span-4      4 columns from 1024px (--4 grids)
     .hzo-span-full   the whole row at EVERY breakpoint (1 / -1) — safe in a
                      single-column grid too, which is why it needs no gate
     .hzo-rowspan-2   2 rows from 768px — the tall cell. Pair with --dense so
                      the hole it leaves gets backfilled.
     .hzo-rowspan-3   3 rows from 1024px

   Spans are declared on the CELL, not the grid, and carry no .hzo-grid
   ancestor requirement — they work inside .hzo-bento__stack or any hand-rolled
   grid on the same page. That is deliberate: the utility describes the cell.

   ── Arbitrary template ──────────────────────────────────────────────────
   .hzo-grid--custom reads --hzo-grid-template (desktop) and the optional
   --hzo-grid-template-md (tablet). Mobile always stays single-column. This is
   the escape hatch for compositions the 2/3/4 ladder cannot express — a
   golden-ratio split, a 5-up logo wall, a sidebar rail:

     <div class="hzo-grid hzo-grid--custom" style="--hzo-grid-template: 1.6fr 1fr">

   Unset --hzo-grid-template falls back to a responsive auto-fit, so a typo
   degrades to a sensible grid rather than to `none` + auto columns.

   ── Gap ─────────────────────────────────────────────────────────────────
   Steps the --space ladder, same names as .hzo-stack / .hzo-cluster. Default
   (no modifier) is unchanged at --space-lg. --gap-0 is the flush-tile case
   (a mosaic where cells touch); it is NOT a substitute for a designed rhythm.

   ── Density ─────────────────────────────────────────────────────────────
   .hzo-grid--dense turns on grid-auto-flow: dense so later small cells
   backfill the gaps a span leaves. Note the a11y caveat: dense packing can
   put DOM order out of step with visual order. Keep source order meaningful.
   ────────────────────────────────────────────────────────────────────────── */

.hzo-span-full { grid-column: 1 / -1; }

@media (min-width: 768px) {
    .hzo-span-2    { grid-column: span 2; }
    .hzo-rowspan-2 { grid-row: span 2; }
}
@media (min-width: 1024px) {
    .hzo-span-3    { grid-column: span 3; }
    .hzo-span-4    { grid-column: span 4; }
    .hzo-rowspan-3 { grid-row: span 3; }
}

.hzo-grid--custom { grid-template-columns: 1fr; }
@media (min-width: 768px) {
    .hzo-grid--custom {
        grid-template-columns: var(--hzo-grid-template-md,
            var(--hzo-grid-template, repeat(auto-fit, minmax(240px, 1fr))));
    }
}
@media (min-width: 1024px) {
    .hzo-grid--custom {
        grid-template-columns: var(--hzo-grid-template, repeat(auto-fit, minmax(240px, 1fr)));
    }
}

.hzo-grid--gap-0   { gap: 0; }
.hzo-grid--gap-xs  { gap: var(--space-xs); }
.hzo-grid--gap-sm  { gap: var(--space-sm); }
.hzo-grid--gap-md  { gap: var(--space-md); }
.hzo-grid--gap-lg  { gap: var(--space-lg); }
.hzo-grid--gap-xl  { gap: var(--space-xl); }
.hzo-grid--gap-2xl { gap: var(--space-2xl); }

.hzo-grid--dense { grid-auto-flow: dense; }


/* --- _base/scroll-reveal.css --- */
/* ─── Scroll reveal (opt-in utility; reduced-motion-safe; no-JS-safe) ──────
   Add class="hzo-reveal" to any element to fade + rise in as it scrolls into
   view. main.js adds .hzo-reveal--init (ONLY to below-the-fold elements, so
   above-fold content never flashes), then .is-revealed when it enters view.
   No JS or reduced-motion → content stays fully visible, never hidden.

   The reveal itself is a KEYFRAME animation (not a transition): components
   that declare their own `transition` shorthand later in the file (buttons,
   tags, option cards…) would silently cancel a transition-based reveal by
   source order — an animation can't be clobbered that way, works on ANY
   element, and keeps fade + rise in lockstep. It only fires on elements
   that went through --init (below-fold), so above-fold content never
   animates on load. Stagger: main.js sets an inline animation-delay;
   `backwards` fill keeps the element hidden through that delay. */
.hzo-reveal { transition: opacity 0.6s ease, transform 0.6s ease; }
.hzo-reveal.hzo-reveal--init { opacity: 0; transform: translateY(20px); will-change: opacity, transform; }
.hzo-reveal.is-revealed { opacity: 1; transform: none; }
.hzo-reveal.hzo-reveal--init.is-revealed {
    animation: hzo-reveal-in 0.6s var(--ease-standard, ease) backwards;
}
@keyframes hzo-reveal-in {
    from { opacity: 0; transform: translateY(20px); }
    to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
    .hzo-reveal, .hzo-reveal.hzo-reveal--init { opacity: 1; transform: none; transition: none; animation: none; }
}


/* --- _base/section-edges.css --- */
/* ─── Section edges — parameterized boundary treatments ────────────────
   Wave S spatial vocabulary. A section boundary is a design decision. Today
   the fleet has exactly two answers to it: a hard colour change, or one of the
   four frozen paths in components/section-divider/section-dividers.css (wave /
   mosaic). Both are choices you make once and cannot tune. This file makes the
   boundary a PARAMETER: angle, size, shape, and colour.

   NOTHING HERE REPLACES ANYTHING. section-dividers.css keeps its wave and
   mosaic paths and keeps working exactly as before; this is the additive,
   token-driven vocabulary that new work should reach for first.

   ── The model: the edge IS the neighbour's surface ──────────────────────
   The treatment is a pseudo-element painted in --edge-color, laid over the
   bottom (or top) band of THIS section. Set --edge-color to the
   ADJACENT section's background and the two surfaces interlock — no third
   colour appears at the seam, which is what separates a designed transition
   from a decorative squiggle. On the standard alternator that is one line:

     <section class="hzo-section hzo-section--dark
                     hzo-section--edge-bottom hzo-edge--diagonal"
              style="--edge-color: var(--bg-alt)">

   The next section is --pale (--bg-alt), so the dark band appears to be cut
   on a diagonal by the pale one. Get --edge-color wrong and it reads as a
   stripe; that is the single thing to check in a screenshot.

   ── Classes ─────────────────────────────────────────────────────────────
     .hzo-section--edge-bottom   arm the bottom edge (uses ::after)
     .hzo-section--edge-top      arm the top edge    (uses ::before)
   Shape (compose one onto an armed edge):
     .hzo-edge--diagonal         straight cut, rises left → right
     .hzo-edge--diagonal-rev     straight cut, rises right → left
     .hzo-edge--angled           as --diagonal, but sized from --edge-angle
     .hzo-edge--angled-rev       as --diagonal-rev, but sized from --edge-angle
     .hzo-edge--curve            the neighbour rises as a wide dome
     .hzo-edge--arch             soft rounded corners; the quietest option
   No shape class = a flat band of --edge-color, which is a legitimate (and
   very quiet) treatment: a colour shelf under the section.

   ── diagonal vs angled: which straight cut ──────────────────────────────
   Both draw the SAME triangle. They differ only in what fixes its size, and
   therefore in what stays constant as the page widens:
     --diagonal   height is --edge-size. The band is a known px height at
                  every width, so the ANGLE flattens as the page widens
                  (64px reads 9.7° at 375 and 1.9° at 1920).
     --angled     height is derived from --edge-angle, so the ANGLE is what
                  holds and the band height grows with the viewport.
   Pick --diagonal when the band must fit a known vertical budget; pick
   --angled when the slope itself is the design. Do not stack them.

   ── Tokens ──────────────────────────────────────────────────────────────
     --edge-color   the neighbour's surface        default --bg-page
     --edge-size    band height (--diagonal etc.)  default 64px
     --edge-angle   used by --angled / --angled-rev only    default 6deg
     --edge-radius  used by --arch only            default --radius-3
   --edge-rise is DERIVED, not authored: it resolves to --edge-size normally
   and to the tan()-computed rise on an --angled edge. It is the single value
   both the band height and .hzo-section--edge-pad read, which is what keeps
   the pad honest on an angled edge. Read it; do not set it.

   ── What --edge-angle actually costs you ────────────────────────────────
   The cut spans the FULL width of the section, so a true angle θ needs a rise
   of `width × tan(θ)` — not half of it. That makes the band tall on a wide
   screen, and that is the honest price of a stated angle:
   (measured in Chromium, not calculated — see resources/release-checks/)
     --edge-angle   375     768    1250    1440    1920
       3deg        19.6px  40.2px  65.5px  75.5px  100.6px
       6deg        39.4px  80.7px  131.4px 151.3px 201.8px
      12deg        79.7px  163.2px 265.7px 306.1px 408.1px
   6deg is the token default and is a strong move at desktop; 2–4deg is the
   range that reads as a designed slope rather than a wedge. Whatever you
   pick, pair it with .hzo-section--edge-pad — a 200px band over an 80px
   padding is 120px of band sitting on copy.
   (`vw` includes the scrollbar, so on a scrolling desktop page the run is a
   scrollbar narrower than the rise assumes and the drawn angle lands under
   0.1° over the stated one — 6.06° for 6° at 1440 with a 15px bar. Same
   caveat, and the same magnitude, as the one at the top of _base/bleed.css.)

   ── Two things that will bite ───────────────────────────────────────────
   1. THE EDGE EATS PADDING. The band overlays the section's own bottom
      padding (48px mobile / 64px tablet / 80px desktop). At the default 64px
      an edge on mobile reaches past the padding into content. Either keep
      --edge-size at or below the section's padding on small screens, or add
      .hzo-section--edge-pad (below) which adds the band height back as extra
      padding on the edged side. Prefer the pad class; it is one word.
   2. ::before / ::after ARE SPENT ON IMAGE SECTIONS. .hzo-section--image and
      .hzo-hero--image already use ::before for their scrim, so an --edge-top
      cannot be added to an image section. Put a --edge-bottom on the section
      ABOVE it instead — the composition is identical and the pseudo is free.

   Content is lifted to --z-above on any edged section so the band can never
   paint over copy. Direct children only, mirroring what image-band.css
   already does for its scrim. Purely additive: these selectors require the
   new classes, so no existing section's computed output changes.
   ────────────────────────────────────────────────────────────────────────── */

/* --edge-rise is the ONE height the band and the pad both read. Declared on the
   armed section (not in :root) so it is scoped to edged sections and so the
   --angled override below can win on specificity alone. */
.hzo-section--edge-bottom,
.hzo-section--edge-top {
    position: relative;
    --edge-rise: var(--edge-size, 64px);
}

.hzo-section--edge-bottom > *,
.hzo-section--edge-top > * { position: relative; z-index: var(--z-above, 1); }

.hzo-section--edge-bottom::after,
.hzo-section--edge-top::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    height: var(--edge-rise, var(--edge-size, 64px));
    background: var(--edge-color, var(--bg-page));
    pointer-events: none;
}
.hzo-section--edge-bottom::after { bottom: 0; }
.hzo-section--edge-top::before   { top: 0; }

/* Diagonal / angled — the straight cut. The wedge keeps the corner it is
   anchored to and loses the opposite one, so THIS section's colour reads as the
   rising side. ONE corner-to-corner triangle spanning the whole band: the
   hypotenuse runs (0,100%) → (100%,0), so its rise is the band height and its
   run is the FULL band width. That is the geometry --angled has to satisfy.
   --angled shares the shape and differs only in how the height is derived. */
.hzo-section--edge-bottom.hzo-edge--diagonal::after,
.hzo-section--edge-bottom.hzo-edge--angled::after { clip-path: polygon(0 100%, 100% 0, 100% 100%); }
.hzo-section--edge-bottom.hzo-edge--diagonal-rev::after,
.hzo-section--edge-bottom.hzo-edge--angled-rev::after { clip-path: polygon(0 0, 100% 100%, 0 100%); }
.hzo-section--edge-top.hzo-edge--diagonal::before,
.hzo-section--edge-top.hzo-edge--angled::before { clip-path: polygon(0 0, 100% 0, 0 100%); }
.hzo-section--edge-top.hzo-edge--diagonal-rev::before,
.hzo-section--edge-top.hzo-edge--angled-rev::before { clip-path: polygon(0 0, 100% 0, 100% 100%); }

/* Curve — the neighbour's surface rises as one wide, shallow dome. The
   elliptical radius (`/`) is what keeps it shallow: 50% horizontally, the full
   band height vertically. A circular radius here would read as a bubble. */
.hzo-section--edge-bottom.hzo-edge--curve::after { border-radius: 50% 50% 0 0 / 100% 100% 0 0; }
.hzo-section--edge-top.hzo-edge--curve::before   { border-radius: 0 0 50% 50% / 0 0 100% 100%; }

/* Arch — rounded corners only. The restrained option: it softens the seam
   without announcing itself, and it is the one that survives at 375px. */
.hzo-section--edge-bottom.hzo-edge--arch::after {
    border-radius: var(--edge-radius, var(--radius-3)) var(--edge-radius, var(--radius-3)) 0 0;
}
.hzo-section--edge-top.hzo-edge--arch::before {
    border-radius: 0 0 var(--edge-radius, var(--radius-3)) var(--edge-radius, var(--radius-3));
}

/* Angled — the same straight cut, sized so it holds a TRUE --edge-angle at every
   viewport width instead of flattening as the page widens.

   THE RISE IS 100vw, NOT 50vw. The clip-path above is a single corner-to-corner
   triangle, so tan(θ) = rise / FULL width. A 50vw rise draws atan(0.5·tan θ) —
   3.0° for a stated 6° — which is what shipped in the first cut of this file and
   is why the class name and the token both lied. 50vw would only be right for a
   symmetric two-half peak, and no shape here is one.

   Overriding --edge-rise (not `height`) is deliberate: it keeps
   .hzo-section--edge-pad correct automatically, since the pad reads the same
   value. tan() is baseline in every browser the fleet supports; the @supports
   gate means an engine without it degrades to a --edge-size-tall straight cut —
   still a diagonal, just at a fixed band height — rather than losing the cut. */
@supports (height: calc(1px * tan(45deg))) {
    .hzo-section--edge-bottom.hzo-edge--angled,
    .hzo-section--edge-bottom.hzo-edge--angled-rev,
    .hzo-section--edge-top.hzo-edge--angled,
    .hzo-section--edge-top.hzo-edge--angled-rev {
        --edge-rise: calc(100vw * tan(var(--edge-angle, 6deg)));
    }
}

/* Padding restore — adds the band height back so the edge overlays empty
   space instead of copy. Compound selector (0,2,0) to beat the base
   .hzo-section padding ladder (0,1,0) at every breakpoint.

   Reads --edge-rise, NOT --edge-size, so it covers an --angled band too. With
   --edge-size it restored a flat 64px under a band that measures 101px at 1920,
   leaving 37px of cut sitting on the copy it exists to protect. */
.hzo-section.hzo-section--edge-pad.hzo-section--edge-bottom { padding-bottom: calc(var(--space-xl) + var(--edge-rise, var(--edge-size, 64px))); }
.hzo-section.hzo-section--edge-pad.hzo-section--edge-top    { padding-top: calc(var(--space-xl) + var(--edge-rise, var(--edge-size, 64px))); }
@media (min-width: 768px) {
    .hzo-section.hzo-section--edge-pad.hzo-section--edge-bottom { padding-bottom: calc(var(--space-2xl) + var(--edge-rise, var(--edge-size, 64px))); }
    .hzo-section.hzo-section--edge-pad.hzo-section--edge-top    { padding-top: calc(var(--space-2xl) + var(--edge-rise, var(--edge-size, 64px))); }
}
@media (min-width: 1250px) {
    .hzo-section.hzo-section--edge-pad.hzo-section--edge-bottom { padding-bottom: calc(var(--space-3xl) + var(--edge-rise, var(--edge-size, 64px))); }
    .hzo-section.hzo-section--edge-pad.hzo-section--edge-top    { padding-top: calc(var(--space-3xl) + var(--edge-rise, var(--edge-size, 64px))); }
}


/* --- _base/section-surface-variants.css --- */
/* ─── Section surface variants ────────────────────────────────────────
   Formalize the previously-inline section backgrounds into modifiers:
     (light)              default .hzo-section — white (--bg-page)
     .hzo-section--pale   neutral pale gray (--bg-alt) — the standard alternator
     .hzo-section--tint   brand-tinted light surface (--brand-pale), dark text
     .hzo-section--dark   brand solid + white text (defined earlier in this file)
   Vertical rhythm comes from the base .hzo-section padding ladder.
   (Named --tint, NOT --brand: "--brand" would read as the solid brand color;
   this is a pale brand TINT. The solid brand surface is --dark.) */
.hzo-section--pale  { background: var(--bg-alt); }
.hzo-section--tint  { background: var(--brand-pale); }

/* Radial "glow" variant of the dark surface. Pair WITH --dark so all the
   white-text rules still apply: class="hzo-section hzo-section--dark
   hzo-section--dark-glow". Lightens toward a focal point above center and
   sinks toward --brand-deep at the edges — adds depth to CTA/stat bands
   without touching the color system (everything derives from the same
   --bg-dark-section the flat variant uses). */
.hzo-section--dark.hzo-section--dark-glow {
    background: radial-gradient(120% 90% at 50% 30%,
        color-mix(in srgb, var(--bg-dark-section) 82%, white) 0%,
        var(--bg-dark-section) 55%,
        color-mix(in srgb, var(--bg-dark-section) 60%, var(--brand-deep)) 100%);
}

/* Compact padding modifier — one step down from the base section ladder
   (lg / xl / 2xl vs the base xl / 2xl / 3xl). For low-height utility bands —
   trust strips, thin logo/rating rows, an eyebrow-only intro — that would
   look over-padded at full section rhythm. Replaces the inline
   `style="padding-top: var(--space-lg); padding-bottom: var(--space-lg)"`
   that trust-row.html and the homepage trust bar hand-roll today.

   Compound `.hzo-section.hzo-section--compact` (0,2,0) so it beats BOTH the
   base `.hzo-section` padding (0,1,0, declared in main.css which loads after
   this file) AND the section-adjacency padding-top rules earlier in this file
   (0,2,0) — those are declared before this point, so equal specificity resolves
   to compact by source order. Author intent (a deliberately tight band) wins
   over the automatic adjacency restore, which is what we want. */
.hzo-section.hzo-section--compact { padding-top: var(--space-lg); padding-bottom: var(--space-lg); }
@media (min-width: 768px) {
    .hzo-section.hzo-section--compact { padding-top: var(--space-xl); padding-bottom: var(--space-xl); }
}
@media (min-width: 1250px) {
    .hzo-section.hzo-section--compact { padding-top: var(--space-2xl); padding-bottom: var(--space-2xl); }
}


/* --- _base/skeleton.css --- */
/* ─── Skeleton loading placeholder (always-loaded; reduced-motion-safe) ─────
   Audit G47 (the missing "loading vocabulary"). A space-reserving shimmer block
   for content loading in — AJAX grids, review loads, product loops — with ZERO
   CLS (it reserves the box). The ONE shared primitive so components compose it
   instead of each rolling a private loading cue (product-grid, filters, review
   grids all want this).

   Compose: give it a size (or a variant). Under reduced-motion the ambient
   shimmer STOPS (PRESERVE-5) but the muted box stays (still reads "loading").
   Under forced-colors it becomes a bordered box; in print it's removed. */
.hzo-skeleton {
    position: relative;
    overflow: hidden;
    background: var(--bg-alt);
    border-radius: var(--radius-2);
    /* space-reserving — set a height/width, or use a --* variant below */
}

.hzo-skeleton::after {
    content: "";
    position: absolute;
    inset: 0;
    transform: translateX(-100%);
    background: linear-gradient(
        90deg,
        transparent,
        color-mix(in srgb, var(--bg-page) 65%, transparent),
        transparent
    );
    /* Ambient loop (like the marquee/wave drift) — not a micro-interaction
       transition, so it carries its own tunable speed rather than a --dur-* step. */
    animation: hzo-skeleton-shimmer var(--hzo-skeleton-speed, 1.4s) ease-in-out infinite;
    will-change: transform;
}

@keyframes hzo-skeleton-shimmer {
    to { transform: translateX(100%); }
}

.hzo-skeleton--text   { height: 0.75em; margin-block: 0.3em; border-radius: var(--radius-1); }
.hzo-skeleton--line   { height: 1em; }
.hzo-skeleton--title  { height: 1.4em; width: 60%; }
.hzo-skeleton--circle { border-radius: var(--radius-full); aspect-ratio: 1; }
.hzo-skeleton--media  { aspect-ratio: 4 / 3; }
.hzo-skeleton--block  { min-height: 8rem; }

@media (prefers-reduced-motion: reduce) {
    .hzo-skeleton::after { animation: none; }
}
@media (forced-colors: active) {
    .hzo-skeleton { border: 1px solid CanvasText; background: Canvas; }
    .hzo-skeleton::after { display: none; }
}
@media print {
    .hzo-skeleton { display: none !important; }
}


/* --- _base/skip-link.css --- */
/* ─── Skip link (a11y) ────────────────────────────────────────────────
   Visually hidden until focused. Intended as the FIRST focusable element in
   the header, jumping keyboard/AT users past the nav to <main id="main">.
   Reveal on :focus (not :focus-visible — it must appear for keyboard Tab). */
.hzo-skip-link {
    position: fixed;
    top: 8px;
    left: 8px;
    z-index: var(--z-toast);
    transform: translateY(-150%);
    background: var(--brand);
    color: var(--text-on-brand) !important;
    padding: 12px 20px;
    border-radius: var(--radius-input);
    font-family: var(--font-body);
    font-weight: var(--fw-semibold, 600);
    text-decoration: none;
    transition: transform var(--transition-base) ease;
}
.hzo-skip-link:focus { transform: translateY(0); outline: 3px solid var(--color-focus); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .hzo-skip-link { transition: none; } }


/* --- _base/sr-only.css --- */
/* ─── Screen-reader-only utility (always-loaded) ───────────────────────────
   The single shared visually-hidden utility, so components stop re-inlining the
   clip technique (several audit findings needed one). `.hzo-sr-only` content is
   invisible on screen but stays in the accessibility tree AND the keyboard/tab
   flow — use it for the real label an icon-only control needs, a heading that
   orders the document outline, or status text that has a visible twin.

   Uses the clip technique (NOT display:none / visibility:hidden / [hidden],
   which drop the node from the a11y tree). `--focusable` reveals on focus (the
   skip-link pattern) so a hidden-but-focusable control isn't a silent trap. */
.hzo-sr-only {
    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;
}

.hzo-sr-only--focusable:focus,
.hzo-sr-only--focusable:focus-within {
    position: static !important;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    clip-path: none;
    white-space: normal;
}


/* --- _base/surface-dark.css --- */
/* ─── Surface dark — scoped token remap ────────────────────────────────
   Re-points the generic surface/text/link tokens to dark-brand values within
   its subtree, so any .hzo-card / aside / summary reads dark without bespoke
   rules. Apply to a wrapper (nested cards inherit the remapped tokens) or
   straight onto a single card/aside. It only sets custom properties + its own
   bg/color, so it does NOT fight .hzo-section--dark's descendant rules (both
   resolve to light text). Utility — no block. */
.hzo-surface--dark {
    --card-bg:      color-mix(in srgb, var(--bg-dark-section) 84%, #000);
    --card-border:  rgba(255, 255, 255, 0.14);
    --border:       rgba(255, 255, 255, 0.14);
    --text-heading: #ffffff;
    --text-body:    rgba(255, 255, 255, 0.88);
    --text-muted:   rgba(255, 255, 255, 0.68);
    --brand-text:   var(--accent-hover-on-dark, var(--accent));
    --accent-text:  var(--accent-hover-on-dark, var(--accent));
    background: var(--surface-dark-bg, var(--bg-dark-section));
    color: var(--text-body);
    border-radius: var(--radius-card);
}


/* --- _base/text-wrapping-polish.css --- */
/* ─── Text wrapping polish — display heading (companion to main.css) ────
   main.css balances h1-h4 + .hzo-visual-h1..4; .hzo-display lives here so
   its rule does too. Progressive — ignored by older browsers. */
.hzo-display { text-wrap: balance; }


/* --- _base/typography.css --- */
/* ─── Typography ────────────────────────────────────────────────────── */
/* .hzo-display is the one step above h1 — the top-of-page statement. Its size
   is now the --fs-display token (Wave S); the inline fallback below is the
   historical literal, so a checkout without the token renders identically and
   the computed value is unchanged either way. Tokenizing it is what lets an
   archetype, the design widget, or a client's custom.css retune the display
   step without re-declaring this whole rule. */
.hzo-display {
    font-family: var(--font-heading);
    font-weight: var(--font-weight-heading, 700);
    font-size: calc(var(--fs-display, clamp(2.25rem, 5vw, 5rem)) * var(--hzo-type-scale, 1) * var(--hzo-heading-scale, 1));
    line-height: 1.05;
    letter-spacing: var(--heading-letter-spacing, -0.015em);
    text-transform: var(--heading-transform, none);
    color: var(--text-heading);
    margin: 0 0 var(--space-lg);
}
.hzo-subtitle {
    font-family: var(--font-body);
    font-size: 1.25rem;
    font-weight: var(--fw-regular, 400);
    line-height: var(--lh-normal);
    color: var(--text-muted);
    margin: 0 0 var(--space-xl);
}
.hzo-lead {
    font-size: 1.25rem;
    line-height: var(--lh-normal);
    color: var(--text-body);
    margin: 0 0 var(--space-lg);
}
.hzo-small {
    font-size: 0.875rem;
    color: var(--text-muted);
    margin: 0 0 var(--space-md);
}
.hzo-eyebrow {
    display: inline-block;
    font-family: var(--font-body);
    font-size: var(--eyebrow-size, 15px);
    font-weight: var(--fw-semibold, 600);
    text-transform: uppercase;
    letter-spacing: var(--eyebrow-tracking, 0.7px);
    color: var(--accent-text);
    margin: 0 0 var(--space-md);
}
/* SEO pattern: eyebrows ARE the heading tags (h1-h6) for keyword placement.
   The class must fully override heading element defaults AND any theme.json
   heading styles. !important on every property a theme could set on
   headings (font-family, font-size, font-weight, color, line-height,
   text-transform, letter-spacing, margin). See quality-rules.md §6.2. */
h1.hzo-eyebrow, h2.hzo-eyebrow, h3.hzo-eyebrow,
h4.hzo-eyebrow, h5.hzo-eyebrow, h6.hzo-eyebrow {
    display: inline-block !important;
    font-family: var(--font-body) !important;
    font-size: var(--eyebrow-size, 15px) !important;
    font-weight: var(--fw-semibold, 600) !important;
    line-height: 1.4 !important;
    text-transform: uppercase !important;
    letter-spacing: var(--eyebrow-tracking, 0.7px) !important;
    color: var(--accent-text) !important;
    margin: 0 0 var(--space-md) !important;
}
.hzo-meta {
    font-family: var(--font-body);
    font-size: var(--fs-75);
    font-weight: var(--fw-medium, 500);
    color: var(--text-muted);
    line-height: var(--lh-normal);
}
.hzo-caption {
    font-family: var(--font-body);
    font-size: var(--fs-75);
    color: var(--text-muted);
    font-style: italic;
    line-height: var(--lh-normal);
    margin: var(--space-sm) 0 0;
}


/* --- _base/utility-color-tokens.css --- */
/* ─── Utility color tokens (additive; warning + info not in main.css) ─── */
:root {
    --color-warning:        #D97706;
    --color-warning-bg:     #FFFBEB;
    --color-info:           #2563EB;
    --color-info-bg:        #EFF6FF;
    --color-warm-highlight: #FFF8E5;
}


/* --- _base/view-transitions.css --- */
/* ─── Cross-document View Transitions (roadmap big bet 2) ─────────────
   MPA navigations get a fast root crossfade, and the header + logo morph
   between pages instead of repainting — the "app-smooth" page-load feel.
   Supporting browsers only (Chrome/Edge 126+, Safari 18.2+); everywhere
   else this whole block is inert and navigation behaves exactly as today.
   Gated on prefers-reduced-motion: reduced-motion users keep instant
   loads (the @view-transition rule itself is inside the media query, so
   no transition is even attempted).

   Naming discipline: view-transition-name must be UNIQUE per rendered
   page or the browser SKIPS the whole transition. Only two guaranteed
   singletons are named — the site header and the logo INSIDE it (the
   mobile drawer also renders a .hzo-site-logo, so the scoped selector
   matters). Heroes are deliberately NOT named: the hero carousel renders
   several .hzo-hero--image elements on one page, which would void the
   transition sitewide on those pages. */
@media (prefers-reduced-motion: no-preference) {
    @view-transition {
        navigation: auto;
    }
    .hzo-site-header { view-transition-name: hzo-header; }
    .hzo-site-header .hzo-site-logo { view-transition-name: hzo-logo; }
    /* Root crossfade at the system's base speed — the UA default (~250ms)
       reads a touch slow for a marketing site; header/logo groups keep the
       UA's default morph timing. */
    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation-duration: var(--duration-base, 200ms);
        animation-timing-function: var(--ease-standard, ease);
    }
}


/* --- _base/visited-normalization.css --- */
/* ─── :visited state normalization ───────────────────────────────────
   Browsers default :visited links to dark purple/grey, which overrides
   our styling once a link has been clicked. Each class-based link type
   below resets :visited to match its base color. This is critical for
   nav, footer, and card CTAs where visited links would otherwise shift
   color after a visitor's first navigation. */
.hzo-nav-link:visited,
.hzo-site-nav__link:visited,
.hzo-mega-menu__link:visited,
.hzo-mobile-menu__link:visited { color: var(--nav-link); }

/* :visited:hover — must be re-declared because :visited and :hover have the
   same specificity, so without this block the :visited rule above (which
   comes later in source order) would win on hover-over-visited links. */
.hzo-nav-link:visited:hover,
.hzo-site-nav__link:visited:hover,
.hzo-mega-menu__link:visited:hover,
.hzo-mobile-menu__link:visited:hover { color: var(--nav-link-hover); }

.hzo-breadcrumb a:visited { color: var(--text-muted); }
.hzo-breadcrumb a:visited:hover { color: var(--brand); }
.hzo-breadcrumb [aria-current="page"]:visited { color: var(--text-body); }

.hzo-card__link:visited { color: var(--accent-text) !important; }
.hzo-card__link:visited:hover { color: color-mix(in srgb, var(--accent-text) 70%, black) !important; }

.hzo-site-logo:visited { color: var(--brand); }

/* Dark-footer variant */
.hzo-site-footer a:visited { color: var(--footer-link-base-dark, rgba(255,255,255,0.85)); }
.hzo-footer-link:visited { color: var(--footer-link-dark, rgba(255,255,255,0.8)) !important; }

/* Light-footer variant */
.hzo-site-footer--light a:visited { color: var(--text-body) !important; }
.hzo-site-footer--light .hzo-footer-link:visited { color: var(--footer-link) !important; }

/* Buttons do NOT need :visited rules. The base .hzo-btn rules use !important
   on `color` and `background` which already overrides a:visited from per-client
   main.css. Adding :visited here creates a maintenance trap: if the base color
   changes, the :visited rule goes stale and shows the wrong color once a
   button link has been clicked. */


/* --- alert/alert.css --- */
/* ─── Alerts ────────────────────────────────────────────────────────── */
.hzo-alert {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: var(--space-md) var(--space-lg);
    border-left: 4px solid;
    border-radius: 4px;
    font-size: 15px;
    font-weight: var(--fw-500, 500);
    line-height: var(--lh-normal);
}
.hzo-alert > svg { flex-shrink: 0; }
.hzo-alert--info    { background: var(--color-info-bg);    border-color: var(--color-info);    color: var(--color-info); }
.hzo-alert--success { background: var(--success-bg);       border-color: var(--success);       color: var(--success-text, #166534); }
.hzo-alert--warning { background: var(--color-warning-bg); border-color: var(--color-warning); color: var(--warning-text, #92400E); }
.hzo-alert--error   { background: var(--error-bg);         border-color: var(--error);         color: var(--error-text, #991B1B); }


/* --- announcer/announcer.css --- */
/* ─── Live-Region Announcer (.hzo-announcer-region) ────────────────────────
   The two aria-live regions (polite + assertive) that hzo.announce() writes
   into. They must be in the accessibility tree at ALL times — so they are
   hidden with the CLIP technique, NEVER display:none / visibility:hidden /
   [hidden], which drop the node from the a11y tree and kill announcements
   (WCAG 4.1.3). Zero-size, non-interactive, never focusable.

   forced-colors safe by construction: clipped (not opacity/visibility-hidden),
   so `forced-colors: active` cannot surface it (avoids the §12.2 opacity trap).
   No motion ships (SR text injection only) → no transition to kill and nothing
   is ever motion-hidden, so no prefers-reduced-motion block is needed. Needs no
   z-index (clip-hidden, zero-size); the optional toast:true delegate resolves
   to --z-toast inside toast.css, never here. */
.hzo-announcer-region {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    -webkit-user-select: none;
    user-select: none;
    pointer-events: none;
}

/* Page-level [hzo_announce config="true"] node: sets JS defaults, emits no UI.
   NOT a live region, so display:none is safe (and correct — never announced). */
.hzo-announcer-config {
    display: none !important;
}

/* Optional [hzo_announce] click trigger. Unstyled by default — it is an author
   affordance that composes inline; add .hzo-btn* (or your own classes) via the
   shortcode `class` attribute when a visible button is wanted. */
.hzo-announce-trigger {
    font: inherit;
    color: inherit;
}


/* --- answer/answer.css --- */
/* ─── AEO Answer Block (.hzo-answer) ────────────────────────────────────────
   The visible surface for W-004 (answer.php): ONE question as a real heading
   + a 40–60-word answer-first paragraph optimized for AI Overviews / featured
   snippets. Three display modes — inline (default, always-visible AEO shape),
   card (boxed), disclosure (native <details>). The FAQPage/QAPage JSON-LD is
   PHP-emitted and has no styling surface.

   Tokens only — no bare hex/px/ms, no z-index (this block never portals or
   stacks). schema="off" still renders the visible block; a schema-carrying
   block adds only an invisible <script>. */

.hzo-answer {
    max-width: var(--prose-max-width);
}

/* Question heading — a REAL heading; its size comes from the type-scale ladder
   (base.css), so level (h2/h3/h4) stays decoupled from visual weight. */
.hzo-answer__question {
    margin: 0 0 var(--space-sm);
    color: var(--text-heading);
}

/* Answer-first paragraph. */
.hzo-answer__text {
    margin: 0;
    color: var(--text-body);
    line-height: var(--body-line-height, 1.6);
}

/* Optional byline: <cite> author · <time> date. */
.hzo-answer__byline {
    margin: var(--space-md) 0 0;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm);
    color: var(--text-muted);
    font-size: var(--fs-100, 14px);
}
.hzo-answer__author {
    font-style: normal;                 /* <cite> defaults italic; keep it plain */
    font-weight: var(--fw-medium, 500);
    color: var(--text-body);
}
.hzo-answer__byline-sep {
    color: var(--text-muted);
}

/* ── Card display: boxed, elevated ── */
.hzo-answer--card {
    padding: var(--space-lg);
    background: var(--surface);
    border: var(--border-1) solid var(--card-border, var(--border));
    border-radius: var(--radius-panel);
    box-shadow: var(--card-shadow);
}

/* ── Group: multiple Q&As sharing ONE FAQPage node ── */
.hzo-answer--group {
    display: flex;
    flex-direction: column;
    gap: var(--space-xl);
}
.hzo-answer--group.hzo-answer--card {
    gap: var(--space-lg);               /* tighter when boxed as a single card */
}
.hzo-answer__item > .hzo-answer__question {
    margin: 0 0 var(--space-sm);
}
.hzo-answer__item > .hzo-answer__text {
    margin: 0;
}

/* ── Disclosure display: native <details> = APG Disclosure pattern ──
   The <summary> carries button semantics + Enter/Space + aria-expanded for
   free; the caret is our own (default marker hidden). */
.hzo-answer--disclosure {
    padding: 0 var(--space-lg);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-panel);
}
.hzo-answer__summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    min-height: 44px;                   /* ≥44×44 comfort tap target (§11.2) */
    padding-block: var(--space-md);
    cursor: pointer;
    list-style: none;                   /* hide default marker; use our caret */
}
.hzo-answer__summary::-webkit-details-marker {
    display: none;
}
.hzo-answer__summary > .hzo-answer__question {
    margin: 0;
}
/* Chevron caret (rotates on open). */
.hzo-answer__summary::after {
    content: "";
    flex-shrink: 0;
    width: 0.5em;
    height: 0.5em;
    border-right: var(--border-2) solid currentColor;
    border-bottom: var(--border-2) solid currentColor;
    transform: rotate(45deg);
    transition: transform var(--dur-1) var(--ease-standard);
}
.hzo-answer--disclosure[open] > .hzo-answer__summary::after {
    transform: rotate(-135deg);
}
/* Real focus ring on the summary (WCAG 2.4.13) — outline (not shadow-only) so
   it survives forced-colors. */
.hzo-answer__summary:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: 2px;
}
.hzo-answer__body {
    padding-bottom: var(--space-md);
}

/* ── Print: an AEO answer is exactly what customers print/PDF — force it fully
   visible. Open a collapsed <details>, reset an opt-in reveal, drop the caret
   and box shadow (G13 force-reveal). ── */
@media print {
    .hzo-answer.hzo-reveal {
        opacity: 1 !important;
        transform: none !important;
    }
    .hzo-answer--card,
    .hzo-answer--disclosure {
        box-shadow: none;
        border-color: currentColor;
    }
    .hzo-answer--disclosure:not([open]) > .hzo-answer__body {
        display: block !important;      /* override the UA details-collapse rule */
    }
    .hzo-answer__summary::after {
        display: none;
    }
}

/* ── Forced-colors: never convey state through color/shadow alone — keep a
   system-color border, caret, and focus ring (§12.2). ── */
@media (forced-colors: active) {
    .hzo-answer--card,
    .hzo-answer--disclosure {
        border: var(--border-1) solid CanvasText;
    }
    .hzo-answer__summary::after {
        border-color: CanvasText;
    }
    .hzo-answer__summary:focus-visible {
        outline-color: CanvasText;
    }
}

/* ── Reduced-motion: kill the only animated affordance (the caret rotate).
   PRESERVE-5 — ambient motion stops; content stays visible. ── */
@media (prefers-reduced-motion: reduce) {
    .hzo-answer__summary::after {
        transition: none;
    }
}


/* --- avatar/avatar.css --- */
/* ─── Avatar ──────────────────────────────────────────────────────────
   Circular user/brand image with an initials fallback. Base 48px; --sm 32,
   --lg 64. Image: <span class="hzo-avatar"><img alt="..."></span>. Initials:
   <span class="hzo-avatar hzo-avatar--initials">JD</span> (brand-pale field +
   brand-text letters). Composed by the testimonial + team/bio blocks. */
.hzo-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 48px;
    height: 48px;
    border-radius: 50%;
    overflow: hidden;
    background: var(--bg-alt);
    color: var(--text-muted);
    font-family: var(--font-body);
    font-weight: var(--fw-600, 600);
    font-size: 1rem;
    line-height: var(--lh-none);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    user-select: none;
}
.hzo-avatar > img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.hzo-avatar--initials { background: var(--brand-pale); color: var(--brand-text); }
.hzo-avatar--sm { width: 32px; height: 32px; font-size: 0.8125rem; }
.hzo-avatar--lg { width: 64px; height: 64px; font-size: 1.25rem; }


/* --- avatar-group/avatar-group.css --- */
/* avatar-group — @tokens: --avatar-group-overlap

   An overlapping avatar stack — composes `.hzo-avatar` children into a
   condensed "who's here" cluster (testimonial rows, team credits, "+N more").
   Each disc gets a ring in the page colour so it reads as separate, and a
   negative left margin so it tucks under its left neighbour. Later children
   paint ABOVE earlier ones (source order + z-index) so each ring cleanly
   overlaps the one before it.

   Token-only: the overlap distance has no semantic token, so it is a component
   token with a literal fallback (the icon-button.css dial pattern). Ring width
   + shape read the semantic border/radius scale; the pill matches the base
   avatar footprint (48px, same dimension literal avatar.css uses). */
.hzo-avatar-group {
    display: inline-flex;
    align-items: center;
}

/* Direct children (the `.hzo-avatar` discs + the `+N` pill) overlap + ring. */
.hzo-avatar-group > * {
    border: var(--border-2) solid var(--bg-page);
    border-radius: var(--radius-full);
    margin-left: calc(-1 * var(--avatar-group-overlap, 12px));
}
.hzo-avatar-group > *:first-child { margin-left: 0; }

/* Later children win the stacking so each ring sits on its left neighbour. */
.hzo-avatar-group > *:nth-child(1) { z-index: 1; }
.hzo-avatar-group > *:nth-child(2) { z-index: 2; }
.hzo-avatar-group > *:nth-child(3) { z-index: 3; }
.hzo-avatar-group > *:nth-child(4) { z-index: 4; }
.hzo-avatar-group > *:nth-child(5) { z-index: 5; }

/* Overflow pill — the "+N" summary. Same footprint as a base avatar; muted ink
   on the alt surface so it reads as a count, not a person. Sits last, on top. */
.hzo-avatar-group__more {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 48px;
    height: 48px;
    background: var(--bg-alt);
    color: var(--text-muted);
    font-size: var(--fs-50);
    font-weight: var(--fw-600, 600);
    line-height: var(--lh-none);
    user-select: none;
    z-index: 6;
}


/* --- back-to-top/back-to-top.css --- */
/* ─── Back-to-top ──────────────────────────────────────────────────────
   Site chrome (ships fleet-wide from parts/footer.html — deliberately not
   opt-in; [hzo_back_to_top] can emit further instances). back-to-top.js
   (Phase-2 widget; behavior moved out of main.js) adds .is-visible once
   scrollY passes two viewports — or the data-hzo-back-to-top-threshold[-px]
   override; click smooth-scrolls to top (instant under reduced motion).
   Quiet circle: brand bg, white arrow, --shadow-md, 44px target. Rides
   between --z-sticky-bar (10) and --z-header (20) — above the mobile sticky
   CTA, below every overlay. Hidden from print (components/_base/
   print-styles.css). */
.hzo-back-to-top {
    position: fixed;
    right: var(--space-lg);
    bottom: calc(var(--space-lg) + env(safe-area-inset-bottom, 0px));
    z-index: var(--z-back-to-top, 15);
    width: 44px;
    height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--radius-full);
    background: var(--back-to-top-bg, var(--brand));
    color: var(--back-to-top-color, var(--text-on-brand));
    box-shadow: var(--shadow-md);
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    transform: translateY(var(--space-sm));
    transition: opacity var(--duration-base) var(--ease-standard),
                transform var(--duration-base) var(--ease-out),
                visibility var(--duration-base),
                background var(--duration-base) var(--ease-standard);
}
.hzo-back-to-top svg { width: 20px; height: 20px; display: block; }
.hzo-back-to-top.is-visible {
    opacity: 1;
    visibility: visible;
    transform: none;
}
.hzo-back-to-top:hover { background: var(--back-to-top-bg-hover, var(--brand-hover-on-light)); }
.hzo-back-to-top:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
    .hzo-back-to-top { transition: none; transform: none; }
}


/* --- badge/badge.css --- */
/* ─── Badges ─────────────────────────────────────────────────────── */
.hzo-badge {
    display: inline-flex;
    align-items: center;
    line-height: var(--lh-none);
    padding: 6px 11px;
    border-radius: var(--badge-radius, var(--radius-full));
    font-size: var(--fs-50);
    font-weight: var(--badge-weight, var(--fw-600, 600));
    letter-spacing: 0.03em;
    text-transform: uppercase;
    background: var(--bg-alt);
    color: var(--text-body);
}
.hzo-badge--primary   { background: var(--brand); color: var(--text-on-brand); }
.hzo-badge--secondary { background: var(--accent); color: var(--text-on-accent); }
/* State variants — PF-043.
   These four used to hardcode `color: #fff` on the shared semantic state fill,
   with no on-state ink token able to co-vary with a retint. Badge text is
   --fs-50 (12px), i.e. NORMAL text, so WCAG AA wants 4.5:1. White on fill:

       --info     #2563EB   5.17:1   pass
       --error    #DC2626   4.83:1   pass, thin
       --warning  #D97706   3.19:1   FAIL
       --success  #16A34A   3.30:1   FAIL

   These are engine tokens, not recipe-derived, so the two failures were
   identical on every install — not a per-client palette problem. And the
   failure was one-way: a client who retinted --success/--warning LIGHTER made
   it worse, because the ink stayed white.

   Fixed by giving the badge its own fill + ink tokens rather than by moving
   --success / --warning themselves. Those two are also consumed by alert and
   callout, where they are already safe (pale -bg fill + dark -ink text), and
   darkening the shared token to fix a badge would have shifted the semantic hue
   everywhere it is used. Defaults are the two darkened fills PF-043 proposed:

       --badge-success-bg  #15803D   white on it = 5.02:1   pass
       --badge-warning-bg  #B45309   white on it = 5.02:1   pass

   error/info route through the same token names at their existing values, so a
   client retinting any state has ONE place to do it and an ink token to go with
   it. Anyone overriding a fill LIGHTER must also set --badge-on-state dark.

   The tokens are declared here as var() fallbacks rather than in tokens.css's
   Badge block (which today holds only --badge-radius / --badge-weight). Folding
   them in there alongside the other component tokens is the tidier end state and
   is a safe follow-up — the fallbacks keep working either way. */
.hzo-badge--success   { background: var(--badge-success-bg, #15803D); color: var(--badge-on-state, #fff); }
.hzo-badge--warning   { background: var(--badge-warning-bg, #B45309); color: var(--badge-on-state, #fff); }
.hzo-badge--error     { background: var(--badge-error-bg, var(--error)); color: var(--badge-on-state, #fff); }
.hzo-badge--info      { background: var(--badge-info-bg, var(--color-info)); color: var(--badge-on-state, #fff); }
.hzo-badge--outline   { background: transparent; color: var(--brand-text); border: 1px solid var(--brand-text); }
/* Sale — promo attention badge. Reuses the accent (the system's `--emphasis`
   attention colour) so it stands apart from the stock states; no new token. Always
   pair with the word "Sale" so the meaning is never carried by colour alone.
   Stock states compose existing variants: in-stock = --success, low-stock =
   --warning, out-of-stock = the base (neutral) badge — unavailability is a state,
   not an error, so red (--error) is reserved for genuine errors. */
.hzo-badge--sale      { background: var(--accent); color: var(--text-on-accent); }


/* --- banner/banner.css --- */
/* ─── Top banner / promo bar ───────────────────────────────────────────
   Full-width announcement strip. Reuses brand/accent/section-surface tokens.
   Dismissed in-session via [data-hzo-banner-dismiss] (banner.js) — NO browser
   storage; collapses height + opacity (no layout jank), reduced-motion-bounded.
   Static markup needs no aria-live; if inserted dynamically use aria-live=polite. */
.hzo-banner {
    overflow: hidden;
    background: var(--brand);
    color: var(--text-on-brand);
    transition: height var(--transition-base) ease, opacity var(--transition-base) ease;
}
.hzo-banner--accent { background: var(--accent); color: var(--text-on-accent); }
.hzo-banner--pale   { background: var(--bg-alt); color: var(--text-body); }
.hzo-banner__inner {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding-top: 10px;
    padding-bottom: 10px;
}
.hzo-banner__text { flex: 1; margin: 0; text-align: center; font-size: var(--fs-100); line-height: 1.4; }
.hzo-banner a { color: inherit; text-decoration: underline; }
.hzo-banner__close {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    border-radius: var(--radius-full);
    cursor: pointer;
    transition: background var(--transition-base) ease;
}
.hzo-banner__close:hover { background: color-mix(in srgb, currentColor 15%, transparent); }
.hzo-banner__close:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
.hzo-banner__close > svg { width: 16px; height: 16px; display: block; }
.hzo-banner.is-dismissing { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .hzo-banner { transition: none; } }


/* --- blog-duo/blog-duo.css --- */
/* blog-duo — a responsive two-panel layout: a featured item beside a companion
   list (e.g. a lead article next to a "more articles" column). Single column on
   mobile; an asymmetric 5fr / 6fr split from 900px. CSS-only; token-only spacing;
   brand-agnostic. */
.hzo-blog-duo {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-xl);
}
@media (min-width: 900px) {
    .hzo-blog-duo {
        grid-template-columns: 5fr 6fr;
        gap: var(--space-2xl);
        align-items: start;
    }
}


/* --- breadcrumb/breadcrumb.css --- */
/* ─── Breadcrumb ────────────────────────────────────────────────────── */
.hzo-breadcrumb {
    font-family: var(--font-body);
    font-size: var(--fs-75);
    color: var(--text-muted);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    list-style: none;
    padding: 0;
    margin: 0;
}
.hzo-breadcrumb a {
    color: var(--text-muted);
    text-decoration: none;
    transition: color 0.15s;
}
.hzo-breadcrumb a:hover { color: var(--brand-text); }
.hzo-breadcrumb a:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.hzo-breadcrumb__sep { color: var(--border); margin: 0 2px; }
.hzo-breadcrumb [aria-current="page"] { color: var(--text-body); font-weight: var(--fw-500, 500); }

/* Simple underline default (Track-2 design pass) — a restrained hover underline
   grows in from the left, matching the nav-link / card-link family. Token-driven
   color (var(--link-underline, currentColor)). A client with a richer treatment
   (e.g. Hozio's background-image highlight-wipe, which is NOT a pseudo) neutralizes
   this with `.hzo-breadcrumb a::after { display: none }` in custom.css so only the
   bespoke skin shows — byte-identical (a pseudo adds no DOM node). */
.hzo-breadcrumb a { position: relative; }
.hzo-breadcrumb a::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -2px;
    height: 2px;
    background: var(--link-underline, currentColor);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform .2s ease;
    pointer-events: none;
}
.hzo-breadcrumb a:hover::after,
.hzo-breadcrumb a:focus-visible::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
    .hzo-breadcrumb a::after { transition: none; }
}


/* --- buttons/button-icon-slide.css --- */
/* ─── Button icon-slide — trailing arrow nudge ─────────────────────────
   Modifier on .hzo-btn: a trailing arrow (::after) that slides on hover/focus,
   mirroring .hzo-card__link. Reuses the button's flex gap for spacing.
   --icon-slide-distance (4px) with var(--ease-spring). Reduced motion → no
   transition (arrow stays put). No block. */
.hzo-btn--icon-slide::after,
.hzo-button--icon-slide::after {
    content: "";
    display: inline-block;
    width: 1em;
    height: 1em;
    flex-shrink: 0;
    background-color: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E") no-repeat center / contain;
            mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E") no-repeat center / contain;
    transition: transform var(--transition-base) var(--ease-spring);
}
.hzo-btn--icon-slide:hover::after,
.hzo-btn--icon-slide:focus-visible::after,
.hzo-button--icon-slide:hover::after,
.hzo-button--icon-slide:focus-visible::after { transform: translateX(var(--icon-slide-distance)); }
@media (prefers-reduced-motion: reduce) {
    .hzo-btn--icon-slide::after,
.hzo-button--icon-slide::after { transition: none; }
}


/* --- buttons/buttons.css --- */
/* ─── Buttons ────────────────────────────────────────────────────────
   !important is scoped to `color` and `background` only — those fight
   `a:visited` from per-client main.css (links styled as buttons would
   otherwise shift to the visited color). All other properties (padding,
   border, border-radius, text-decoration) win on class specificity alone
   and do not need !important. See bottom-of-file note on why buttons
   intentionally use !important here instead of `.hzo-btn:visited` rules. */
/* Button component tokens (Hozio Studio Live, Phase 2a). Each property reads a
   --btn-* token that DEFAULTS (via the var() fallback) to the original semantic
   value — so with no override the render is byte-identical, and the design
   widget can override any of them per variant. "Linked by default": the
   fallback IS the link to --accent / --brand; an override breaks just that link. */
/* Variant sheets — @imported here so the component loader (which enqueues only
   buttons.css) pulls them on the same request (widget convention; precedent:
   carousel, tooltip). primitives.css also imports button-icon-slide.css
   directly for the monolith path — the duplicate rules are identical, so the
   cascade is unchanged. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */

.hzo-btn,
.hzo-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    padding: var(--btn-padding-y, 14px) var(--btn-padding-x, 32px);
    font-family: var(--font-body);
    font-size: 1rem;
    font-weight: var(--btn-weight, var(--fw-semibold, 600));
    text-decoration: none;
    border-radius: var(--btn-radius, var(--radius-pill));
    border: var(--border-1) solid transparent;
    cursor: pointer;
    transition: background var(--transition-base), border-color var(--transition-base), color var(--transition-base), box-shadow var(--transition-base), transform 0.1s;
    min-height: 48px;
    line-height: var(--lh-none);
    white-space: nowrap;
    background: var(--btn-primary-bg, var(--accent)) !important;
    color: var(--btn-primary-text, var(--text-on-accent)) !important;
}
/* PF-042: both tokens are DECLARED in tokens.css now, so the inline fallbacks
   below are unreachable — kept only as a record of what this path used to
   resolve to. The old comment here claimed "bg darkens on hover → flip text
   light for contrast"; that assumption was never checked and measured 2.62:1 on
   a live client (and 2.19:1 at the engine's own default accent). See the
   --btn-primary-hover block in tokens.css for the measurements and for what is
   still NOT fixed. */
.hzo-btn:hover, .hzo-btn.is-hover,
.hzo-button:hover, .hzo-button.is-hover {
    background: var(--btn-primary-hover, var(--accent-hover-on-light)) !important;
    color: var(--btn-primary-hover-text, #fff) !important;
}
.hzo-btn:focus-visible, .hzo-btn.is-focus,
.hzo-button:focus-visible, .hzo-button.is-focus {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
}
.hzo-btn.is-active, .hzo-btn:active,
.hzo-button.is-active, .hzo-button:active { transform: translateY(1px); }
.hzo-btn.is-disabled, .hzo-btn[disabled], .hzo-btn[aria-disabled="true"],
.hzo-button.is-disabled, .hzo-button[disabled], .hzo-button[aria-disabled="true"] {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

.hzo-btn--primary,
.hzo-button--primary {
    background: var(--btn-primary-bg, var(--accent)) !important;
    color: var(--btn-primary-text, var(--text-on-accent)) !important;
    border-color: var(--btn-primary-bg, var(--accent));
}
/* PF-042: this path used to darken to a DIFFERENT colour than bare .hzo-btn
   above (--accent-text vs --accent-hover-on-light), because --btn-primary-hover
   was undeclared and the two fallbacks disagreed — so a site got one of two
   hover colours depending on whether the author wrote the variant class.
   Declaring the token reconciles them; these fallbacks are now unreachable. */
.hzo-btn--primary:hover,
.hzo-button--primary:hover {
    background: var(--btn-primary-hover, var(--accent-text)) !important;
    border-color: var(--btn-primary-hover, var(--accent-text));
    color: var(--btn-primary-hover-text, #fff) !important;
}
.hzo-btn--secondary,
.hzo-button--secondary {
    background: var(--btn-secondary-bg, var(--brand)) !important;
    color: var(--btn-secondary-text, var(--text-on-brand)) !important;
    border-color: var(--btn-secondary-bg, var(--brand));
}
.hzo-btn--secondary:hover,
.hzo-button--secondary:hover {
    background: var(--btn-secondary-hover, var(--brand-hover-on-light)) !important;
    border-color: var(--btn-secondary-hover, var(--brand-hover-on-light));
    color: var(--btn-secondary-text, var(--text-on-brand)) !important;
}
.hzo-btn--outline,
.hzo-button--outline {
    background: transparent !important;
    color: var(--btn-outline-color, var(--brand)) !important;
    border-color: var(--btn-outline-color, var(--brand));
}
.hzo-btn--outline:hover,
.hzo-button--outline:hover {
    background: var(--btn-outline-color, var(--brand)) !important;
    color: var(--btn-secondary-text, var(--text-on-brand)) !important;
    border-color: var(--btn-outline-color, var(--brand));
}
.hzo-btn--ghost,
.hzo-button--ghost {
    background: transparent !important;
    color: var(--btn-ghost-color, var(--brand)) !important;
    border-color: transparent;
}
.hzo-btn--ghost:hover,
.hzo-button--ghost:hover {
    background: var(--btn-ghost-hover, var(--brand-pale)) !important;
    color: var(--btn-ghost-color, var(--brand)) !important;
}
/* Tertiary — the quiet FILLED tier, and the missing rung on this ladder.
   Between --outline (a brand-coloured border, which is loud) and --ghost
   (transparent, which has no rest-state affordance at all on a light surface),
   there was nothing: a neutral, obviously-clickable, obviously-subordinate
   action. That gap is why dense UI ("Load more", "Filters", "Back to all
   services", pagination-adjacent actions) kept reaching for --outline and
   putting three brand-coloured borders in one row.

   Neutral by design: it reads --bg-alt / --border / --text-heading, no brand
   colour at all, so it can never compete with the primary. It IS visible at
   rest, which is the whole point and the reason it is not just another ghost.
   Hover deepens the fill and the border together (two properties, per the
   interaction-state rule) using color-mix off its own tokens, so it retints
   automatically with a client's neutrals. */
.hzo-btn--tertiary,
.hzo-button--tertiary {
    background: var(--btn-tertiary-bg, var(--bg-alt)) !important;
    color: var(--btn-tertiary-text, var(--text-heading)) !important;
    border-color: var(--btn-tertiary-border, var(--border));
}
.hzo-btn--tertiary:hover,
.hzo-button--tertiary:hover {
    background: var(--btn-tertiary-hover, color-mix(in srgb, var(--btn-tertiary-bg, var(--bg-alt)) 88%, var(--btn-tertiary-text, var(--text-heading)))) !important;
    color: var(--btn-tertiary-text, var(--text-heading)) !important;
    border-color: var(--btn-tertiary-hover-border, var(--text-muted));
}
.hzo-btn--danger,
.hzo-button--danger {
    background: var(--btn-danger-bg, var(--error)) !important;
    color: #fff !important;
    border-color: var(--btn-danger-bg, var(--error));
}
.hzo-btn--danger:hover,
.hzo-button--danger:hover {
    background: var(--btn-danger-hover, #B91C1C) !important;
    border-color: var(--btn-danger-hover, #B91C1C);
    color: #fff !important;
}

/* A little depth on hover so filled buttons feel tactile instead of flat — a
   default-polish touch; per-client themes still control everything via --btn-*.
   Selector list carries the S4 .hzo-button alias so both vocabularies match. */
.hzo-btn--primary:hover,
.hzo-btn--secondary:hover,
.hzo-btn--danger:hover,
.hzo-button--primary:hover,
.hzo-button--secondary:hover,
.hzo-button--danger:hover { box-shadow: var(--shadow-md); }

.hzo-btn--sm,
.hzo-button--sm { padding: calc(var(--btn-padding-y, 14px) - 4px) calc(var(--btn-padding-x, 32px) - 10px); font-size: 0.875rem; min-height: 36px; }
.hzo-btn--lg,
.hzo-button--lg { padding: calc(var(--btn-padding-y, 14px) + 2px) calc(var(--btn-padding-x, 32px) + 8px); font-size: 1.125rem; min-height: 52px; }

/* Full-width — stretches to the container at all breakpoints. (Mobile already
   stretches non-inline buttons; this opts desktop in too — e.g. a form submit
   or a stacked CTA pair on a narrow card.) */
.hzo-btn--block,
.hzo-button--block { width: 100%; }

/* Icon-only — square button sized to the button height, for a lone icon
   (search / menu / close, nav arrows, social). Pair with --sm / --lg to
   resize. Always intrinsic width (excluded from the mobile full-width rule
   below). The author MUST supply an aria-label — there is no text node. */
.hzo-btn--icon,
.hzo-button--icon { padding: 0; width: 48px; min-width: 48px; }
.hzo-btn--icon.hzo-btn--sm,
.hzo-button--icon.hzo-button--sm { width: 36px; min-width: 36px; }
.hzo-btn--icon.hzo-btn--lg,
.hzo-button--icon.hzo-button--lg { width: 52px; min-width: 52px; }
.hzo-btn--icon > svg,
.hzo-button--icon > svg { width: 20px; height: 20px; }

/* Link button — button semantics, link appearance. For the lowest-emphasis
   inline actions ("Cancel", "Skip"). !important on color/background matches the
   base button rules so it wins the per-client a:visited fight (see the
   bottom-of-file note) — no stale :visited rule needed. */
.hzo-btn--link,
.hzo-button--link {
    background: transparent !important;
    color: var(--brand-text) !important;
    border-color: transparent;
    min-height: 0;
    padding: var(--space-xs) 0;
    border-radius: var(--radius-0);
    text-decoration: underline;
    text-underline-offset: 3px;
}
.hzo-btn--link:hover,
.hzo-button--link:hover {
    background: transparent !important;
    color: var(--brand-hover-on-dark) !important;
    text-decoration-thickness: 2px;
}
.hzo-btn--on-dark.hzo-btn--link,
.hzo-btn--on-dark.hzo-btn--link:hover,
.hzo-button--on-dark.hzo-button--link,
.hzo-button--on-dark.hzo-button--link:hover { color: #fff !important; }

/* Mobile: buttons go full-width by default for easier tapping + stacked
   layout. Opt out with .hzo-btn--inline when a button needs to stay
   intrinsic width on mobile (side-by-side pairs, etc.). Icon + link buttons
   are intrinsic-width by nature, so they're excluded too. */
@media (max-width: 767px) {
    .hzo-btn:not(.hzo-btn--inline):not(.hzo-btn--icon):not(.hzo-btn--link),
.hzo-button:not(.hzo-button--inline):not(.hzo-button--icon):not(.hzo-button--link) { width: 100%; }
}

/* ═══ MODIFIER vs VARIANT — the contract (PF-062) ══════════════════════════
   Read this before adding any class to this file.

   A VARIANT answers "which button is this": --primary, --secondary, --outline,
   --ghost, --tertiary, --link, --danger. Exactly one per button. It sets the
   fill, the ink and the border.

   A MODIFIER answers "under what conditions": --on-dark, --sm, --lg, --block,
   --inline, --icon. Zero or more. It ADJUSTS a variant. It must never be the
   only thing a button carries and expect to look like anything.

   THE FAILURE THIS PREVENTS. Before Wave S every --on-dark rule in this file
   was written as a COMPOUND (.hzo-btn--on-dark.hzo-btn--secondary, …), so
   `class="hzo-btn hzo-btn--on-dark hzo-btn--lg"` matched no on-dark rule at
   all, fell through to the base .hzo-btn, and rendered as the ACCENT FILL —
   a second primary button, sitting next to the real primary, with no visual
   hierarchy between them. Nothing errored. The class was real, the markup
   looked deliberate, the button rendered perfectly. It was visible only in a
   screenshot, and the engine shipped it in its own reference patterns.

   TWO RULES FOLLOW, and both are enforced below:
   1. Every modifier gets a BARE fallback that degrades to something sensible,
      so a missing variant is a slightly-wrong button rather than a silently
      wrong one.
   2. Reference patterns name the variant explicitly. A pattern is a teaching
      surface; it should model the contract, not the shorthand.

   When adding a new modifier to this file, write its bare rule first.
   ═══════════════════════════════════════════════════════════════════════════ */

/* BARE --on-dark FALLBACK (PF-062). Degrades to the on-dark OUTLINE treatment:
   transparent fill, white ink, white border. That is the one answer correct in
   BOTH on-dark contexts — over a photo (where a filled secondary fights the
   image) and over a solid brand band (where an outline is clearly subordinate
   to the accent primary).

   The :not() chain is load-bearing, not defensive noise. This rule is declared
   AFTER the variant rules above, and `.hzo-btn--on-dark` alone is (0,1,0) —
   the same specificity as `.hzo-btn--primary`. Without the exclusions it would
   win on source order and silently turn every correctly-authored
   `--primary --on-dark` button into an outline. Every variant in this file
   must appear in the chain; add new ones here when you add them above. */
.hzo-btn--on-dark:not(.hzo-btn--primary):not(.hzo-btn--secondary):not(.hzo-btn--tertiary):not(.hzo-btn--outline):not(.hzo-btn--ghost):not(.hzo-btn--link):not(.hzo-btn--danger),
.hzo-button--on-dark:not(.hzo-button--primary):not(.hzo-button--secondary):not(.hzo-button--tertiary):not(.hzo-button--outline):not(.hzo-button--ghost):not(.hzo-button--link):not(.hzo-button--danger) {
    background: transparent !important;
    color: var(--btn-on-dark-accent, #fff) !important;
    border-color: var(--btn-on-dark-accent, #fff);
}
.hzo-btn--on-dark:not(.hzo-btn--primary):not(.hzo-btn--secondary):not(.hzo-btn--tertiary):not(.hzo-btn--outline):not(.hzo-btn--ghost):not(.hzo-btn--link):not(.hzo-btn--danger):hover,
.hzo-button--on-dark:not(.hzo-button--primary):not(.hzo-button--secondary):not(.hzo-button--tertiary):not(.hzo-button--outline):not(.hzo-button--ghost):not(.hzo-button--link):not(.hzo-button--danger):hover {
    background: var(--btn-on-dark-accent, #fff) !important;
    color: var(--btn-on-dark-text, var(--brand)) !important;
}

/* Dark-section button overrides.
   On brand-colored dark sections, the SECONDARY button (now blue) would
   disappear since it shares the section's color. Flip it to a white button
   with brand text for clear visibility. Primary (green) stays green — it
   has plenty of contrast against blue — and its hover gets the lighter
   on-dark variant for extra feedback. !important on color/background
   matches base rules so on-dark variants win the :visited fight. */
.hzo-btn--on-dark.hzo-btn--primary:hover,
.hzo-button--on-dark.hzo-button--primary:hover {
    /* Palest-accent fill on dark — accent family, lightest stop (--accent-pale);
       the dark accent-on-fill text (--text-on-accent) reads cleanly on it. */
    background: var(--accent-pale) !important;
    border-color: var(--accent-pale);
    color: var(--text-on-accent) !important;
}
.hzo-btn--on-dark.hzo-btn--secondary,
.hzo-button--on-dark.hzo-button--secondary {
    background: var(--btn-on-dark-bg, #fff) !important;
    color: var(--btn-on-dark-text, var(--brand)) !important;
    border-color: var(--btn-on-dark-bg, #fff);
}
.hzo-btn--on-dark.hzo-btn--secondary:hover,
.hzo-button--on-dark.hzo-button--secondary:hover {
    /* Clear branded tint away from white so the hover obviously reads. */
    background: color-mix(in srgb, var(--btn-on-dark-bg, #ffffff) 75%, var(--btn-on-dark-text, var(--brand))) !important;
    border-color: color-mix(in srgb, var(--btn-on-dark-bg, #ffffff) 75%, var(--btn-on-dark-text, var(--brand)));
    color: var(--brand-hover-on-light) !important;
}
.hzo-btn--on-dark.hzo-btn--outline,
.hzo-button--on-dark.hzo-button--outline {
    color: var(--btn-on-dark-accent, #fff) !important;
    border-color: var(--btn-on-dark-accent, #fff);
    background: transparent !important;
}
.hzo-btn--on-dark.hzo-btn--outline:hover,
.hzo-button--on-dark.hzo-button--outline:hover {
    background: var(--btn-on-dark-accent, #fff) !important;
    color: var(--btn-on-dark-text, var(--brand)) !important;
}
/* Tertiary on dark — the same "quiet but visible" job, inverted: a low-opacity
   white veil rather than the light neutral fill, which would read as a second
   white (secondary) button on a dark band. */
.hzo-btn--on-dark.hzo-btn--tertiary,
.hzo-button--on-dark.hzo-button--tertiary {
    background: rgba(255, 255, 255, 0.14) !important;
    color: var(--btn-on-dark-accent, #fff) !important;
    border-color: rgba(255, 255, 255, 0.34);
}
.hzo-btn--on-dark.hzo-btn--tertiary:hover,
.hzo-button--on-dark.hzo-button--tertiary:hover {
    background: rgba(255, 255, 255, 0.26) !important;
    color: var(--btn-on-dark-accent, #fff) !important;
    border-color: rgba(255, 255, 255, 0.55);
}
.hzo-btn--on-dark.hzo-btn--ghost,
.hzo-button--on-dark.hzo-button--ghost { color: var(--btn-on-dark-accent, #fff) !important; }
.hzo-btn--on-dark.hzo-btn--ghost:hover,
.hzo-button--on-dark.hzo-button--ghost:hover {
    background: rgba(255,255,255,0.2) !important;
    color: var(--btn-on-dark-accent, #fff) !important;
}

/* Loading state — add `is-loading` + `aria-busy="true"`. The label is hidden
   (color → transparent) and a spinner is painted centered; the label still
   occupies space so the button keeps its width and doesn't reflow. pointer-
   events off blocks a double-submit. The ring color is set per variant so it
   reads on each background. */
.hzo-btn.is-loading,
.hzo-button.is-loading {
    color: transparent !important;
    pointer-events: none;
    position: relative;
}
.hzo-btn.is-loading::after,
.hzo-button.is-loading::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 18px;
    height: 18px;
    margin: -9px 0 0 -9px;
    border-radius: 50%;
    /* Default: dark ring, reads on the accent/primary fill. */
    border: var(--border-2) solid color-mix(in srgb, var(--text-on-accent) 30%, transparent);
    border-top-color: var(--text-on-accent);
    animation: hzo-spin 0.6s linear infinite;
}
/* Filled-dark variants → white ring. */
.hzo-btn--secondary.is-loading::after,
.hzo-btn--danger.is-loading::after,
.hzo-button--secondary.is-loading::after,
.hzo-button--danger.is-loading::after {
    border-color: rgba(255, 255, 255, 0.35);
    border-top-color: #fff;
}
/* Transparent variants → brand ring. */
.hzo-btn--outline.is-loading::after,
.hzo-btn--ghost.is-loading::after,
.hzo-btn--link.is-loading::after,
.hzo-button--outline.is-loading::after,
.hzo-button--ghost.is-loading::after,
.hzo-button--link.is-loading::after {
    border-color: color-mix(in srgb, var(--brand) 30%, transparent);
    border-top-color: var(--brand);
}
@media (prefers-reduced-motion: reduce) {
    .hzo-btn.is-loading::after,
.hzo-button.is-loading::after { animation-duration: 1.5s; }
}

/* Added confirmation (add-to-cart) — briefly swaps the label for a success check
   after a successful add. Mirrors `.is-loading`: label hidden, glyph centered, no
   reflow, pointer-events off. The JS driver (buttons.js, this folder) toggles it on, fires
   hzo.toast('Added to cart'), then reverts to default after a short window. It's a
   state swap, not an animation, so it's reduced-motion-safe by construction. An
   integration wanting a persistent state sets the label to "In cart" instead. */
.hzo-btn.is-added,
.hzo-button.is-added {
    color: transparent !important;
    pointer-events: none;
    position: relative;
}
.hzo-btn.is-added::after,
.hzo-button.is-added::after {
    content: "\2713"; /* ✓ */
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 1.15em;
    font-weight: var(--fw-700);
    line-height: var(--lh-none);
    color: var(--text-on-accent); /* reads on the primary/accent fill */
}
.hzo-btn--secondary.is-added::after,
.hzo-btn--danger.is-added::after,
.hzo-button--secondary.is-added::after,
.hzo-button--danger.is-added::after { color: #fff; }
.hzo-btn--outline.is-added::after,
.hzo-btn--ghost.is-added::after,
.hzo-btn--link.is-added::after,
.hzo-button--outline.is-added::after,
.hzo-button--ghost.is-added::after,
.hzo-button--link.is-added::after { color: var(--brand); }
/* Tertiary's fill is neutral, so the check takes the neutral heading ink
   rather than the brand (which would be the only brand-coloured thing on an
   intentionally brand-free button). */
.hzo-btn--tertiary.is-added::after,
.hzo-button--tertiary.is-added::after { color: var(--btn-tertiary-text, var(--text-heading)); }


/* --- callback/callback.css --- */
/* ═══════════════════════════════════════════════════════════════════════════
   Instant Callback Request (.hzo-callback) — W-041

   The lowest-friction phone-first lead form: masked tel field + "Call me back",
   optional when-window chips (native radios), a promise line that swaps to
   after-hours copy, and a success surface (countdown / thank-you / tel). The
   default strip is a single column; layout=inline packs field+CTA on one row
   (auto-collapsing on a narrow container, no JS); layout=card wraps it in a
   surface panel.

   Tokens ONLY — --space-* / --radius-* / --shadow-* / --dur-* / --ease-* /
   --border-* / --text-* / --surface / --accent* / --error* / --focus-ring /
   --fs-* / --lh-*. No hardcoded hex; px only for hairline borders / icon sizing
   where existing components already do. The widget renders no top-layer surface,
   so it claims NO z-index (inline errors never elevate above --z-toast).

   GATING: ALWAYS-LOAD (callback.php un-gates the shortcode) — the CSS ships
   wherever the shortcode can render (sticky/hero/footer).
   ═══════════════════════════════════════════════════════════════════════════ */

.hzo-callback {
    container-type: inline-size;
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    margin: 0;
    max-width: 32rem;
    text-align: start;
}
.hzo-callback--align-center { margin-inline: auto; align-items: stretch; text-align: center; }
.hzo-callback--align-start  { margin-inline: 0; }

/* ── Card layout: elevate onto a surface panel ── */
.hzo-callback--card {
    padding: var(--space-lg);
    background: var(--surface);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-2);
    box-shadow: var(--shadow-2);
}

.hzo-callback__heading {
    margin: 0;
    font-size: var(--fs-500);
    line-height: var(--lh-snug);
    color: var(--text-heading);
}

/* ── Promise line (in-hours ↔ after-hours copy swap target) ── */
.hzo-callback__promise {
    margin: 0;
    color: var(--text-body);
    font-size: var(--fs-200);
    line-height: var(--lh-normal);
    transition: color var(--dur-1) var(--ease-standard);
}
.hzo-callback.is-after-hours .hzo-callback__promise { color: var(--text-muted); }

/* ── Field ── */
.hzo-callback__field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
}
.hzo-callback__label {
    font-size: var(--fs-100);
    font-weight: 600;
    color: var(--text-heading);
    line-height: var(--lh-snug);
}
.hzo-callback__input {
    width: 100%;
    padding: var(--space-smd) var(--space-md);
    font-size: var(--fs-300);          /* ≥16px avoids iOS zoom-on-focus */
    font-family: var(--font-body);
    color: var(--text-body);
    background: var(--bg-page);
    border: var(--border-1) solid var(--input-border, var(--border));
    border-radius: var(--radius-1);
    min-height: 48px;
    transition: border-color var(--dur-1) var(--ease-standard), box-shadow var(--dur-1) var(--ease-standard);
}
.hzo-callback__input:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: var(--space-2xs);
    border-color: var(--focus-ring);
}
/* Visible error border (never invisible — G49) + error-tinted ring. */
.hzo-callback__input--error,
.hzo-callback__input[aria-invalid="true"] {
    border-color: var(--error);
}
.hzo-callback__input--error:focus-visible,
.hzo-callback__input[aria-invalid="true"]:focus-visible {
    outline-color: var(--error);
    border-color: var(--error);
}

.hzo-callback__error {
    color: var(--error-ink);
    font-size: var(--fs-100);
    line-height: var(--lh-snug);
}

/* ── When-window chips (native radios styled as chips; APG radiogroup) ── */
.hzo-callback__window {
    margin: 0;
    padding: 0;
    border: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}
.hzo-callback__legend {
    padding: 0;
    font-size: var(--fs-100);
    font-weight: 600;
    color: var(--text-heading);
}
.hzo-callback__chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
}
.hzo-callback__chip {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;                 /* comfortable tap height */
    padding: var(--space-sm) var(--space-md);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-full);
    color: var(--text-body);
    background: var(--bg-page);
    font-size: var(--fs-100);
    line-height: 1;
    cursor: pointer;
    transition: border-color var(--dur-1) var(--ease-standard),
                background var(--dur-1) var(--ease-standard),
                color var(--dur-1) var(--ease-standard);
}
.hzo-callback__chip:hover { border-color: var(--accent-text); }
/* The real radio is visually hidden but focus lands on it → ring the label. */
.hzo-callback__chip-input {
    position: absolute;
    opacity: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    inset: 0;
    cursor: pointer;
}
.hzo-callback__chip:has(.hzo-callback__chip-input:checked) {
    background: var(--accent-pale);
    border-color: var(--accent-text);
    color: var(--accent-text);
    font-weight: 600;
}
.hzo-callback__chip:has(.hzo-callback__chip-input:focus-visible) {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: var(--space-2xs);
}
.hzo-callback__chip:active { transform: translateY(1px); }

/* ── Consent (TCPA) ── */
.hzo-callback__consent { font-size: var(--fs-75); color: var(--text-muted); line-height: var(--lh-normal); }
.hzo-callback__consent-label {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    cursor: pointer;
}
.hzo-callback__consent-box {
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    margin-top: 2px;
    accent-color: var(--accent-text);
}
.hzo-callback__consent-box:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: var(--space-2xs);
}
.hzo-callback__consent-text { margin: 0; }

/* ── Honeypot — off-screen, never shown, never announced ── */
.hzo-callback__hp {
    position: absolute !important;
    left: -9999px !important;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* ── Submit CTA (extends .hzo-btn --primary; adds a busy spinner) ── */
.hzo-callback__submit {
    position: relative;
    width: 100%;
}
.hzo-callback--inline .hzo-callback__submit { width: auto; }
.hzo-callback__spinner {
    display: none;
    width: 1em;
    height: 1em;
    margin-inline-start: var(--space-sm);
    border: var(--border-2) solid currentColor;
    border-top-color: transparent;
    border-radius: var(--radius-full);
    animation: hzo-callback-spin var(--dur-4) linear infinite;
}
.hzo-callback__submit.is-loading .hzo-callback__spinner { display: inline-block; }
@keyframes hzo-callback-spin { to { transform: rotate(360deg); } }

/* ── Inline layout: field + CTA share a row; collapses on a narrow container ── */
.hzo-callback--inline {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-end;
    max-width: none;
}
.hzo-callback--inline .hzo-callback__promise { flex: 1 0 100%; }
.hzo-callback--inline .hzo-callback__field { flex: 1 1 12rem; }
@container (max-width: 26rem) {
    .hzo-callback--inline { flex-direction: column; align-items: stretch; }
    .hzo-callback--inline .hzo-callback__submit { width: 100%; }
}

/* ── Success surface (role=status; server ships it hidden + empty) ── */
.hzo-callback__success {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    align-items: inherit;
}
.hzo-callback--align-center .hzo-callback__success { align-items: center; text-align: center; }
.hzo-callback__success-title {
    margin: 0;
    font-size: var(--fs-400);
    line-height: var(--lh-snug);
    color: var(--text-heading);
}
.hzo-callback__success-title:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: var(--space-2xs);
    border-radius: var(--radius-1);
}
.hzo-callback__success-text { margin: 0; color: var(--text-body); font-size: var(--fs-200); }

/* success=tel — a real, tappable, ≥44px tel link (never auto-dials) ── */
.hzo-callback__success-tel {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 48px;
    padding: var(--space-smd) var(--space-lg);
    border-radius: var(--radius-full);
    background: var(--accent-pale);
    color: var(--accent-text);
    font-weight: 600;
    text-decoration: none;
    transition: background var(--dur-1) var(--ease-standard);
}
.hzo-callback__success-tel:hover { background: var(--accent); color: var(--text-on-accent); }
.hzo-callback__success-tel:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: var(--space-2xs);
}

/* ── Countdown ring (decorative; number carries the meaning) ── */
.hzo-callback__countdown {
    --hzo-cb-progress: 0;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-xs);
}
.hzo-callback__countdown-num {
    display: grid;
    place-items: center;
    width: 4.5rem;
    height: 4.5rem;
    border-radius: var(--radius-full);
    font-size: var(--fs-500);
    font-family: var(--font-number, var(--font-body));
    font-weight: 700;
    color: var(--accent-text);
    /* Decorative progress dial: an accent wedge grows over a quiet track as the
       countdown elapses (--hzo-cb-progress 0→1). The inner disc keeps the number
       legible. Purely visual — the number/text carry the meaning. */
    background:
        radial-gradient(var(--surface) 54%, transparent 55%),
        conic-gradient(var(--accent) calc(var(--hzo-cb-progress) * 360deg), var(--border) 0deg);
}
.hzo-callback__countdown-text { color: var(--text-body); font-size: var(--fs-100); }

/* ═══ Reduced motion — spinner slows (never freezes), ring goes static ═══════ */
@media (prefers-reduced-motion: reduce) {
    .hzo-callback__spinner { animation-duration: 2s; }
    .hzo-callback__countdown-num { background: var(--surface-2); }
    .hzo-callback__promise,
    .hzo-callback__input,
    .hzo-callback__chip,
    .hzo-callback__success-tel { transition: none; }
}

/* ═══ Forced colors — real borders + aria-invalid carry state (never shadow/
   opacity alone); the ring falls back to the static remaining-time text ═══════ */
@media (forced-colors: active) {
    .hzo-callback__input { border: 1px solid ButtonBorder; }
    .hzo-callback__input[aria-invalid="true"] { border-color: Mark; }
    .hzo-callback__chip { border: 1px solid ButtonBorder; }
    .hzo-callback__chip:has(.hzo-callback__chip-input:checked) { border-color: Highlight; }
    .hzo-callback__countdown-num { background: ButtonFace; forced-color-adjust: none; }
}

/* ═══ Print — force the form + promise + consent visible; hide the ring; keep
   the success tel number so a printed estimate keeps the callback path ═══════ */
@media print {
    .hzo-callback,
    .hzo-callback__promise,
    .hzo-callback__consent { display: flex !important; }
    .hzo-callback__spinner,
    .hzo-callback__countdown-num { display: none !important; }
    .hzo-callback__success[hidden] { display: block !important; }
}


/* --- callout/callout.css --- */
/* callout — @tokens: --callout-gap, --callout-padding, --callout-radius

   A static inline note box for content emphasis (info / tip / success / warning
   / danger): a tinted panel with a coloured left accent bar. Distinct from the
   `.hzo-alert` feedback component — alert is dismissible UI feedback; callout is
   permanent editorial emphasis inside prose. Primitive: CSS-only, not
   interactive (no focus/disabled idioms).

   Token-only: the base bar reads --brand and the tint reads --bg-alt; each
   modifier swaps to a state colour + its matching -bg tint (all four state
   colours have a -bg pair in tokens.css). The accent-bar width has no semantic
   role, so it's a component token with a --border-3 fallback (the icon-button.css
   dial pattern). Gap + padding follow the same fallback shape. */
.hzo-callout {
    display: flex;
    gap: var(--callout-gap, var(--space-md));
    padding: var(--callout-padding, var(--space-md));
    border-radius: var(--callout-radius, var(--radius-panel));
    border-left: var(--callout-bar-width, var(--border-3)) solid var(--brand);
    background: var(--bg-alt);
    color: var(--text-body);
}

/* Optional leading glyph — sits in the numeral/heading ink so it reads as chrome,
   not body copy. Sized to the body line-box; doesn't stretch in the flex row. */
.hzo-callout__icon {
    flex: 0 0 auto;
    line-height: var(--lh-none);
    color: var(--text-heading);
}

/* Text stack — title + body live in a column beside the icon. */
.hzo-callout__title {
    font-weight: var(--fw-600);
    line-height: var(--lh-snug);
    color: var(--text-heading);
}
.hzo-callout__body {
    line-height: var(--lh-normal);
    color: var(--text-body);
}

/* Intent modifiers — recolor the accent bar + tint together. Each reads its
   state colour and paired -bg tint (no borrowed tokens). Danger maps to the
   error role. */
.hzo-callout--info    { border-left-color: var(--info);    background: var(--info-bg); }
.hzo-callout--success { border-left-color: var(--success); background: var(--success-bg); }
.hzo-callout--warning { border-left-color: var(--warning); background: var(--warning-bg); }
.hzo-callout--danger  { border-left-color: var(--error);   background: var(--error-bg); }


/* --- card/card-base.css --- */
/* ─── Cards ─────────────────────────────────────────────────────────── */
/* Base card rules — moved verbatim from card.css when card.css became the
   card-family import manifest (Phase-2 widgetization). CSS requires @imports
   before any rule, and card-variants.css must cascade AFTER these base rules
   (equal-specificity overrides like --bordered's box-shadow), so the rules
   had to move out of the manifest file. Content is otherwise unchanged. */
.hzo-card {
    background: var(--card-bg, #fff);
    border: 1px solid var(--card-border, var(--border));
    border-radius: var(--card-radius, var(--radius-panel));
    padding: var(--card-padding, var(--space-lg));
    box-shadow: var(--card-shadow, var(--shadow-sm)); /* soft rest elevation; widget Shadow control overrides */
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    /* opacity rides along so .hzo-reveal works on cards directly — this
       shorthand outranks .hzo-reveal's by source order, so without the
       opacity entry a revealed card would snap instead of fade. */
    transition: box-shadow var(--transition-base), transform var(--transition-base), opacity 0.6s ease;
}
/* Only cards that are actually clickable (contain a link) get the hover lift —
   static info cards stay flat. :has() is well-supported in current browsers. */
.hzo-card:has(a):hover { box-shadow: var(--card-hover-shadow, var(--shadow-lg)); transform: translateY(-4px); }
@media (prefers-reduced-motion: reduce) { .hzo-card:has(a):hover { transform: none; } }
/* Badges, icons, and other inline-sized elements inside cards shouldn't
   stretch to full width (default flex behavior). */
.hzo-card > .hzo-badge,
.hzo-card > .hzo-card__icon { align-self: flex-start; }
.hzo-card__icon {
    width: 56px;
    height: 56px;
    background: var(--brand-pale);
    border-radius: var(--radius-control);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--brand);
    font-size: 28px;
    line-height: var(--lh-none);
    margin-bottom: var(--space-sm);
}
.hzo-card__title {
    font-family: var(--font-heading);
    font-size: 1.25rem;
    font-weight: var(--fw-700, 700);
    color: var(--text-heading);
    margin: 0;
    letter-spacing: 0.015em;
}
.hzo-card__text {
    color: var(--text-muted);
    margin: 0;
    line-height: 1.6;
}
/* !important on color: fights a:visited from per-client main.css.
   Matching :visited rule at the bottom of this file preserves the color
   after the link has been clicked. */
.hzo-card__link {
    position: relative;  /* anchor for the ::before underline (Bucket-2) */
    color: var(--accent-text) !important;
    font-weight: var(--fw-600, 600);
    text-decoration: none;
    align-self: flex-start;  /* inline — don't stretch to full card width */
    margin-top: auto;
    padding-top: var(--space-sm);
    transition: color 0.15s;
}
.hzo-card__link:hover { color: color-mix(in srgb, var(--accent-text) 70%, black) !important; }
/* Auto-rendered CTA arrow (bold Lucide arrow-right, masked so it inherits the
   link color). Sized 1em — same scale as the label — and floats right on hover.
   Replaces the old literal "→" glyph in the markup (cleaner + animatable). */
.hzo-card__link::after {
    content: "";
    display: inline-block;
    width: 1em;
    height: 1em;
    margin-left: 0.4em;
    vertical-align: -0.12em;
    flex-shrink: 0;
    background-color: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E") no-repeat center / contain;
            mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E") no-repeat center / contain;
    transition: transform var(--duration-slow, 400ms) var(--ease-spring, cubic-bezier(0.34, 1.56, 0.5, 1));
}
.hzo-card__link:hover::after,
.hzo-card__link:focus-visible::after { transform: translateX(5px); }
@media (prefers-reduced-motion: reduce) {
    .hzo-card__link::after { transition: none; }
}
/* Bucket-2 engine-default underline — a restrained 2px hover underline that
   coexists with the ::after arrow above (::before never collides). Token-driven
   (var(--link-underline, currentColor)); a client's own custom.css ::before wins
   (loads last). Design intent: card hover = underline grows + arrow floats. */
.hzo-card__link::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -2px;
    height: 2px;
    background: var(--link-underline, currentColor);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform .2s ease;
    pointer-events: none;
}
.hzo-card__link:hover::before,
.hzo-card__link:focus-visible::before { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
    .hzo-card__link::before { transition: none; }
}


/* --- card/card-media.css --- */
/* ─── Card media (post / blog cards) ──────────────────────────────────
   Full-bleed image at the top of a .hzo-card — negative margins cancel the
   card's padding so the image meets the inner edges; top corners stay rounded. */
.hzo-card__media {
    margin: calc(-1 * var(--card-padding, var(--space-lg))) calc(-1 * var(--card-padding, var(--space-lg))) 0;
    border-radius: var(--card-radius, var(--radius-panel)) var(--card-radius, var(--radius-panel)) 0 0;
    overflow: hidden;
    aspect-ratio: 16 / 9;
}
.hzo-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform var(--duration-slow, 400ms) var(--ease-standard, ease); }
/* Alternate media ratios (default is 16/9 above). */
.hzo-card__media--square { aspect-ratio: 1 / 1; }
.hzo-card__media--wide   { aspect-ratio: 21 / 9; }
/* Subtle image zoom on hover for clickable cards (reduced-motion-safe). */
.hzo-card:has(a):hover .hzo-card__media img { transform: scale(1.05); }
@media (prefers-reduced-motion: reduce) {
    .hzo-card:has(a):hover .hzo-card__media img { transform: none; }
}


/* --- card/card-variants.css --- */
/* ─── Card variants (extend .hzo-card; compose its subparts) ──────────
   Depth choice per art-direction §5 "flat-with-intent": --bordered = flat +
   border (the system default, made explicit); --elevated = shadow, no border.
   Zero new tokens — bordered uses --border, elevated uses --shadow-md. */
.hzo-card--bordered { box-shadow: none; border-color: var(--border); }
.hzo-card--elevated { box-shadow: var(--card-elevated-shadow, var(--shadow-md)); border-color: transparent; }

/* Body wrapper — groups title/text/CTA, mainly for the horizontal layout. */
.hzo-card__body { display: flex; flex-direction: column; gap: var(--space-sm); min-width: 0; }

/* Horizontal card — media beside body; stacks to vertical at the mobile
   breakpoint (767px, the system's mobile boundary). Reuses __media/__body. */
.hzo-card--horizontal { flex-direction: row; align-items: stretch; gap: var(--space-lg); }
.hzo-card--horizontal > .hzo-card__media {
    flex: 0 0 40%;
    aspect-ratio: auto;          /* fill the card height instead of forcing 16/9 */
    margin: calc(-1 * var(--card-padding, var(--space-lg))) 0 calc(-1 * var(--card-padding, var(--space-lg))) calc(-1 * var(--card-padding, var(--space-lg)));
    border-radius: var(--card-radius, var(--radius-panel)) 0 0 var(--card-radius, var(--radius-panel));
}
.hzo-card--horizontal > .hzo-card__body { flex: 1 1 auto; }
@media (max-width: 767px) {
    .hzo-card--horizontal { flex-direction: column; gap: var(--space-md); }
    .hzo-card--horizontal > .hzo-card__media {
        flex-basis: auto;
        aspect-ratio: 16 / 9;
        margin: calc(-1 * var(--card-padding, var(--space-lg))) calc(-1 * var(--card-padding, var(--space-lg))) 0;
        border-radius: var(--card-radius, var(--radius-panel)) var(--card-radius, var(--radius-panel)) 0 0;
    }
}

/* Link card — the WHOLE card is one clickable target (accessible stretched-link:
   a real anchor on the title, ::after covers the card). The card must contain
   exactly ONE interactive element (the title link). Hover lift comes from the
   base .hzo-card:has(a):hover rule; the focus ring shows for keyboard only. */
.hzo-card--link { position: relative; }
.hzo-card--link .hzo-card__title a { color: inherit; text-decoration: none; background-image: none; }
.hzo-card--link .hzo-card__title a::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.hzo-card--link:has(.hzo-card__title a:focus-visible) { outline: 2px solid var(--color-focus); outline-offset: 2px; }

/* PERSISTENT AFFORDANCE (Wave S craft default). Until now a link card's ONLY
   signal that it was clickable was the hover lift — the title's underline is
   removed by the rule above, precisely so the card reads as one surface. That
   leaves three groups with no signal at all: every touch user (no hover
   exists), every screenshot the work is reviewed from, and anyone scanning the
   page without moving the pointer. "Looks like a card, happens to be a link"
   is exactly the rest-state affordance failure quality-rules calls out.

   The fix is a resting arrow marker on the title. It is:
     visible at rest    — the affordance, and the part that matters
     animated on hover  — so hover still changes a second property alongside
                          the existing lift
   Rendered on the TITLE element's ::after, not the anchor's, because the
   anchor's ::after is already spent on the stretched-link overlay above.
   Masked currentColor-style fill (the same Lucide arrow .hzo-card__link uses),
   so it needs no icon system. Set --card-link-marker: none to opt a card
   family out entirely.

   A .hzo-card--link with no .hzo-card__title gets no marker — its affordance
   is then whatever its own content provides, which is a decision the author
   has to make deliberately rather than inherit. */
.hzo-card--link .hzo-card__title::after {
    content: var(--card-link-marker, "");
    display: inline-block;
    width: 0.68em;
    height: 0.68em;
    margin-left: 0.4em;
    vertical-align: -0.02em;
    background-color: var(--card-link-marker-color, var(--accent-text));
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E") no-repeat center / contain;
            mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E") no-repeat center / contain;
    transition: transform var(--duration-slow, 400ms) var(--ease-spring, cubic-bezier(0.34, 1.56, 0.5, 1));
}
.hzo-card--link:hover .hzo-card__title::after,
.hzo-card--link:focus-within .hzo-card__title::after { transform: translateX(4px); }
@media (prefers-reduced-motion: reduce) {
    .hzo-card--link .hzo-card__title::after { transition: none; }
}


/* --- card/category-card.css --- */
/* ─── Category card (a .hzo-card variant) ─────────────────────────────
   Shop-by-category / locations-index tile: media on top, centered name +
   count. Usually a whole-card link — the <a> form gets its own hover lift
   (the base .hzo-card :has(a) lift doesn't apply when the card IS the link). */
.hzo-cat-card { text-align: center; align-items: center; }
.hzo-cat-card .hzo-card__title { margin: 0; }
.hzo-cat-card__count { font-size: var(--fs-100); color: var(--text-muted); margin: 0; }
a.hzo-cat-card,
a.hzo-cat-card:visited { text-decoration: none; color: inherit; }
a.hzo-cat-card:hover { box-shadow: var(--card-hover-shadow, var(--shadow-md)); }
a.hzo-cat-card:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }


/* --- card/equal-height-cards.css --- */
/* ─── Equal-height cards (.hzo-grid--equal-cards) ──────────────────────
   Stretches every card in a .hzo-grid to the full row height so CTAs
   pinned with margin-top:auto (.hzo-card__link, the pricing buttons)
   bottom-align across the row.

   Subgrid was evaluated and DEFERRED — honest assessment: aligning card
   footers with subgrid requires each card to become display:grid with
   grid-template-rows:subgrid plus an agreed row count, which would
   restructure .hzo-card's flex column (its gap, align-self behaviors, and
   margin-top:auto pinning) for every existing composition. height:100% +
   the card's own flex column already achieves the visible goal (equal
   cards, bottom-aligned CTAs) with zero risk to shipped cards. Revisit
   subgrid only if MID-card rows (e.g. matching price lines across tiers)
   ever need cross-card alignment. The `> *` rule covers cards nested one
   level deep (e.g. a link wrapper); grid items themselves already stretch. */
.hzo-grid--equal-cards > * { height: 100%; }
.hzo-grid--equal-cards .hzo-card { height: 100%; }


/* --- card/card.css --- */
/* ─── Card (family manifest) ──────────────────────────────────────────
   The card family is split across sheets: base rules (card-base.css),
   full-bleed media (card-media.css), depth/layout/link-card variants
   (card-variants.css), the category tile (category-card.css), and the
   grid-level equal-height helper (equal-height-cards.css).

   The loader enqueues ONLY <name>.css, so this file @imports every split
   sheet to give it a load path on the same request (the wave-4 video.css
   lesson: a variant sheet without an @import here is dead CSS in
   components mode — before this manifest, all four variant sheets only
   loaded via the assets/css/primitives.css fallback).

   Import order mirrors primitives.css (card.css → card-media →
   card-variants → category-card → equal-height-cards). card-variants
   overrides base .hzo-card declarations at EQUAL specificity
   (--bordered's box-shadow:none, --horizontal's flex-direction:row), so
   base MUST come first — and since CSS requires @imports before any
   rule, the base rules live in card-base.css rather than here. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */


/* --- carousel/carousel-crossfade.css --- */
/* ─── Carousel crossfade mode ─────────────────────────────────────────
   .hzo-carousel--fade: slides stack in one grid cell and fade instead of
   sliding — the right feel for full-bleed hero carousels and testimonial
   decks. The tallest slide sizes the deck (grid stacking — no JS height
   sync). main.js toggles .is-active per slide; nav/dots/autoplay/inert
   logic is shared with the slide mode. */
.hzo-carousel--fade .hzo-carousel__track {
    display: grid;
    transform: none !important;   /* fade mode never translates */
    transition: none;
}
.hzo-carousel--fade .hzo-carousel__slide {
    grid-area: 1 / 1;
    opacity: 0;
    /* visibility rides along so inactive slides (and their CTAs) are also
       removed from hit-testing + tab order when JS never runs (no inert
       without JS). It interpolates discretely: the outgoing slide stays
       visible through the crossfade, then hides at transition end. */
    visibility: hidden;
    transition: opacity var(--carousel-fade-duration, 500ms) ease, visibility var(--carousel-fade-duration, 500ms);
}
.hzo-carousel--fade .hzo-carousel__slide.is-active { opacity: 1; visibility: visible; }
@media (prefers-reduced-motion: reduce) {
    .hzo-carousel--fade .hzo-carousel__slide { transition: none; }
}


/* --- carousel/carousel.css --- */
/* ─── Carousel ─────────────────────────────────────────────────────────
   Core accessible carousel (one slide per view): prev/next + arrow keys,
   aria-roledescription, per-slide labels, inactive slides inert. Optional
   opt-in autoplay (data-hzo-carousel-autoplay) pauses on hover/focus and is
   disabled under reduced-motion; it needs a [data-hzo-carousel-toggle] pause
   button (WCAG 2.2.2). Touch-swipe, infinite-loop (manual), and dot indicators
   are deferred to Wave E.1. Wired by the carousel IIFE in main.js. Markup
   contract: component-patterns §38. Reuses existing tokens; zero new tokens.

   Phase-2 widget: the option contract (per-view/gap/pagination/effect/mousewheel/
   …) lives in carousel.js + carousel.php. The variant sheets are @imported here
   so the component loader (which enqueues only carousel.css) pulls them on demand. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */

.hzo-carousel { position: relative; }
.hzo-carousel__viewport { overflow: hidden; touch-action: pan-y; }  /* swipe horiz, keep vertical page scroll */
.hzo-carousel__track {
    display: flex;
    gap: var(--hzo-cv-gap, 0px);   /* px between slides; JS sets --hzo-cv-gap */
    transition: transform var(--hzo-cv-speed, var(--transition-base)) ease;
}
/* Per-view sizing: JS sets --hzo-cv-per-view / --hzo-cv-gap; the no-JS fallback
   (1 / 0px) resolves to a single 100%-wide slide, so markup still degrades to a
   plain horizontal scroller. */
.hzo-carousel__slide {
    flex: 0 0 calc((100% - (var(--hzo-cv-per-view, 1) - 1) * var(--hzo-cv-gap, 0px)) / var(--hzo-cv-per-view, 1));
    min-width: 0;
}

.hzo-carousel__btn {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border: 0;
    border-radius: var(--radius-full);
    background: var(--surface, #fff);
    color: var(--text-heading);
    box-shadow: var(--shadow-md);
    cursor: pointer;
    z-index: 1;
    transition: background var(--transition-base) ease;
}
.hzo-carousel__btn:hover { background: var(--bg-alt); }
.hzo-carousel__btn:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.hzo-carousel__btn--prev { left: 8px; }
.hzo-carousel__btn--next { right: 8px; }
.hzo-carousel__btn[disabled] { opacity: 0.4; cursor: default; box-shadow: var(--shadow-sm); }
.hzo-carousel__btn > svg { width: 22px; height: 22px; display: block; }

.hzo-carousel__toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-top: var(--space-md);
    padding: 6px var(--space-smd);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-full);
    background: var(--surface, #fff);
    color: var(--text-body);
    font-size: var(--fs-75);
    cursor: pointer;
}
.hzo-carousel__toggle:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

/* Dot indicators (generated by the carousel IIFE). 24px hit area, 10px visual dot. */
.hzo-carousel__dots { display: flex; justify-content: center; gap: var(--space-xs); margin-top: var(--space-md); }
.hzo-carousel__dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
}
.hzo-carousel__dot::before {
    content: "";
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--carousel-dot-color, var(--border));
    transition: background var(--transition-base) ease, transform var(--transition-base) ease;
}
.hzo-carousel__dot[aria-current="true"]::before { background: var(--carousel-dot-active-color, var(--brand)); transform: scale(1.3); }
.hzo-carousel__dot:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 1px; border-radius: 50%; }

/* Progress-ring dots — the active dot draws a circular countdown matching the
   autoplay interval (opt-in: data-hzo-carousel-ring-dots + autoplay; the JS
   injects the SVG and sets --carousel-ring-duration from the autoplay ms).
   The animation is keyed to [aria-current] so it restarts naturally on every
   slide change; .is-paused (hover/focus/toggle) freezes it mid-draw. Ring
   color follows the active dot color. Hidden entirely under reduced motion
   (autoplay is off there anyway). */
.hzo-carousel__dots--ring .hzo-carousel__dot { position: relative; }
.hzo-carousel__dot-ring {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    transform: rotate(-90deg);
    pointer-events: none;
}
.hzo-carousel__dot-ring circle {
    fill: none;
    stroke: var(--carousel-ring-color, var(--carousel-dot-active-color, var(--brand)));
    stroke-width: 2;
    stroke-dasharray: 62.83;   /* 2πr, r=10 in the 28×28 viewBox */
    stroke-dashoffset: 62.83;
    stroke-linecap: round;
}
.hzo-carousel__dot[aria-current="true"] .hzo-carousel__dot-ring circle {
    animation: hzo-ring-progress var(--carousel-ring-duration, 6000ms) linear forwards;
}
.hzo-carousel__dots--ring.is-paused .hzo-carousel__dot-ring circle { animation-play-state: paused; }
@keyframes hzo-ring-progress { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
    .hzo-carousel__dot-ring { display: none; }
}

/* Fraction pagination ("3 / 8") — a compact status line under the deck. */
.hzo-carousel__fraction {
    margin-top: var(--space-md);
    text-align: center;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    letter-spacing: 0.02em;
}

/* Progress-bar pagination — a track that fills as you advance. */
.hzo-carousel__progress {
    margin-top: var(--space-md);
    height: 3px;
    border-radius: var(--radius-pill);
    background: var(--carousel-dot-color, var(--border));
    overflow: hidden;
}
.hzo-carousel__progress-bar {
    display: block;
    height: 100%;
    width: 100%;
    transform-origin: left center;
    transform: scaleX(0);
    background: var(--carousel-dot-active-color, var(--brand));
    transition: transform var(--transition-base) ease;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-carousel__track,
    .hzo-carousel__btn,
    .hzo-carousel__progress-bar,
    .hzo-carousel__dot::before { transition: none; }
}


/* --- category-grid/tile-collage.css --- */
/* ─── Tile collage — connected category grid ──────────────────────────
   Rows of image tiles separated by hairline gaps; radius clips only on the
   four OUTER corners so the grid reads as one connected slab. Each tile is
   a link: photo + bottom scrim + label + CTA line, photo zoom on hover.
   Compose rows with different tile counts (a 3-up row over a 4-up row gives
   the offset-masonry feel). Mobile: rows collapse to a 2-col grid of squares
   with per-tile radius. Markup: blocks/category-tiles.html. */
.hzo-tile-collage {
    display: flex;
    flex-direction: column;
    gap: var(--tile-gap, 6px);
    border-radius: var(--tile-radius, var(--radius-panel));
    overflow: hidden;   /* clips inner corners → outer-only radius */
}
.hzo-tile-row {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;   /* column count = number of tiles in the row */
    gap: var(--tile-gap, 6px);
}
.hzo-tile {
    position: relative;
    display: block;
    aspect-ratio: var(--tile-aspect, 5 / 4.5);
    overflow: hidden;
    text-decoration: none;
    color: #fff;
}
.hzo-tile__img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--duration-slow) var(--ease-standard);
}
.hzo-tile:hover .hzo-tile__img,
.hzo-tile:focus-visible .hzo-tile__img { transform: scale(1.05); }
/* Bottom scrim — pins label readability over any photo. */
.hzo-tile::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to top,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) 88%, transparent) 0%,
        color-mix(in srgb, var(--scrim-color, var(--brand-deep)) 45%, transparent) 38%,
        transparent 70%);
}
.hzo-tile__content {
    position: absolute;
    inset: auto 0 0 0;
    padding: var(--space-md) var(--space-lg);
    z-index: 1;   /* above the scrim */
}
.hzo-tile__label {
    font-family: var(--font-heading);
    font-size: 1.35rem;
    font-weight: var(--fw-700);
    line-height: 1.2;
    margin: 0;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}
.hzo-tile__cta {
    display: inline-block;
    font-size: var(--fs-100);
    font-weight: var(--fw-600);
    margin-top: var(--space-xs);
    opacity: 0.9;
}
.hzo-tile:hover .hzo-tile__cta,
.hzo-tile:focus-visible .hzo-tile__cta { text-decoration: underline; }
.hzo-tile:focus-visible { outline: 2px solid #fff; outline-offset: -3px; }
@media (max-width: 767.98px) {
    .hzo-tile-collage { border-radius: 0; overflow: visible; }
    .hzo-tile-row { grid-auto-flow: row; grid-template-columns: 1fr 1fr; grid-auto-columns: unset; }
    .hzo-tile { aspect-ratio: 1 / 1; border-radius: var(--tile-radius, var(--radius-panel)); }
}
@media (prefers-reduced-motion: reduce) {
    .hzo-tile__img { transition: none; }
    .hzo-tile:hover .hzo-tile__img,
    .hzo-tile:focus-visible .hzo-tile__img { transform: none; }
}


/* --- category-grid/category-grid.css --- */
/* ─── Category grid ───────────────────────────────────────────────────
   Section pattern: shop-by-category (or a locations index) in two shapes —
   a multi-up grid of clickable category cards (image, name, item count;
   blocks/category-grid.html) or the connected tile-collage slab (photo
   tiles + scrim + label + hover zoom; blocks/category-tiles.html).
   Composes always-loaded pieces — .hzo-card / .hzo-cat-card /
   .hzo-card__media (components/card), .hzo-ribbon (components/ribbon),
   .hzo-grid (_base/responsive-content-grid), .hzo-eyebrow /
   .hzo-visual-h2 (base type) — and owns only the tile-collage variant,
   split into tile-collage.css.

   The loader enqueues ONLY <name>.css, so this file @imports the split
   sheet to give it a load path on the same request (the wave-4 video.css
   lesson: a variant sheet without an @import here is dead CSS in
   components mode; before this file existed, tile-collage.css only ever
   loaded through the no-components primitives.css fallback).

   .hzo-category-grid / .hzo-category-grid--cards / .hzo-category-grid--tiles
   on the [hzo_category_grid] shortcode root are structural hooks (unstyled
   by design — the pieces above carry all the visuals). */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */


/* --- check-list/check-list.css --- */
/* check-list — a checklist with token-tinted circular check marks. One column on
   mobile, two from the 600px breakpoint. CSS-only; items are `<li>` children.
   Token-only (space + brand-pale fill + accent-text check); brand-agnostic. */
.hzo-check-list {
    list-style: none;
    margin: var(--space-lg) 0 0;
    padding: 0;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-sm) var(--space-lg);
}
@media (min-width: 600px) {
    .hzo-check-list { grid-template-columns: 1fr 1fr; }
}
.hzo-check-list li {
    position: relative;
    padding-left: 1.9em;
    color: var(--text-body);
    font-weight: var(--fw-medium, 500);
    line-height: 1.4;
}
.hzo-check-list li::before {
    content: "\2713";
    position: absolute;
    left: 0;
    top: -0.05em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.35em;
    height: 1.35em;
    background: var(--brand-pale);
    color: var(--accent-text);
    border-radius: 50%;
    font-size: 0.8em;
    font-weight: 700;
}


/* --- chip/chip.css --- */
/* chip — @tokens: --chip-padding-x, --chip-padding-y

   A compact interactive pill: filter / selection token / removable tag. Sibling
   to the static `.hzo-badge` (a chip is INTERACTIVE — hover / selected / focus /
   removable — so it lives on its own). Composes inside `.hzo-tag-group`.

   Token-only: the control's padding pair has no semantic token in the scale, so
   it's a component token with a literal fallback — the same dial pattern
   icon-button.css / buttons.css use. Everything else reads a semantic/L1 token. */
.hzo-chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    padding: var(--chip-padding-y, 4px) var(--chip-padding-x, 12px);
    border: var(--border-1) solid transparent;
    border-radius: var(--radius-full);
    background: var(--bg-alt);
    color: var(--text-body);
    font-size: var(--fs-50);
    line-height: var(--lh-none);
    cursor: pointer;
    transition: background var(--transition-base) ease, color var(--transition-base) ease, border-color var(--transition-base) ease;
}

/* Leading dot / icon — optional status swatch or glyph. */
.hzo-chip__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.hzo-chip__icon > svg { display: block; }

/* Trailing × — its OWN focusable <button>, so it gets its own focus ring. */
.hzo-chip__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    border-radius: var(--radius-full);
    opacity: var(--opacity-muted);
    cursor: pointer;
    transition: opacity var(--transition-base) ease;
}
.hzo-chip__remove:hover { opacity: 1; }
.hzo-chip__remove > svg { display: block; }

/* Hover — border firms up to signal interactivity (ghost, no fill). */
.hzo-chip:hover {
    border-color: var(--border);
    color: var(--text-heading);
}

/* Selected — brand fill with on-brand ink. Two spellings of one meaning: the
   `--selected` modifier for static markup, `.is-active` for the JS-toggled state. */
.hzo-chip--selected,
.hzo-chip.is-active {
    background: var(--brand);
    color: var(--text-on-brand);
    border-color: var(--brand);
}

/* A11y — required. Focus ring on the chip AND on the remove control; disabled
   state. Reduced-motion is handled globally by components/_base/a11y.css. */
.hzo-chip:focus-visible,
.hzo-chip__remove:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}
.hzo-chip[disabled],
.hzo-chip[aria-disabled="true"],
.hzo-chip.is-disabled {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-chip,
    .hzo-chip__remove { transition: none; }
}


/* --- collage/collage.css --- */
/* collage — a photo-montage grid: one lead image spanning the full width across
   the top, the remaining images in an even two-up grid below. CSS-only layout
   primitive; images are direct children (`<img>`), so no per-image class is
   required. Token-only (space + card radius); brand-agnostic. */
/* ── ONE COMPOSITION (Wave S) ────────────────────────────────────────────
   This component ships exactly ONE composition: one full-width lead image above an even two-up grid.

   That is a feature. One composition is why it is responsive, accessible and
   identical on two hundred sites, and why a client build can place it without
   re-deciding anything. It is also a hard limit: the component cannot express
   a SECOND composition, and making it try — nesting it, stacking modifiers on
   it, overriding its grid from a client sheet — is how a page ends up fighting
   its own components and still looking generic.

   FOR AN AMPLIFY OR ELEVATE SECTION, DO NOT BEND THIS COMPONENT. Take one of:
     • RAW mode — author the section's markup directly, or
     • hand-author to the class contract on the spatial utilities:
       .hzo-grid + .hzo-span-* / .hzo-rowspan-* / --hzo-grid-template,
       .hzo-bleed, .hzo-pull-* / .hzo-z-*, .hzo-section--edge-*, .hzo-measure-*.
   Both are the STANDARD route for a high-intensity section, not an escape
   hatch, and the composition plan is what says which sections those are.

   PLUMBING AND PROOF COMPONENTS ARE UNAFFECTED. Every JS widget and every
   proof/data component (reviews, credentials, ratings, schema) stays
   mandatory — never hand-roll those. This note is about LAYOUT only.
   ────────────────────────────────────────────────────────────────────────── */

.hzo-collage {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-sm);
}
.hzo-collage img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: var(--radius-card, 12px);
    display: block;
}
.hzo-collage > img:first-child { grid-column: 1 / -1; aspect-ratio: 16 / 9; }
.hzo-collage > img:not(:first-child) { aspect-ratio: 1 / 1; }


/* --- combobox/combobox.css --- */
/* combobox — @tokens: --combobox-max-height

   A searchable / autocomplete select: a text `__input` styled like a select
   (with a chevron) over a floating `__listbox` of `__option`s. CSS STRUCTURE
   ONLY — the search/filter behavior + `.is-open` toggle + `.is-active`
   roving-focus come later from JS, like the other interactive components. Its
   own `.hzo-combobox` root — it mirrors the dropdown panel idiom (surface /
   border / shadow-lg) without colliding with `.hzo-dropdown` or `.hzo-form`.

   Token-only: the panel's scroll cap has no semantic token in the scale, so
   it's a component token with a literal fallback — the same dial pattern
   icon-button.css uses. The chevron is a `background` SVG whose stroke is the
   muted-ink literal (matches the .hzo-form select chevron idiom). Everything
   else reads a semantic/L1 token. */
.hzo-combobox {
    position: relative;
}

/* Trigger — a text input painted like a select; chevron room on the right. */
.hzo-combobox__input {
    width: 100%;
    padding: var(--space-sm) var(--space-xl) var(--space-sm) var(--space-md);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-control);
    background: var(--surface) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>") no-repeat right var(--space-md) center / 16px 16px;
    color: var(--text-body);
    font-family: var(--font-body);
    font-size: var(--fs-200);
    line-height: var(--lh-normal);
    cursor: text;
    box-sizing: border-box;
    transition: border-color var(--transition-base) ease;
}

/* Floating panel — mirrors the .hzo-dropdown__menu surface idiom; hidden until
   JS adds `.is-open` to the root. */
.hzo-combobox__listbox {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: var(--z-500);
    margin-top: var(--space-2xs);
    max-height: var(--combobox-max-height, 260px);
    overflow: auto;
    list-style: none;
    padding: 0;
    background: var(--surface);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-panel);
    box-shadow: var(--shadow-lg);
    display: none;
}
.hzo-combobox.is-open .hzo-combobox__listbox { display: block; }

/* Option row — hover / keyboard-active / selected all share the alt-surface
   highlight. `.is-active` is the JS-driven roving-focus row; `[aria-selected]`
   is the committed choice. */
.hzo-combobox__option {
    padding: var(--space-sm) var(--space-md);
    color: var(--text-body);
    cursor: pointer;
}
.hzo-combobox__option:hover,
.hzo-combobox__option.is-active,
.hzo-combobox__option[aria-selected="true"] {
    background: var(--bg-alt);
    color: var(--text-heading);
}

/* A11y — required. Focus ring on the input; disabled state on the input.
   Reduced-motion is handled globally by components/_base/a11y.css. */
.hzo-combobox__input:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}
.hzo-combobox__input[disabled],
.hzo-combobox__input[aria-disabled="true"],
.hzo-combobox.is-disabled {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-combobox__input { transition: none; }
}


/* --- contact-hub/contact-hub.css --- */
/* ═══════════════════════════════════════════════════════════════════════════
   FLOATING CONTACT HUB (W-040) — fixed corner FAB + native-channel disclosure.

   A position:fixed FAB that expands into a stack/fan/radial of native contact
   links (tel:/sms:/wa.me/mailto:/maps). APG Disclosure, NOT a modal trap — it
   never scroll-locks the page. Layered by --hzo-contact-hub-z, a peer of
   --z-back-to-top (15): below every overlay / --z-toast (70) so cart toasts,
   tooltips and modal scrims render above it, and it is inerted by an overlay
   background lock rather than fighting it. All motion is transform/opacity with
   a --i stagger (no JS timers); killed under prefers-reduced-motion.

   Local component tokens (declared here, resolved from the shared ladder — never
   a bare z-index integer, never a hardcoded brand hex). Per-instance offsets +
   channel count arrive as inline data vars from the shortcode.
   ═══════════════════════════════════════════════════════════════════════════ */
.hzo-contact-hub {
    /* Layer peer of back-to-top (15), safely below --z-toast (70). */
    --hzo-contact-hub-z:        var(--z-back-to-top, 15);
    --hzo-contact-hub-offset-x: var(--space-lg);
    --hzo-contact-hub-offset-y: var(--space-lg);
    --hzo-contact-hub-fab:      56px;   /* toggle FAB (>=44 target)   */
    --hzo-contact-hub-target:   48px;   /* channel target (>=44)      */
    --hzo-contact-hub-gap:      var(--space-sm);
    --hzo-contact-hub-bg:       var(--brand);
    --hzo-contact-hub-fg:       var(--text-on-brand);
    --hzo-contact-hub-panel-bg: var(--surface, #fff);
    --hzo-contact-hub-panel-fg: var(--text-body);

    position: fixed;
    z-index: var(--hzo-contact-hub-z);
    display: flex;
    flex-direction: column-reverse;   /* toggle sits at the corner, panel above */
    align-items: flex-end;
    gap: var(--hzo-contact-hub-gap);
    /* Vertical safe-area padding so the FAB clears an iOS home indicator. */
    bottom: calc(var(--hzo-contact-hub-offset-y) + env(safe-area-inset-bottom, 0px));
    right: calc(var(--hzo-contact-hub-offset-x) + env(safe-area-inset-right, 0px));
}

/* ── Corners ──────────────────────────────────────────────────────────────── */
.hzo-contact-hub--bottom-left {
    right: auto;
    left: calc(var(--hzo-contact-hub-offset-x) + env(safe-area-inset-left, 0px));
    align-items: flex-start;
}
.hzo-contact-hub--top-right {
    bottom: auto;
    top: calc(var(--hzo-contact-hub-offset-y) + env(safe-area-inset-top, 0px));
    flex-direction: column;           /* panel opens downward */
}
.hzo-contact-hub--top-left {
    bottom: auto;
    right: auto;
    top: calc(var(--hzo-contact-hub-offset-y) + env(safe-area-inset-top, 0px));
    left: calc(var(--hzo-contact-hub-offset-x) + env(safe-area-inset-left, 0px));
    align-items: flex-start;
    flex-direction: column;
}

/* mobile-only: desktop keeps its header CTA. */
@media (min-width: 782px) {
    .hzo-contact-hub--mobile-only { display: none; }
}

/* ── Toggle FAB ───────────────────────────────────────────────────────────── */
.hzo-contact-hub__toggle {
    position: relative;
    flex: 0 0 auto;
    width: var(--hzo-contact-hub-fab);
    height: var(--hzo-contact-hub-fab);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: var(--border-1) solid transparent;   /* forced-colors visibility fallback */
    border-radius: var(--radius-pill);
    background: var(--hzo-contact-hub-bg);
    color: var(--hzo-contact-hub-fg);
    box-shadow: var(--shadow-lg);
    cursor: pointer;
    transition: transform var(--dur-2) var(--ease-out),
                box-shadow var(--dur-2) var(--ease-standard),
                background var(--dur-2) var(--ease-standard);
}
.hzo-contact-hub__toggle:hover { box-shadow: var(--shadow-xl); transform: translateY(-2px); }
.hzo-contact-hub__toggle:active { transform: translateY(0); }
.hzo-contact-hub__toggle:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: var(--border-2);
}
.hzo-contact-hub__toggle svg { width: 26px; height: 26px; display: block; }

/* Glyph swap: message/etc. when closed, X when open. */
.hzo-contact-hub__toggle-glyph {
    position: absolute;
    transition: opacity var(--dur-1) var(--ease-standard),
                transform var(--dur-2) var(--ease-out);
}
.hzo-contact-hub__toggle-glyph--close { opacity: 0; transform: rotate(-45deg); }
.hzo-contact-hub.is-open .hzo-contact-hub__toggle-glyph--open  { opacity: 0; transform: rotate(45deg); }
.hzo-contact-hub.is-open .hzo-contact-hub__toggle-glyph--close { opacity: 1; transform: rotate(0); }

/* Count / label bubble. */
.hzo-contact-hub__badge {
    position: absolute;
    top: -2px;
    right: -2px;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-pill);
    background: var(--accent);
    color: var(--text-on-accent);
    font-family: var(--font-body);
    font-size: var(--fs-50);
    font-weight: var(--fw-700);
    line-height: 1;
}

/* ── Panel (role=group) ───────────────────────────────────────────────────── */
.hzo-contact-hub__panel {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--hzo-contact-hub-gap);
}
.hzo-contact-hub--bottom-left .hzo-contact-hub__panel,
.hzo-contact-hub--top-left .hzo-contact-hub__panel { align-items: flex-start; }
.hzo-contact-hub__panel[hidden] { display: none; }

/* ── Channel links ────────────────────────────────────────────────────────── */
.hzo-contact-hub__channel {
    --hzo-contact-hub-channel-accent: var(--brand);
    min-height: var(--hzo-contact-hub-target);
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--hzo-contact-hub-panel-bg);
    color: var(--hzo-contact-hub-panel-fg);
    box-shadow: var(--shadow-md);
    text-decoration: none;
    font-family: var(--font-body);
    font-size: var(--fs-100);
    font-weight: var(--fw-600);
    white-space: nowrap;
    /* Reveal choreography: staggered by --i (set inline per channel). */
    opacity: 0;
    transform: translateY(var(--space-sm));
    transition: opacity var(--dur-2) var(--ease-standard),
                transform var(--dur-3) var(--ease-out),
                border-color var(--dur-1) var(--ease-standard),
                box-shadow var(--dur-1) var(--ease-standard);
    transition-delay: calc(var(--i, 0) * 40ms);
}
/* Left-corner / downward reveals travel from the opposite side. */
.hzo-contact-hub--top-right .hzo-contact-hub__channel,
.hzo-contact-hub--top-left .hzo-contact-hub__channel { transform: translateY(calc(var(--space-sm) * -1)); }

.hzo-contact-hub.is-open .hzo-contact-hub__channel { opacity: 1; transform: none; }

.hzo-contact-hub__channel:hover {
    border-color: var(--hzo-contact-hub-channel-accent);
    box-shadow: var(--shadow-lg);
}
.hzo-contact-hub__channel:active { box-shadow: var(--shadow-md); }
.hzo-contact-hub__channel:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: var(--border-2);
}
.hzo-contact-hub__channel[hidden] { display: none; }

.hzo-contact-hub__icon {
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
    color: var(--hzo-contact-hub-channel-accent);
}
.hzo-contact-hub__label { display: inline-block; }

/* Desktop tel: dead-path fallback — the selectable / copied number. */
.hzo-contact-hub__num {
    display: none;
    font-family: var(--font-body);
    font-size: var(--fs-75);
    font-weight: var(--fw-400);
    color: var(--text-muted);
    -webkit-user-select: all;
    user-select: all;
}
@media (hover: hover) and (pointer: fine) {
    .hzo-contact-hub__channel--call .hzo-contact-hub__num { display: inline-block; margin-inline-start: var(--space-sm); }
}

/* ── labels: hover / none — coarse pointer always shows text ──────────────── */
@media (hover: hover) and (pointer: fine) {
    .hzo-contact-hub--labels-hover .hzo-contact-hub__label {
        max-width: 0;
        overflow: hidden;
        opacity: 0;
        transition: max-width var(--dur-2) var(--ease-out), opacity var(--dur-2) var(--ease-standard);
    }
    .hzo-contact-hub--labels-hover .hzo-contact-hub__channel:hover .hzo-contact-hub__label,
    .hzo-contact-hub--labels-hover .hzo-contact-hub__channel:focus-visible .hzo-contact-hub__label {
        max-width: 20ch;
        opacity: 1;
    }
}
/* labels=none — icon-only; the <a> keeps its aria-label (accessible name). */
.hzo-contact-hub--labels-none .hzo-contact-hub__channel {
    width: var(--hzo-contact-hub-target);
    padding: 0;
    justify-content: center;
}
.hzo-contact-hub--labels-none .hzo-contact-hub__label,
.hzo-contact-hub--labels-none .hzo-contact-hub__num { display: none; }

/* ── expand: fan / radial (progressive; base is a vertical stack) ──────────── */
.hzo-contact-hub--fan .hzo-contact-hub__channel { transform-origin: bottom right; }
.hzo-contact-hub--fan.is-open .hzo-contact-hub__channel { transform: none; }
.hzo-contact-hub--radial .hzo-contact-hub__panel { align-items: flex-end; }

/* ── after-scroll: hidden until the JS drops the flag ─────────────────────── */
.hzo-contact-hub--hidden-until-scroll { opacity: 0; visibility: hidden; pointer-events: none; }
.hzo-contact-hub--hidden-until-scroll.is-revealed {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity var(--dur-3) var(--ease-standard);
}

/* ── Attention pulse (one-shot; JS adds .is-pulsing, removes on first open) ── */
@keyframes hzo-contact-hub-pulse {
    0%   { box-shadow: var(--shadow-lg), 0 0 0 0 color-mix(in srgb, var(--brand) 45%, transparent); }
    70%  { box-shadow: var(--shadow-lg), 0 0 0 16px color-mix(in srgb, var(--brand) 0%, transparent); }
    100% { box-shadow: var(--shadow-lg), 0 0 0 0 color-mix(in srgb, var(--brand) 0%, transparent); }
}
.hzo-contact-hub.is-pulsing .hzo-contact-hub__toggle {
    animation: hzo-contact-hub-pulse 1.8s var(--ease-out) 3;
}

/* ── Environment ──────────────────────────────────────────────────────────── */
@media print {
    .hzo-contact-hub { display: none !important; }
}

@media (forced-colors: active) {
    .hzo-contact-hub__toggle,
    .hzo-contact-hub__channel { border-color: ButtonText; }
    .hzo-contact-hub__toggle:focus-visible,
    .hzo-contact-hub__channel:focus-visible { outline-color: Highlight; }
    .hzo-contact-hub__icon { color: ButtonText; }
}

@media (prefers-reduced-motion: reduce) {
    .hzo-contact-hub__toggle,
    .hzo-contact-hub__toggle-glyph,
    .hzo-contact-hub__channel,
    .hzo-contact-hub__label { transition: none; }
    .hzo-contact-hub__channel { transition-delay: 0s; }
    .hzo-contact-hub.is-pulsing .hzo-contact-hub__toggle { animation: none; }
    /* Never leave revealed content at opacity:0 (G13/G60): open state is instant. */
    .hzo-contact-hub.is-open .hzo-contact-hub__channel { opacity: 1; transform: none; }
}


/* --- contact-split/contact-split.css --- */
/* contact-split — @tokens: --contact-split-gap, --contact-split-info-gap

   A two-column contact SECTION: business info / NAP (name-address-phone, hours,
   social) on one side, a form or a map on the other. Full-bleed section that
   OWNS its vertical rhythm (padding via .hzo-section) and holds a .hzo-container.
   A composition, not a control — it only LAYS OUT existing components (reuse
   .hzo-definition-list for the NAP, .hzo-btn in the form, .hzo-map for the map
   side); it never restyles them, so it carries no focus/disabled idioms itself —
   the inner form fields and .hzo-map bring their own a11y.

   Token-only: the two-column gap + the info-stack rhythm have no single semantic
   role that fits, so they are component tokens with a space-scale literal fallback
   (the icon-button.css dial pattern). Everything else reads an L1/L2 token. */

/* The 2-col grid. Info | form on desktop; stacks to one column below the
   breakpoint (max-width query — the QA-ladder direction the standard prefers). */
.hzo-contact-split__grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--contact-split-gap, var(--space-3xl));
    align-items: start;
}
@media (max-width: 768px) {
    .hzo-contact-split__grid {
        grid-template-columns: 1fr;
        gap: var(--space-xl);
    }
}

/* Info column — heading + NAP + hours + social stacked with a shared rhythm.
   Holds plain markup or a reused .hzo-definition-list; adds no type styling of
   its own (the heading / eyebrow / definition-list classes carry that). */
.hzo-contact-split__info {
    display: flex;
    flex-direction: column;
    gap: var(--contact-split-info-gap, var(--space-lg));
}

/* Form / map column — a bare layout slot. The inner form fields and .hzo-map
   style themselves; this only holds them. First/last child margins collapse so
   the slot's contents align flush with the info column top. */
.hzo-contact-split__form > :first-child { margin-top: 0; }
.hzo-contact-split__form > :last-child { margin-bottom: 0; }

/* Modifier — swap the column order so the form/map leads on desktop. Only flips
   above the stack breakpoint; on mobile the source order (info first) is kept. */
@media (min-width: 769px) {
    .hzo-contact-split--form-first .hzo-contact-split__info { order: 2; }
    .hzo-contact-split--form-first .hzo-contact-split__form { order: 1; }
}


/* --- credentials/credentials.css --- */
/* ─── License & Insurance Verification Card (.hzo-credentials) ──────────────
   W-060 — a structured credentials card: one row per license / certification /
   insurance / bond, each with a server-computed status pill (Active / Expires
   soon / Expired), an optional masked number + click-to-copy, and a
   "Verify on {board}" deep link. Markup is emitted by credentials.php
   ([hzo_credentials] > [hzo_credential] rows); this sheet styles ONLY the
   classes that handler outputs.

   The card is fully functional WITHOUT JS — status pills, verify links and the
   full (sr-only) number are all server-rendered. credentials.js only adds the
   copy + mask-reveal convenience; the reduced-motion / forced-colors passes at
   the foot keep the card readable when either is active.

   Tokens only: --surface / --border / --text-* / state --*-bg + --*-ink (pills),
   --brand-text / --accent-text (ink + icon tint), --space-* (rhythm),
   --radius-* (panel + pill + control), --shadow-sm (rest elevation), --fs-* /
   --fw-* / --tracking-* (type), --dur-* / --ease-* (transitions). No hardcoded
   hex, no bare z-index (the card claims none). */

.hzo-credentials-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

/* Optional card heading (a real <h2>, styled by base.css) — only owns rhythm. */
.hzo-credentials__heading {
    margin: 0;
}

/* ── Layouts ────────────────────────────────────────────────────────────────
   grid  (default) — responsive card grid, one credential per card.
   list  — a single column of the same cards, full width.
   table — compact divided rows inside one bordered panel (no per-row chrome). */
.hzo-credentials {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
    gap: var(--space-lg);
    margin: 0;
    padding: 0;
    list-style: none;
}
.hzo-credentials--list {
    grid-template-columns: 1fr;
}
.hzo-credentials--table {
    display: block;
    border: var(--border-1) solid var(--surface-border);
    border-radius: var(--radius-panel);
    overflow: hidden;
}

/* ── Item (the credential card) ─────────────────────────────────────────────
   container-type lets the label/value rows collapse to a stack on a narrow
   card (no media query, no JS). */
.hzo-credentials__item {
    container-type: inline-size;
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    padding: var(--space-lg);
    background: var(--surface);
    border: var(--border-1) solid var(--surface-border);
    border-radius: var(--radius-panel);
    box-shadow: var(--shadow-sm);
}
/* Table layout strips the card chrome down to divided rows. */
.hzo-credentials--table .hzo-credentials__item {
    background: transparent;
    border: 0;
    border-bottom: var(--border-1) solid var(--border);
    border-radius: 0;
    box-shadow: none;
    padding: var(--space-md) var(--space-lg);
}
.hzo-credentials--table .hzo-credentials__item:last-child {
    border-bottom: 0;
}

/* ── Head: type icon + label + status pill ──────────────────────────────── */
.hzo-credentials__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
}
.hzo-credentials__type {
    margin-inline-end: auto;          /* push the status pill to the far edge */
    color: var(--text-heading);
    font-weight: var(--fw-600);
}

/* Shared icon base (every SVG the handler emits carries this class). */
.hzo-credentials__icon {
    width: 1.15em;
    height: 1.15em;
    flex-shrink: 0;
}
/* The type glyph (direct child of head) reads at the trust-icon accent tint,
   a step larger than the inline icons. */
.hzo-credentials__head > .hzo-credentials__icon {
    width: 1.5em;
    height: 1.5em;
    color: var(--accent-text);
}

/* ── Status pill (server-computed; word + glyph carry meaning, never colour
   alone — the icon is decorative reinforcement) ─────────────────────────── */
.hzo-credentials__status {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    padding: 4px 10px;
    border-radius: var(--radius-pill);
    font-size: var(--fs-75);
    font-weight: var(--fw-600);
    line-height: var(--lh-none);
    /* neutral fallback; the data-status variants below tint per state */
    background: var(--bg-alt);
    color: var(--text-body);
}
.hzo-credentials__status .hzo-credentials__icon {
    width: 1em;
    height: 1em;
}
.hzo-credentials__status[data-status="active"] {
    background: var(--success-bg);
    color: var(--success-ink);
}
.hzo-credentials__status[data-status="expiring"] {
    background: var(--warning-bg);
    color: var(--warning-ink);
}
.hzo-credentials__status[data-status="expired"] {
    background: var(--error-bg);
    color: var(--error-ink);
}

/* ── Meta (definition list: number / issued by / expires) ───────────────── */
.hzo-credentials__meta {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    margin: 0;
}
.hzo-credentials__field {
    display: grid;
    grid-template-columns: 5.5rem 1fr;
    gap: var(--space-sm);
    align-items: baseline;
}
.hzo-credentials__label {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--fs-75);
    font-weight: var(--fw-600);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
}
.hzo-credentials__meta dd {
    margin: 0;
    color: var(--text-body);
}
.hzo-credentials__meta time {
    font-variant-numeric: tabular-nums;
}

/* Number row: value + optional mask toggle + optional copy, inline. */
.hzo-credentials__number {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
}
.hzo-credentials__num {
    color: var(--text-heading);
    font-weight: var(--fw-600);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
}

/* Clipped full-number span — always in the DOM for AT + print (PROGRESSIVE
   ENHANCEMENT: the widget is complete without JS). */
.hzo-credentials__vh {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ── Inline action buttons (mask toggle + copy) ─────────────────────────── */
.hzo-credentials__mask-toggle,
.hzo-credentials__copy {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    min-height: 1.75em;
    padding: var(--space-2xs);
    border: 0;
    border-radius: var(--radius-control);
    background: transparent;
    color: var(--accent-text);
    font: inherit;
    font-size: var(--fs-75);
    line-height: var(--lh-none);
    cursor: pointer;
    transition: color var(--dur-1) var(--ease-standard),
                background-color var(--dur-1) var(--ease-standard);
}
.hzo-credentials__mask-toggle:hover,
.hzo-credentials__copy:hover {
    background: var(--bg-alt);
    color: var(--brand-text);
}
.hzo-credentials__mask-toggle:focus-visible,
.hzo-credentials__copy:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: 2px;
}
/* Mask toggle carries BOTH glyphs (eye = masked, eye-off = revealed); CSS shows
   exactly one per aria-pressed state, so no JS icon swap is needed and the button
   is correct even mid-enhancement. */
.hzo-credentials__mask-toggle .hzo-credentials__icon:last-of-type,
.hzo-credentials__mask-toggle[aria-pressed="true"] .hzo-credentials__icon:first-of-type {
    display: none;
}
.hzo-credentials__mask-toggle[aria-pressed="true"] .hzo-credentials__icon:last-of-type {
    display: inline;
}

/* Brief "Copied" acknowledgement (credentials.js swaps the text + this class). */
.hzo-credentials__copy.is-copied {
    color: var(--success-ink);
}
/* Copy fell back to manual (clipboard blocked / insecure context): the number
   text is selected + the label reads "Press Ctrl+C" — a still-working recovery,
   never a dead end (never colour-only; the relabel carries the meaning). */
.hzo-credentials__copy.is-copy-failed {
    color: var(--warning-ink);
}

/* ── Verify deep link — pinned to the card foot so links align across cards
   of differing height ───────────────────────────────────────────────────── */
.hzo-credentials__verify {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    align-self: flex-start;
    margin-top: auto;
    color: var(--brand-text);
    font-size: var(--fs-100);
    font-weight: var(--fw-600);
    text-decoration: none;
    transition: color var(--dur-1) var(--ease-standard);
}
.hzo-credentials__verify:hover {
    color: var(--brand-hover-on-light);
    text-decoration: underline;
}
.hzo-credentials__verify:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: 2px;
    border-radius: var(--radius-control);
}

/* ── Narrow card: stack each label above its value (no JS) ───────────────── */
@container (max-width: 22rem) {
    .hzo-credentials__field {
        grid-template-columns: 1fr;
        gap: var(--space-2xs);
    }
}

/* ── Reduced motion — the card carries no essential motion; kill the hover /
   state transitions so nothing animates (content stays fully visible). ──── */
@media (prefers-reduced-motion: reduce) {
    .hzo-credentials__mask-toggle,
    .hzo-credentials__copy,
    .hzo-credentials__verify {
        transition: none;
    }
}

/* ── Forced colors — state is carried by the pill's WORD, not colour alone, so
   High Contrast stays correct. Restore a visible edge on pills + action
   buttons and underline the verify affordance. ─────────────────────────── */
@media (forced-colors: active) {
    .hzo-credentials__status {
        border: var(--border-1) solid currentColor;
    }
    .hzo-credentials__mask-toggle,
    .hzo-credentials__copy {
        border: var(--border-1) solid currentColor;
    }
    .hzo-credentials__verify {
        text-decoration: underline;
    }
}

/* ── Print / PDF (G13) — home-service customers routinely print or PDF an
   estimate / service page, so the credentials must survive it: force-reveal the
   card even if it was wrapped in scroll-reveal (opacity:0 below the fold), print
   the FULL number (drop the masked span, un-clip the sr-only full number), spell
   out the verify URL as visible text, and hide the copy / mask chrome (useless on
   paper). ─────────────────────────────────────────────────────────────────── */
@media print {
    .hzo-credentials.hzo-reveal,
    .hzo-credentials.hzo-reveal--init,
    .hzo-credentials .hzo-reveal {
        opacity: 1 !important;
        transform: none !important;
    }
    .hzo-credentials__item {
        break-inside: avoid;
        box-shadow: none;
    }
    /* Show the full number: the masked visible span is aria-hidden decoration on
       paper; the clipped full-number span becomes the printed value. */
    .hzo-credentials__num[data-hzo-mask],
    .hzo-credentials__mask-toggle,
    .hzo-credentials__copy {
        display: none !important;
    }
    .hzo-credentials__vh {
        position: static;
        width: auto;
        height: auto;
        margin: 0;
        overflow: visible;
        clip: auto;
        white-space: normal;
        color: var(--text-heading);
        font-weight: var(--fw-600);
    }
    /* Print the destination so a paper reader can still reach the board lookup. */
    .hzo-credentials__verify[href]::after {
        content: " (" attr(href) ")";
        font-weight: var(--fw-400);
        word-break: break-all;
    }
}


/* --- cta-band/cta-band-split.css --- */
/* ─── Closing CTA Band — SPLIT layout ─────────────────────────────────────
   Copy on the left, actions/capture on the right; stacks on narrow viewports.
   Mirrors via logical properties for RTL. @imported by cta-band.css. */
.hzo-cta-band--split .hzo-cta-band__inner {
    text-align: start;
    align-items: stretch;
}
.hzo-cta-band--split .hzo-cta-band__actions { justify-content: flex-start; }

@media (min-width: 768px) {
    .hzo-cta-band--split .hzo-cta-band__inner {
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-2xl);
    }
    .hzo-cta-band--split .hzo-cta-band__copy { flex: 1 1 auto; max-width: 40rem; }
    .hzo-cta-band--split .hzo-cta-band__actions,
    .hzo-cta-band--split .hzo-cta-band__capture {
        flex: 0 0 auto;
        flex-direction: column;
        align-items: stretch;
    }
    .hzo-cta-band--split .hzo-cta-band__capture { min-width: 22rem; }
}


/* --- cta-band/cta-band-contained.css --- */
/* ─── Closing CTA Band — CONTAINED layout ─────────────────────────────────
   An inset card floating over a full-bleed background (image/gradient/solid).
   The band keeps its full-width media; the content sits in a padded, rounded
   surface panel with breathing room around it. @imported by cta-band.css. */
.hzo-cta-band--contained { padding-block: var(--space-2xl); }
.hzo-cta-band--contained .hzo-cta-band__inner {
    max-width: 60rem;
    padding: var(--space-2xl) var(--space-xl);
    border-radius: var(--radius-card);
    background: color-mix(in srgb, var(--brand-deep) 78%, transparent);
    box-shadow: var(--shadow-lg);
    text-align: center;
    align-items: center;
}
.hzo-cta-band--contained .hzo-cta-band__copy { max-width: 44rem; }
.hzo-cta-band--contained .hzo-cta-band__actions { justify-content: center; }
.hzo-cta-band--contained .hzo-cta-band__subhead,
.hzo-cta-band--contained .hzo-cta-band__capture { margin-inline: auto; }

/* A light-text-on-dark card by default; the dark-text scheme flips the panel to
   the surface token so authored dark copy stays readable. */
.hzo-cta-band--contained.hzo-cta-band--text-dark .hzo-cta-band__inner {
    background: var(--surface, #fff);
    border: 1px solid var(--surface-border, var(--border));
}

@media (forced-colors: active) {
    .hzo-cta-band--contained .hzo-cta-band__inner {
        background: Canvas;
        border: 1px solid CanvasText;
    }
}


/* --- cta-band/cta-band.css --- */
/* ═════════════════════════════════════════════════════════════════════════
   CLOSING CTA BAND (W-091) — component styles

   The full-width bottom-of-page closer between the thin promo banner and the
   floating sticky-cta. Layouts (centered / split / contained), backgrounds
   (solid / gradient / image / video), and the contrast-safe overlay scrim are
   ALL pure CSS; cta-band.js is progressive enhancement only (video pause,
   async capture, desktop click-to-copy, mobile sticky handoff).

   STACKING (spec §8.5): the root sets `isolation:isolate` to open its own
   stacking context, then layers video -> scrim -> content -> pause-toggle by
   DOM ORDER using positioned elements only — NO z-index token is spent (and no
   bare z-index integer either). Every layer is position:absolute except the
   content wrapper (position:relative); painted last-in-DOM = topmost.

   Variant sheets are @imported so the loader (which enqueues only cta-band.css)
   pulls them on the same request.
   ═════════════════════════════════════════════════════════════════════════ */
/* ── ONE COMPOSITION (Wave S) ────────────────────────────────────────────
   This component ships exactly ONE composition: a full-width closing band - heading plus actions, centred, split or contained.

   That is a feature. One composition is why it is responsive, accessible and
   identical on two hundred sites, and why a client build can place it without
   re-deciding anything. It is also a hard limit: the component cannot express
   a SECOND composition, and making it try — nesting it, stacking modifiers on
   it, overriding its grid from a client sheet — is how a page ends up fighting
   its own components and still looking generic.

   FOR AN AMPLIFY OR ELEVATE SECTION, DO NOT BEND THIS COMPONENT. Take one of:
     • RAW mode — author the section's markup directly, or
     • hand-author to the class contract on the spatial utilities:
       .hzo-grid + .hzo-span-* / .hzo-rowspan-* / --hzo-grid-template,
       .hzo-bleed, .hzo-pull-* / .hzo-z-*, .hzo-section--edge-*, .hzo-measure-*.
   Both are the STANDARD route for a high-intensity section, not an escape
   hatch, and the composition plan is what says which sections those are.

   PLUMBING AND PROOF COMPONENTS ARE UNAFFECTED. Every JS widget and every
   proof/data component (reviews, credentials, ratings, schema) stays
   mandatory — never hand-roll those. This note is about LAYOUT only.
   ────────────────────────────────────────────────────────────────────────── */

/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */

.hzo-cta-band {
    position: relative;
    isolation: isolate;                 /* own stacking context — see header */
    overflow: hidden;
    padding: var(--space-2xl) var(--space-lg);
    background: var(--hzo-cta-bg, var(--brand));
    color: var(--text-on-brand, #fff);
}

/* ── Background variants ─────────────────────────────────────────────────── */
/* --gradient-brand-sweep IS the literal that used to sit here (tokens.css) — one
   definition of the fleet's diagonal brand wash, shared with .hzo-mega-cta. */
.hzo-cta-band--bg-gradient { background: var(--hzo-cta-gradient, var(--gradient-brand-sweep, linear-gradient(135deg, var(--brand), var(--brand-deep)))); }
.hzo-cta-band--bg-image,
.hzo-cta-band--bg-video {
    background-color: var(--brand-deep);            /* fallback under the media */
    background-image: var(--hzo-cta-image, none);   /* image url / video poster */
    background-size: cover;
    background-position: center;
}

/* Decorative background <video> — muted/loop/aria-hidden, painted first. */
.hzo-cta-band__video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* AA-floor overlay scrim (color-mix). Strength/tint come from the handler as
   --hzo-cta-overlay (%) + --hzo-cta-overlay-color; only rendered when > 0. */
.hzo-cta-band__scrim {
    position: absolute;
    inset: 0;
    background: color-mix(in srgb, var(--hzo-cta-overlay-color, var(--brand-deep)) var(--hzo-cta-overlay, 55%), transparent);
    pointer-events: none;
}

/* Content wrapper — positioned (relative) so it paints above the absolute
   video + scrim purely by DOM order. */
.hzo-cta-band__inner {
    position: relative;
    max-width: var(--container-max-width, 1350px);
    margin-inline: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
}

/* ── Centered (default) ──────────────────────────────────────────────────── */
.hzo-cta-band--centered .hzo-cta-band__inner { text-align: center; align-items: center; }
.hzo-cta-band--centered .hzo-cta-band__copy { max-width: 46rem; }
.hzo-cta-band--centered .hzo-cta-band__actions { justify-content: center; }

/* ── Copy column ─────────────────────────────────────────────────────────── */
.hzo-cta-band__copy {
    display: flex;
    flex-direction: column;
    gap: var(--space-smd, 12px);
}
.hzo-cta-band__eyebrow {
    margin: 0;
    font-size: var(--eyebrow-size, 15px);
    font-weight: var(--fw-semibold, 600);
    letter-spacing: var(--tracking-wide, 0.05em);
    text-transform: uppercase;
    opacity: var(--opacity-muted, 0.85);
}
.hzo-cta-band__heading {
    margin: 0;
    text-wrap: balance;
    color: inherit;
}
.hzo-cta-band__subhead {
    margin: 0;
    max-width: var(--prose-max-width, 65ch);
    font-size: 1.125rem;
    line-height: var(--lh-relaxed, 1.7);
    opacity: 0.92;
}
.hzo-cta-band--centered .hzo-cta-band__subhead { margin-inline: auto; }

/* ── Trust line + star rating ────────────────────────────────────────────── */
.hzo-cta-band__trust {
    margin: 0;
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm);
    font-size: 0.95rem;
    opacity: 0.9;
}
.hzo-cta-band__rating {
    position: relative;
    display: inline-block;
    font-size: 1.05rem;
    line-height: 1;
    white-space: nowrap;
}
.hzo-cta-band__stars-base { color: color-mix(in srgb, currentColor 35%, transparent); }
.hzo-cta-band__stars-fill {
    position: absolute;
    inset: 0;
    overflow: hidden;
    width: var(--hzo-cta-stars, 100%);
    color: #f6c945;                     /* warm review-star gold (not a brand hue) */
}

/* ── Actions ─────────────────────────────────────────────────────────────── */
.hzo-cta-band__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-md);
}
.hzo-cta-band__action { display: inline-flex; align-items: center; gap: var(--space-sm); }
.hzo-cta-band__icon { width: 20px; height: 20px; flex-shrink: 0; }

/* ── Inline capture (reuses .hzo-input-group) ────────────────────────────── */
.hzo-cta-band__capture { width: 100%; max-width: 32rem; }
.hzo-cta-band--centered .hzo-cta-band__capture { margin-inline: auto; }
.hzo-cta-band__capture .hzo-input-group { max-width: none; }
/* In-flight submit-lock: dimmed but the spinner/text still read (forced-colors
   safe — never the ONLY busy signal). */
.hzo-cta-band__submit[aria-busy="true"],
.hzo-cta-band__submit.is-loading { opacity: var(--opacity-muted, 0.65); }
/* Invalid capture field — a VISIBLE border + ring, never suppressed (G49). */
.hzo-cta-band__capture input[aria-invalid="true"] {
    border-color: var(--error, #dc2626);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--error, #dc2626) 30%, transparent);
}
.hzo-cta-band__capture-status {
    margin: var(--space-sm) 0 0;
    font-size: 0.9rem;
}
.hzo-cta-band__capture-status.is-error { color: color-mix(in srgb, #fff 88%, var(--error)); }
.hzo-cta-band--text-dark .hzo-cta-band__capture-status.is-error { color: var(--error-text, #991b1b); }
/* Confirmation swap fragment (JS replaces the input-group with this). */
.hzo-cta-band__confirm {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    font-size: 1.05rem;
    font-weight: var(--fw-semibold, 600);
}

/* ── Text scheme ─────────────────────────────────────────────────────────── */
.hzo-cta-band--text-light { color: var(--heading-color-on-dark, #fff); }
.hzo-cta-band--text-light .hzo-cta-band__heading,
.hzo-cta-band--text-light .hzo-cta-band__eyebrow { color: var(--heading-color-on-dark, #fff); }
.hzo-cta-band--text-dark { color: var(--text-body); }
.hzo-cta-band--text-dark .hzo-cta-band__heading,
.hzo-cta-band--text-dark .hzo-cta-band__eyebrow { color: var(--text-heading); }
.hzo-cta-band--text-dark .hzo-cta-band__eyebrow { color: var(--brand-text); }

/* ── Video pause/play control — visible at rest (WCAG 2.2.2), painted last ── */
.hzo-cta-band__video-toggle {
    position: absolute;
    right: var(--space-lg);
    bottom: var(--space-lg);
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 10px 18px;
    border: 1px solid var(--surface-border, transparent);
    border-radius: var(--radius-pill);
    background: var(--surface, #fff);
    color: var(--on-surface, var(--text-heading));
    font-family: var(--font-body);
    font-size: 14px;
    font-weight: var(--fw-semibold, 600);
    box-shadow: var(--shadow-md);
    cursor: pointer;
    transition: background var(--dur-2) var(--ease-standard);
}
.hzo-cta-band__video-toggle:hover { background: var(--bg-alt); }
.hzo-cta-band__video-toggle:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* ── Visually-hidden capture label (associated <label>, never a placeholder) ─ */
.hzo-cta-band__sr-only {
    position: absolute !important;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ── Focus ring floor (real outline — forced-colors + box-shadow-unreliable) ─ */
.hzo-cta-band .hzo-btn:focus-visible,
.hzo-cta-band__capture input:focus-visible { outline-offset: 2px; }

/* ── Environment ─────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
    /* Ambient gradient/video motion is class/JS-gated; kill control transitions
       and any reveal travel here (content must never stay opacity:0). */
    .hzo-cta-band__video-toggle { transition: none; }
    .hzo-cta-band.hzo-reveal { opacity: 1 !important; transform: none !important; }
}

@media print {
    /* Force the band visible if .hzo-reveal left it hidden (G13); drop the
       decorative chrome; keep the copy + actions legible on paper. */
    .hzo-cta-band { background: none !important; color: #000 !important; }
    .hzo-cta-band.hzo-reveal { opacity: 1 !important; transform: none !important; }
    .hzo-cta-band__video,
    .hzo-cta-band__scrim,
    .hzo-cta-band__video-toggle { display: none !important; }
    .hzo-cta-band__heading,
    .hzo-cta-band__eyebrow { color: #000 !important; }
    /* Expose the destination of each action so a printed page keeps the number/URL. */
    .hzo-cta-band__action[href]::after {
        content: " (" attr(href) ")";
        font-weight: 400;
        font-size: 0.85em;
    }
}

@media (forced-colors: active) {
    /* The color-mix scrim is dropped by the OS; don't rely on it for contrast.
       Give text a real system pairing and the toggle a real border. */
    .hzo-cta-band { background: Canvas; color: CanvasText; }
    .hzo-cta-band__scrim { display: none; }
    .hzo-cta-band__heading,
    .hzo-cta-band__eyebrow,
    .hzo-cta-band--text-light,
    .hzo-cta-band--text-light .hzo-cta-band__heading,
    .hzo-cta-band--text-light .hzo-cta-band__eyebrow { color: CanvasText; }
    .hzo-cta-band__video-toggle { border: 1px solid CanvasText; }
}


/* --- definition-list/definition-list.css --- */
/* definition-list — @tokens: --definition-list-gap, --definition-list-inline-gap

   A styled semantic <dl>. Structural primitive (no interaction) — term/description
   pairs for glossaries, meta panels, spec summaries. The element selectors (dt/dd)
   are scoped UNDER the root class per the authoring standard's semantic-element
   rule; they borrow no other block's classes and belong to this block only.

   Token-only: the pair rhythm + the inline column gap have no single semantic role
   that fits, so they are component tokens with a space-scale literal fallback (the
   icon-button.css dial pattern). Everything else reads an L1/L2 token directly.
   Static/structural, so no focus/disabled a11y idioms — nothing here is focusable. */
.hzo-definition-list {
    margin: 0;
    font-size: var(--fs-200);
    line-height: var(--lh-normal);
    color: var(--text-body);
}

/* Term — heading ink, semibold, one step below body for a label feel. */
.hzo-definition-list dt {
    font-weight: var(--fw-600);
    color: var(--text-heading);
    font-size: var(--fs-100);
}

/* Description — body ink; reset the UA left indent; carry the pair rhythm below. */
.hzo-definition-list dd {
    margin: 0 0 var(--definition-list-gap, var(--space-md));
    color: var(--text-body);
}
.hzo-definition-list dd:last-child {
    margin-bottom: 0;
}

/* Inline — dt/dd side-by-side on a two-column grid (label auto, value fills).
   The row gap keeps the vertical rhythm; the column gap separates term from
   description. */
.hzo-definition-list--inline {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-xs) var(--definition-list-inline-gap, var(--space-md));
    align-items: baseline;
}
.hzo-definition-list--inline dd {
    margin: 0;
}


/* --- desktop-nav/desktop-ctas.css --- */
/* ─── Desktop CTAs (right side) ─────────────────────────────────── */
.hzo-site-nav__ctas {
    display: none;
    align-items: center;
    gap: var(--space-sm);
    flex-shrink: 0;
}
@media (min-width: 1250px) { .hzo-site-nav__ctas { display: flex; } }


/* --- desktop-nav/header-condense.css --- */
/* ─── Header condense on scroll ────────────────────────────────────────
   main.js (scroll-chrome IIFE) toggles .is-scrolled on .hzo-site-header once
   scrollY passes 24px (hysteresis: removed again below 8px so the boundary
   never flickers). Condensed: the inner bar's min-height multiplies by
   --header-condensed-scale (default 0.85 ≈ 72→61 / 80→68px), the logo scales
   down via transform (no layout shift), and the shadow steps up one notch —
   the fallback chain keeps any per-client --nav-header-shadow in play.
   Sticky positioning, the mega-menu anchor (top:100% inside the header) and
   the mobile header all follow the header's live height automatically.
   The transition rules below are inert until .is-scrolled toggles, so
   resting rendering is unchanged. Reduced motion → instant swap. */
.hzo-site-header {
    transition: box-shadow var(--duration-base) var(--ease-standard);
}
.hzo-site-header .hzo-site-header__inner {
    transition: min-height var(--duration-base) var(--ease-standard);
}
.hzo-site-header .hzo-site-logo img {
    transform-origin: left center;
    transition: transform var(--duration-base) var(--ease-standard);
}
.hzo-site-header.is-scrolled .hzo-site-header__inner {
    min-height: calc(var(--header-height-mobile, 72px) * var(--header-condensed-scale, 0.85));
}
.hzo-site-header.is-scrolled .hzo-site-logo img {
    transform: scale(var(--header-logo-condensed-scale, 0.85));
}
.hzo-site-header.is-scrolled {
    box-shadow: var(--nav-header-shadow-scrolled, var(--nav-header-shadow, 0 2px 10px rgba(0, 0, 0, 0.10)));
}
@media (min-width: 1250px) {
    .hzo-site-header.is-scrolled .hzo-site-header__inner {
        min-height: calc(var(--header-height-desktop, 80px) * var(--header-condensed-scale, 0.85));
    }
}
@media (prefers-reduced-motion: reduce) {
    .hzo-site-header,
    .hzo-site-header .hzo-site-header__inner,
    .hzo-site-header .hzo-site-logo img { transition: none; }
}


/* --- desktop-nav/desktop-nav.css --- */
/* Aggregator sheet — the component loader (includes/component-loader.php)
   enqueues ONLY <name>.css per folder, so desktop-nav's sibling variant sheets
   are @imported here to ride the same front-end request. This is their ONLY
   front-end load path: header-condense.css + desktop-ctas.css are named
   non-<name>.css and are referenced nowhere else, so without these @imports the
   header-condense chrome and the desktop CTA rail under-load fleet-wide.
   (Same fix pattern as footer.css @importing footer-typography + footer-chrome,
   and video.css @importing lite-video + video-background.) @imports must precede
   all style rules — keep them first. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */

/* ─── Desktop nav menu (UL/LI structure) ────────────────────────── */
.hzo-site-nav {
    display: none;
    list-style: none;
    margin: 0;
    padding: 0;
    align-items: stretch;
    align-self: stretch;
    gap: 0;
    flex: 1;
    justify-content: center;
}
@media (min-width: 1250px) { .hzo-site-nav { display: flex; } }

/* display:flex + align-items:center keeps the link text vertically centered while
   the item itself spans the header's full height (align-items:stretch on the nav +
   align-self:stretch on the nav column). That makes the mega-menu's `top:100%`
   anchor to the header's BOTTOM edge instead of the middle of the header, and it
   auto-follows the condense-on-scroll height change. */
.hzo-nav-item { position: relative; display: flex; align-items: center; }

/* !important on color: fights a:visited from per-client main.css.
   Matching :visited rule at bottom-of-file preserves the color. */
.hzo-site-nav__link {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 14px 13px;
    font-family: var(--font-body);
    font-size: 0.9375rem;
    font-weight: var(--fw-600, 600);
    color: var(--nav-link) !important;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    transition: color 200ms ease;
}
/* Non-clickable dropdown parents: <span> triggers have no href, so no pointer cursor */
span.hzo-site-nav__link { cursor: default; }

.hzo-site-nav__link:hover,
.hzo-nav-item:hover > .hzo-site-nav__link,
.hzo-nav-item.is-open > .hzo-site-nav__link { color: var(--nav-link-hover) !important; }
a.hzo-site-nav__link:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }

/* G37: current page — brand color, heavier weight, and a short underline bar so
   "where am I" reads at a glance (H1 visibility of status). */
.hzo-site-nav__link.is-active,
.hzo-site-nav__link[aria-current="page"] {
    position: relative;
    color: var(--nav-link-active, var(--brand-text, var(--brand))) !important;
    font-weight: var(--fw-bold, 700);
}
.hzo-site-nav__link.is-active::after,
.hzo-site-nav__link[aria-current="page"]::after {
    content: "";
    position: absolute;
    left: 13px; right: 13px; bottom: 8px;
    height: 2px;
    background: currentColor;
    border-radius: 2px;
}
.hzo-mega-menu__link.is-active { color: var(--brand-text, var(--brand)) !important; font-weight: var(--fw-bold, 700); }

.hzo-nav-chevron {
    flex-shrink: 0;
    opacity: 0.45;
    transition: transform 200ms ease, opacity 200ms ease;
}
.hzo-nav-item:hover > .hzo-site-nav__link .hzo-nav-chevron,
.hzo-nav-item.is-open > .hzo-site-nav__link .hzo-nav-chevron {
    transform: rotate(180deg);
    opacity: 0.8;
}

/* Reduced motion: kill the nav-link colour + chevron rotate/fade transitions
   (the rotated/coloured end-states still apply — only the animation is dropped).
   header-condense.css ships its own reduced-motion block for the header shell. */
@media (prefers-reduced-motion: reduce) {
    .hzo-site-nav__link,
    .hzo-nav-chevron { transition: none; }
}


/* --- divider/divider.css --- */
/* ─── Divider / rule ──────────────────────────────────────────────────
   Horizontal separator. Plain <hr class="hzo-divider"> or a labeled variant:
   <div class="hzo-divider hzo-divider--labeled"><span>or</span></div>. */
.hzo-divider {
    border: 0;
    border-top: var(--border-1) solid var(--border);
    height: 0;
    margin: var(--space-lg) 0;
}
.hzo-divider--labeled {
    border-top: 0;
    height: auto;
    display: flex;
    align-items: center;
    gap: var(--space-md);
    color: var(--text-muted);
    font-family: var(--font-body);
    font-size: var(--fs-75);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
}
.hzo-divider--labeled::before,
.hzo-divider--labeled::after {
    content: "";
    flex: 1;
    border-top: var(--border-1) solid var(--border);
}


/* --- drawer/drawer.css --- */
/* ─── Drawer / off-canvas ──────────────────────────────────────────────
   Edge-anchored side panel (filters, mini-cart, secondary nav). Shares the
   modal's overlay MECHANICS — focus trap, the shared background inert + scroll
   lock, Esc / backdrop / close-button dismiss, focus return (drawer.js, built
   on hzo.trapFocus in main.js) — but NOT its layout: it stays a side panel
   at EVERY width. Phones cap the width but keep the side anchor; it never
   becomes a bottom sheet (that's the modal). Default slides from the right;
   --left slides from the left. Portaled to <body> on init. */
.hzo-drawer {
    position: fixed;
    inset: 0;
    z-index: var(--z-drawer);
    display: flex;
    justify-content: flex-end;
    background: rgba(0, 0, 0, 0.5);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--transition-base) ease, visibility var(--transition-base);
}
.hzo-drawer--left { justify-content: flex-start; }
.hzo-drawer.is-open { opacity: 1; visibility: visible; }

.hzo-drawer__panel {
    position: relative;
    display: flex;
    flex-direction: column;
    width: min(85vw, 360px);
    max-width: 100%;
    height: 100%;
    background: var(--surface, #fff);
    box-shadow: var(--shadow-lg);
    overflow-y: auto;
    transform: translateX(100%);
    transition: transform var(--transition-base) ease;
}
.hzo-drawer--left .hzo-drawer__panel { transform: translateX(-100%); }
.hzo-drawer.is-open .hzo-drawer__panel { transform: none; }
.hzo-drawer__panel--wide { width: min(92vw, 480px); }

.hzo-drawer__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-lg);
    border-bottom: var(--border-1) solid var(--border);
    flex-shrink: 0;
}
.hzo-drawer__title {
    margin: 0;
    font-family: var(--font-heading);
    font-size: var(--fs-400);
    line-height: 1.3;
    color: var(--text-heading);
}
.hzo-drawer__close {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin: -10px -10px -10px 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: var(--radius-control);
    cursor: pointer;
    transition: background var(--transition-base) ease, color var(--transition-base) ease;
}
.hzo-drawer__close:hover { background: var(--bg-alt); color: var(--text-heading); }
.hzo-drawer__close:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

.hzo-drawer__body {
    flex: 1 1 auto;
    min-height: 0;                    /* G7: let the body shrink so IT is the sole scroller — */
    overflow-y: auto;                 /* the footer (mini-cart Checkout) is never pushed off-screen */
    overscroll-behavior: contain;
    padding: var(--space-lg);
    color: var(--text-body);
}
.hzo-drawer__body > :first-child { margin-top: 0; }
.hzo-drawer__body > :last-child { margin-bottom: 0; }

.hzo-drawer__footer {
    flex-shrink: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    padding: var(--space-lg);
    border-top: var(--border-1) solid var(--border);
}

@media (prefers-reduced-motion: reduce) {
    .hzo-drawer,
    .hzo-drawer__panel,
    .hzo-drawer__close { transition: none; }
}


/* --- embed/embed.css --- */
/* ─── Embed frame (.hzo-embed) ─────────────────────────────────────────
   Aspect-ratio wrapper for any embedded media — iframes, <video>, or the
   .hzo-lite-video facade below. --embed-aspect reshapes it (default 16/9;
   e.g. style="--embed-aspect: 9 / 16" for vertical shorts). The frame owns
   radius + clipping; whatever fills it stretches edge to edge. */
.hzo-embed {
    position: relative;
    aspect-ratio: var(--embed-aspect, 16 / 9);
    border-radius: var(--radius-panel);
    overflow: hidden;
    background: var(--bg-alt);   /* letterbox field while media loads */
}
.hzo-embed > iframe,
.hzo-embed > video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
}
.hzo-embed > video { object-fit: cover; }


/* --- empty-state/empty-state.css --- */
/* ─── Empty state ─────────────────────────────────────────────────────
   Zero-state for no search results, an empty cart, or no posts/orders.
   Centered icon + headline (markup uses .hzo-visual-h3) + one-line
   explanation + a primary action (and optional secondary links). Composes
   .hzo-btn for the action; no new tokens. */
.hzo-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--space-sm);
    max-width: 480px;
    margin-inline: auto;
    padding: var(--space-2xl) 0;
}
.hzo-empty__icon {
    width: 64px;
    height: 64px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--brand);
    background: var(--brand-pale);
    border-radius: var(--radius-full);
    margin-bottom: var(--space-sm);
}
.hzo-empty__icon svg { width: 32px; height: 32px; }
.hzo-empty__text { color: var(--text-muted); line-height: var(--lh-normal); margin: 0; max-width: 40ch; }
.hzo-empty__actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-sm); margin-top: var(--space-md); }

/* ── Widget modifiers (opt-in via [hzo_empty_state]; Phase-2 wave 6) ──
   ADDITIVE ONLY. The base .hzo-empty rules above are a shared class contract —
   mini-cart chrome (.hzo-empty.hzo-minicart__empty) and wc-ajax's no-results
   injection reuse them — so they never change; variants are new classes. */
.hzo-empty--left {
    align-items: flex-start;
    text-align: left;
    margin-inline: 0;
}
.hzo-empty--left .hzo-empty__actions { justify-content: flex-start; }
.hzo-empty--compact { padding: var(--space-lg) 0; }
.hzo-empty--compact .hzo-empty__icon { width: 48px; height: 48px; }
.hzo-empty--compact .hzo-empty__icon svg { width: 24px; height: 24px; }

/* ═══════════════════════════════════════════════════════════════════════
   STATIC BLOCK PRIMITIVES (component library Wave D) — CSS-only, no JS,
   no new tokens.
   ═══════════════════════════════════════════════════════════════════════ */

/* ─── Error variant (audit G48) — a FAILED load, visually distinct from an
   empty result. The 'alert' glyph carries the --error tone; the retry button
   reuses .hzo-btn styling. ─────────────────────────────────────────────── */
.hzo-empty--error .hzo-empty__icon { color: var(--error); }


/* --- fat-footer/fat-footer.css --- */
/* fat-footer — a rich multi-column footer LAYOUT that arranges the engine's
   existing footer components (footer-chrome / footer-heading / trust-row /
   input-group / footer-legal) into a brand column + link columns + an offers
   card, a newsletter row, a trust-badge row, and a legal/credits bar. Layout +
   token-driven color only; it composes existing components and reinvents nothing.

   Brand-agnostic: the footer surface comes from --footer-bg (set on
   .hzo-site-footer, footer-chrome.css); the offers panel reads
   --fat-footer-offers-bg (falls back to --bg-dark-section); legal bar + fav read
   --brand-deep / --brand; white-on-dark text is the dark-footer convention.
   A client dials the exact footer tones via those tokens in its own :root. */

/* ── Final CTA band (arranges .hzo-section--dark + eyebrow + visual-h2 + btn) ── */
.hzo-fat-footer__cta-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-lg);
    flex-wrap: wrap;
}
.hzo-fat-footer__cta .hzo-eyebrow { color: var(--accent); }
.hzo-fat-footer__cta .hzo-visual-h2 { margin: 6px 0 0; }
.hzo-fat-footer__cta-phone { color: rgba(255, 255, 255, 0.85); font-weight: 700; }
.hzo-fat-footer__cta-phone a { color: #fff; }

/* ── Fat-footer grid (arranges existing footer components) ── */
.hzo-fat-footer {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-xl);
    padding-bottom: var(--space-xl);
}
@media (min-width: 640px) { .hzo-fat-footer { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1024px) {
    .hzo-fat-footer { grid-template-columns: 1.15fr 1fr 1fr 1fr auto; gap: var(--space-lg-2); align-items: start; }
}
.hzo-fat-footer__list { list-style: none; margin: 0; padding: 0; }
.hzo-fat-footer__list li { margin: 0 0 10px; }
.hzo-fat-footer__h2 { margin-top: var(--space-lg); }
.hzo-fat-footer__loc { margin: var(--space-md) 0; color: rgba(255, 255, 255, 0.72); font-size: 14px; }
.hzo-fat-footer__loc p { margin: 0 0 4px; }

/* Free Tools card = existing .hzo-card--bordered, re-toned for the dark footer */
.hzo-fat-footer__offers { align-self: start; background: var(--fat-footer-offers-bg, var(--bg-dark-section)); border-color: rgba(255, 255, 255, 0.10); }
@media (min-width: 1024px) { .hzo-fat-footer__offers { width: 260px; } }
.hzo-fat-footer__offers .hzo-footer-heading { color: var(--accent); }

/* ── Newsletter row (wraps the existing .hzo-input-group) ── */
.hzo-fat-footer__news {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-lg);
    flex-wrap: wrap;
    padding: var(--space-lg) 0;
    border-top: 1px solid rgba(255, 255, 255, 0.1);
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.hzo-fat-footer__news-h { text-transform: none; color: #fff; font-size: 17px; margin: 0 0 4px; opacity: 1; }
.hzo-fat-footer__news-p { color: rgba(255, 255, 255, 0.55); font-size: 13px; margin: 0; }

/* ── Trust badges (wraps the existing .hzo-trust-row) ── */
.hzo-fat-footer__badges { padding: var(--space-lg) 0 var(--space-xl-2); }
.hzo-fat-footer__badges-lead { color: rgba(255, 255, 255, 0.5); margin-bottom: var(--space-md); }

/* ── Legal / credits bar (engine footer-legal classes) ── */
.hzo-fat-footer__legalwrap { background: var(--brand-deep); }
.hzo-fat-footer__legalwrap .hzo-site-footer__legal { padding: 18px 0; }
.hzo-fat-footer__credit { display: inline-flex; align-items: center; gap: 12px; }
.hzo-fat-footer__fav {
    width: 30px; height: 30px; flex: none;
    background: var(--brand); color: #fff;
    border-radius: var(--radius-input);
    display: inline-flex; align-items: center; justify-content: center;
    font-family: var(--font-heading); font-weight: 900; font-size: 17px;
}
.hzo-fat-footer__legal-links { display: flex; gap: 16px; flex-wrap: wrap; }


/* --- feature-tabs/feature-tabs.css --- */
/* feature-tabs — @tokens: --feature-tabs-gap, --feature-tabs-media-radius

   A tabbed feature showcase. The TAB UI is the existing .hzo-tabs (its list,
   tabs, panels + the main.js roving-focus wiring, reused verbatim — this file
   adds NO tab styling and re-themes nothing). All this component contributes is
   the layout INSIDE each panel: a two-column feature — media on one side, copy
   (heading + text + CTA) on the other. It composes .hzo-tabs + .hzo-btn; it only
   arranges them.

   No focus/disabled idioms here — the focusable parts (tabs, CTA) belong to
   .hzo-tabs / .hzo-btn and carry their own a11y. Token-only: the two-column gap +
   the media radius have no single semantic role that fits, so they are component
   tokens with a literal-token fallback (the icon-button.css dial pattern). */

/* Two-column panel layout — media | body, stacked on mobile, side-by-side above
   the breakpoint (min-width, mirroring .hzo-feature / .hzo-hero--split). Applied
   inside each .hzo-tabs__panel; .hzo-tabs owns the panel's top padding + hidden. */
.hzo-feature-tabs__panel {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--feature-tabs-gap, var(--space-xl));
    align-items: center;
}
@media (min-width: 1024px) {
    .hzo-feature-tabs__panel {
        grid-template-columns: 1fr 1fr;
        gap: var(--space-3xl);
    }
}

/* Media side — image fills the column at panel radius (matches .hzo-feature__media). */
.hzo-feature-tabs__media img {
    width: 100%;
    height: auto;
    display: block;
    border-radius: var(--feature-tabs-media-radius, var(--radius-panel));
}

/* Body side — heading + text + CTA stack. Reset first/last margins so the copy
   aligns flush with the media top; the heading / body / .hzo-btn classes carry
   all type + control styling. */
.hzo-feature-tabs__body > :first-child { margin-top: 0; }
.hzo-feature-tabs__body > :last-child { margin-bottom: 0; }

/* Modifier — media on the opposite side on desktop (alternating rhythm across
   panels). Below the breakpoint the source order (media first) is kept. */
@media (min-width: 1024px) {
    .hzo-feature-tabs__panel--media-last .hzo-feature-tabs__media { order: 2; }
}


/* --- field/field.css --- */
/* field — @tokens: (none — layout + typography only, all semantic/L1 tokens)

   A standalone single-field wrapper (label + control + hint + error) for an input
   used OUTSIDE a full `.hzo-form` — e.g. a search box or filter in a widget. This
   styles LAYOUT + label/hint/error TYPOGRAPHY only; it deliberately does NOT touch
   the input element itself (that's the input's own job — `.hzo-form` inputs, native
   controls, or a primitive like `.hzo-range`). Nothing in the wrapper is focusable,
   so no focus idiom here — the control inside carries its own focus ring.

   Token-only: every value is a semantic/L1 token straight off the scale, so there
   are no component-token dials to declare. */
.hzo-field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
}

/* Label — heading-ink, semibold, small-text step. */
.hzo-field__label {
    font-weight: var(--fw-600);
    color: var(--text-heading);
    font-size: var(--fs-100);
    line-height: var(--lh-snug);
}

/* Control slot — holds the input; the input styles itself. */
.hzo-field__control {
    display: block;
}

/* Hint — muted helper text below the control. */
.hzo-field__hint {
    color: var(--text-muted);
    font-size: var(--fs-50);
    line-height: var(--lh-normal);
}

/* Error — same size as the hint, error ink. Hidden by default; the --error
   modifier reveals it (error text only exists when the field is in error). */
.hzo-field__error {
    display: none;
    color: var(--error);
    font-size: var(--fs-50);
    line-height: var(--lh-normal);
}

/* Modifier: error — surface the error message and tint the label to match. */
.hzo-field--error .hzo-field__error {
    display: block;
}
.hzo-field--error .hzo-field__label {
    color: var(--error);
}

/* Modifier: inline — label sits BESIDE the control (label auto, control fills). */
.hzo-field--inline {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: var(--space-sm);
}
/* Hint + error span both columns so they align under the control. */
.hzo-field--inline .hzo-field__hint,
.hzo-field--inline .hzo-field__error {
    grid-column: 2;
}

/* Disabled — dims the whole field group (the control inside is disabled in its
   own markup; this greys the label/hint to match). No focus idiom: nothing in
   the wrapper is focusable. */
.hzo-field[aria-disabled="true"],
.hzo-field.is-disabled {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}


/* --- file-upload/file-upload.css --- */
/* file-upload — @tokens: --file-upload-icon-size

   A dropzone that wraps a hidden native <input type="file">. The root is the
   <label>, so a click anywhere on the zone opens the file picker while the input
   stays keyboard-focusable behind the sr-only pattern. `__zone` is the visible
   dashed panel; `.is-dragover` is JS-toggled onto it during a drag.

   Token-only: the glyph box has no semantic token in the scale, so it's a
   component token with a literal fallback — the same dial pattern
   icon-button.css uses (`var(--icon-button-icon-size, 20px)`). The hover /
   dragover tint reads --brand-pale (a declared L2 token). Everything else reads
   a semantic/L1 token. */
.hzo-file-upload {
    display: block;
    cursor: pointer;
}

/* Native input — visually hidden (sr-only) but kept in the tab order + as the
   drop target. */
.hzo-file-upload__input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
    opacity: 0;
}

/* Visible dropzone — the dashed panel the user sees and drags onto. */
.hzo-file-upload__zone {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-xl);
    border: var(--border-2) dashed var(--border);
    border-radius: var(--radius-panel);
    background: var(--bg-alt);
    color: var(--text-muted);
    text-align: center;
    transition: border-color var(--transition-base) ease, background var(--transition-base) ease;
}

/* Leading glyph — fixed box, muted ink. */
.hzo-file-upload__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--file-upload-icon-size, 40px);
    height: var(--file-upload-icon-size, 40px);
    color: var(--text-muted);
}
.hzo-file-upload__icon > svg { display: block; }

/* Prompt text — can carry a bolded <strong> "Browse" + a muted remainder. */
.hzo-file-upload__text {
    font-size: var(--fs-100);
    line-height: var(--lh-normal);
}

/* Hover / dragover — brand border + subtle brand tint to signal the live drop
   target. Two spellings of one meaning: :hover for pointer, `.is-dragover` for
   the JS-toggled drag state. */
.hzo-file-upload:hover .hzo-file-upload__zone,
.hzo-file-upload__zone.is-dragover {
    border-color: var(--brand);
    background: var(--brand-pale);
}

/* A11y — required. Focus lives on the sr-only input; surface the ring on the
   visible zone via the sibling combinator. Disabled state. Reduced-motion is
   handled globally by components/_base/a11y.css. */
.hzo-file-upload__input:focus-visible ~ .hzo-file-upload__zone {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}
.hzo-file-upload.is-disabled,
.hzo-file-upload__input[disabled] ~ .hzo-file-upload__zone,
.hzo-file-upload__input[aria-disabled="true"] ~ .hzo-file-upload__zone {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-file-upload__zone { transition: none; }
}


/* --- filters/filters.css --- */
/* ─── Filters / facets (UI surface) ───────────────────────────────────
   Accessible faceted-filter UI: fieldset/legend groups of checkboxes (multi)
   or radios (single), active-filter chips (reuse .hzo-tag--removable), a
   clear-all control, and an aria-live result-count region. Behaviour — chip↔box
   sync, clear-all, count announce, and the hzo:filterchange event — is wired by
   filters.js (Phase-2 widget) off the .hzo-filters / [data-hzo-filters] contract.
   UI surface only: it emits state, it does not query. Zero new tokens.
   Markup contract: component-patterns.md §47. */
.hzo-filters { display: flex; flex-direction: column; gap: var(--space-md); max-width: 320px; }
.hzo-filters__bar {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-sm); flex-wrap: wrap;
}
.hzo-filters__count { margin: 0; font-size: 0.875rem; color: var(--text-muted); }
.hzo-filter-group { border: 0; margin: 0; padding: 0; min-width: 0; }
.hzo-filter-group > legend {
    padding: 0; margin: 0 0 var(--space-sm);
    font-family: var(--font-heading);
    font-weight: var(--fw-600);
    font-size: 0.9375rem; color: var(--text-heading);
}
.hzo-filter-group + .hzo-filter-group { border-top: var(--border-1) solid var(--border); padding-top: var(--space-md); }
.hzo-filter-group__options { display: flex; flex-direction: column; gap: var(--space-sm); }
.hzo-filter-option {
    display: flex; align-items: center; gap: 10px;
    font-size: 0.9375rem; color: var(--text-body); line-height: 1.3; cursor: pointer;
}
.hzo-filter-option > input { flex: 0 0 auto; width: 16px; height: 16px; accent-color: var(--brand); cursor: pointer; }

/* G26 — no-JS "Apply filters" submit (rendered inside <noscript>, so present
   only when scripting is off). Spaced from the last fieldset. */
.hzo-filters__apply { margin-top: var(--space-md); align-self: flex-start; }


/* --- footer/footer-typography.css --- */
/* ─── Footer typography (for dark footer contexts) ──────────────────── */
.hzo-footer-heading {
    display: block;
    font-family: var(--font-body);
    font-size: var(--fs-100);
    font-weight: var(--fw-700, 700);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wider);
    color: var(--footer-heading-dark, #fff);
    margin: 0 0 var(--space-lg);
}
/* !important on color: fights a:visited from per-client main.css.
   Footer-link variants below match this !important so they can beat the base. */
.hzo-footer-link {
    font-family: var(--font-body);
    font-size: var(--fs-100);
    font-weight: var(--fw-500, 500);
    color: var(--footer-link-dark, rgba(255,255,255,0.8)) !important;
    text-decoration: none;
    line-height: var(--lh-normal);
    padding: var(--space-sm) 0;
    transition: color 0.15s;
    display: block;
    width: fit-content;  /* stack vertically, but only clickable on the text itself */
}
.hzo-footer-link:hover,
.hzo-footer-link:focus-visible { color: var(--footer-link-hover-dark, #fff) !important; }
.hzo-footer-legal {
    font-family: var(--font-body);
    font-size: var(--fs-75);
    color: var(--footer-legal-text-dark, rgba(255,255,255,0.65));
    line-height: 1.6;
}

/* NAP (Name, Address, Phone) block — for the footer contact column and any
   other spot that needs a formatted business-info listing. Each row has a
   small accent-colored icon on the left and text/link on the right. */
.hzo-footer-nap {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
}
.hzo-footer-nap__name {
    font-family: var(--font-body);
    font-weight: var(--fw-700, 700);
    color: var(--footer-heading-dark, #fff);
    font-size: 15px;
    margin: 0 0 var(--space-xs);
}
.hzo-footer-nap__row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    font-family: var(--font-body);
    font-size: var(--fs-100);
    font-weight: var(--fw-500, 500);
    line-height: var(--lh-normal);
    color: var(--footer-link-dark, rgba(255,255,255,0.8));
    margin: 0;
}
/* Icons hidden by default. A client can re-show them with
   `.hzo-footer-nap__icon { display: flex; }` in their main.css. */
.hzo-footer-nap__icon {
    display: none;
    flex-shrink: 0;
    width: 16px;
    height: 16px;
    color: var(--accent);
    margin-top: 3px;
}
/* Contact lines read like the other footer links: inherit the footer link color
   and lighten to #fff on hover. The NAP anchors carry the .hzo-footer-nap__link
   class (added in [hzo_nap], v1.1.93) — the class EXISTS purely so a per-client
   content-link rule like `a:not([class])` excludes them by contract; the styling
   here keeps the original descendant selectors, which win on normal cascade
   order within this file. No !important needed. */
.hzo-footer-nap__row a,
.hzo-footer-nap__row a:visited {
    color: inherit;
    background-image: none;
    text-decoration: none;
    transition: color 0.2s ease;
}
.hzo-footer-nap__row a:hover,
.hzo-footer-nap__row a:visited:hover,
.hzo-footer-nap__row a:focus-visible { color: var(--footer-link-hover-dark, #fff); }

/* Light footer variant — when the footer sits on a light background.
   !important on color matches the base .hzo-footer-link so the light
   variant wins the same-specificity fight against a:visited. */
.hzo-site-footer--light { background: var(--footer-bg-light, var(--bg-alt)); color: var(--footer-text-light, var(--text-body)); }
.hzo-site-footer--light .hzo-footer-heading { color: var(--footer-heading-light, var(--text-heading)); }
.hzo-site-footer--light .hzo-footer-link { color: var(--footer-link) !important; }
.hzo-site-footer--light .hzo-footer-link:hover,
.hzo-site-footer--light .hzo-footer-link:focus-visible { color: var(--footer-link-hover) !important; }
.hzo-site-footer--light .hzo-footer-legal { color: var(--footer-legal-text-light, var(--text-muted)); }
.hzo-site-footer--light a { color: var(--text-body) !important; }
/* NAP contact lines mirror the footer-link polarity on light footers too. */
.hzo-site-footer--light .hzo-footer-nap__row,
.hzo-site-footer--light .hzo-footer-nap__row a { color: var(--footer-link) !important; }
.hzo-site-footer--light .hzo-footer-nap__row a:hover,
.hzo-site-footer--light .hzo-footer-nap__row a:focus-visible { color: var(--footer-link-hover) !important; }


/* --- footer/footer-chrome.css --- */
/* ─── Site footer chrome ────────────────────────────────────────────── */
.hzo-site-footer { background: var(--footer-bg, transparent); color: var(--footer-text, #fff); }
.hzo-site-footer a { color: var(--footer-link-base-dark, rgba(255,255,255,0.85)); text-decoration: none; }
.hzo-site-footer a:hover { color: var(--footer-link-hover-dark, #fff); }

/* Footer column grid — single column on mobile, 4-column on tablet+.
   Brand column (first) takes 2× the width of the other columns on desktop. */
.hzo-site-footer__grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--footer-gap, 32px);
}
@media (min-width: 768px) {
    .hzo-site-footer__grid {
        grid-template-columns: 2fr 1fr 1fr 1fr;
        gap: var(--footer-gap, 40px);
    }
}

/* Footer legal bar — stacks on mobile, spreads on tablet+ */
.hzo-site-footer__legal {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--footer-legal-gap, 12px);
}
@media (min-width: 768px) {
    .hzo-site-footer__legal {
        flex-direction: row;
        justify-content: space-between;
        align-items: center;
        gap: var(--footer-legal-gap, 16px);
    }
}


/* --- footer/footer.css --- */
/* ─── Site footer chrome + typography ──────────────────────────────────
   Aggregator sheet — the component loader (includes/component-loader.php)
   enqueues ONLY <name>.css per folder, so the footer's two variant sheets
   are @imported here to ride the same request. This is their ONLY front-end
   load path: on the front end hzostudio_enqueue_components() REPLACES the
   whole-file primitives.css enqueue, so primitives.css's own @imports of
   these sheets (lines 27 + 41) load in the block-editor context only. Every
   other component's primary sheet is already named <name>.css and is picked
   up directly; footer's was split into two non-<name>.css files, so without
   this aggregator the footer renders UNSTYLED on the front end.
   (Same fix pattern as video.css @importing lite-video + video-background —
   Wave-4 lesson: variant sheets predating widgetization need a <name>.css.)
   Order matches primitives.css: typography (link/heading/NAP) then chrome
   (background + grid). @imports must precede all rules — keep them first. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */

/* Reduced-motion: kill the color transitions the footer link + NAP anchors
   animate (belt-and-braces on top of the variant sheets — per the convention
   the aggregator ends with a transition-kill; mirrors the shipped video.css). */
@media (prefers-reduced-motion: reduce) {
    .hzo-footer-link,
    .hzo-footer-nap__row a { transition: none; }
}


/* --- form/fluent-forms-wrapper.css --- */
/* ─── Fluent Forms wrapper styling (universal, inside .hzo-form) ─────
   The .hzo-form rules above target NATIVE HTML elements
   (input/textarea/select/checkbox/radio), which already catches
   Fluent's rendered inputs because Fluent emits standard HTML inside
   its own .ff-el-* wrapper divs. This block fills the gap: styling
   for those wrapper divs (labels, help text, field-to-field spacing,
   multi-column rows, section breaks, submit button, error states).

   Source: actual Fluent Forms output sampled from studio.hoziodev.com.
   See component-patterns.md §12 for the markup spec.

   !important is retained where Fluent's own default rule sets the same
   property — same rationale as the base .hzo-form rules above. */

/* Field-to-field vertical rhythm */
.hzo-form .ff-el-group {
    margin-bottom: var(--space-lg);
}

/* Label wrapper + inner label typography */
.hzo-form .ff-el-input--label {
    margin-bottom: var(--space-sm);
}

.hzo-form .ff-el-input--label label {
    display: inline;
    font-family: var(--font-body) !important;
    font-size: 15px !important;
    font-weight: var(--fw-600) !important;
    line-height: 1.4 !important;
    color: var(--text-body) !important;
    margin: 0 !important;
}

/* Required asterisk — Fluent emits .ff-el-is-required.asterisk-right
   on the label wrapper. We render the asterisk via ::after on the
   inner label so we control the color and weight. */
.hzo-form .ff-el-input--label.ff-el-is-required label::after {
    content: " *";
    color: var(--error);
    font-weight: var(--fw-700);
    margin-left: 2px;
}

/* Tooltip info icon — Fluent emits .ff-el-tooltip with an SVG inside
   the label wrapper, surfacing the field's help text on hover. Restyle
   the icon color + bring the cursor signal in line with our body type. */
.hzo-form .ff-el-tooltip {
    display: inline-flex !important;
    align-items: center;
    margin-left: 4px;
    color: var(--text-muted);
    cursor: help;
}
.hzo-form .ff-el-tooltip svg {
    width: 14px;
    height: 14px;
    fill: currentColor;
}

/* Help message — Fluent's equivalent to our .hzo-form__description */
.hzo-form .ff-el-help-message {
    font-size: 15px;
    color: var(--text-muted);
    line-height: var(--lh-normal);
    margin-top: var(--space-sm);
    margin-bottom: 0;
}

/* Multi-column rows — Fluent uses .ff-t-container / .ff-t-cell for
   side-by-side field pairs (name first/last, address city/state).
   Mobile collapses to single column; tablet+ shows side-by-side. */
.hzo-form .ff-t-container {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-md);
}

@media (min-width: 768px) {
    .hzo-form .ff-t-container {
        grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
    }
}

.hzo-form .ff-t-cell {
    min-width: 0; /* Prevent grid blowout from long content / overflow */
}

/* Section break — Fluent wraps in .ff-el-section-break containing an
   h3 .ff-el-section-title, optional description, and an <hr>. Replace
   Fluent's default look with a Hozio-style top-border + heading. */
.hzo-form .ff-el-section-break {
    margin: var(--space-2xl) 0 var(--space-lg) 0;
    padding-top: var(--space-lg);
    border-top: 1px solid var(--border);
}

.hzo-form .ff-el-section-title {
    font-family: var(--font-heading);
    font-size: var(--fs-500);
    font-weight: var(--fw-700);
    line-height: 1.3;
    color: var(--text-heading);
    margin: 0 0 var(--space-sm) 0;
}

.hzo-form .ff-section_break_desk {
    font-size: var(--fs-200);
    color: var(--text-body);
    line-height: var(--lh-normal);
    margin: 0;
}

.hzo-form .ff-el-section-break hr {
    display: none; /* We use the section-break's own border-top */
}

/* Checkbox + radio option container — Fluent wraps each option in
   .ff-el-form-check with a <label.ff-el-form-check-label> wrapping the
   input and a <span> with the option text. The native checkbox/radio
   inside is already styled by the .hzo-form input[type=...] rules above. */
.hzo-form .ff-el-form-check {
    display: block;
    padding: 6px 0;
}

.hzo-form .ff-el-form-check-label {
    display: inline-flex;
    align-items: flex-start;
    gap: 10px;
    font-size: 15px;
    color: var(--text-body);
    line-height: var(--lh-normal);
    cursor: pointer;
    margin: 0;
}

/* Native browser default vertical-aligns the checkbox/radio input mid-
   line, which looks fine for single-line option text but misaligns when
   the label wraps to multiple lines (e.g. the marketing consent statement
   with Privacy Policy link). Nudge the input down 2px so it sits flush
   with the cap-height of the first line of text. */
.hzo-form .ff-el-form-check-label > input[type="checkbox"],
.hzo-form .ff-el-form-check-label > input[type="radio"] {
    margin-top: 2px !important;
    flex-shrink: 0;
}


/* --- form/fluent-forms-multistep.css --- */
/* ─── Fluent Forms — multi-step (progress, panes, prev/next) ───────────
   Fluent's multi-step markup: .ff-step-titles (the progress <ul>; Fluent
   marks the current <li> ff_active and finished ones ff_completed),
   .ff-step-container > .ff-step-body-content > .fluentform-step panes, and
   a .step-nav row holding .ff-btn-prev / .ff-btn-next. Same policy as the
   main Fluent block earlier in this file: scoped under .hzo-form,
   !important retained only where Fluent's own stylesheet sets the same
   property on the same element. */
.hzo-form .ff-step-titles {
    display: flex !important;        /* Fluent lays titles out as table cells */
    flex-wrap: wrap;
    gap: var(--space-sm) var(--space-lg);
    margin: 0 0 var(--space-lg) !important;
    padding: 0 !important;
    list-style: none;
    counter-reset: hzo-ff-step;
}
.hzo-form .ff-step-titles li {
    display: inline-flex !important;
    align-items: center;
    gap: var(--space-sm);
    padding: 0 !important;
    counter-increment: hzo-ff-step;
    font-family: var(--font-body);
    font-size: var(--fs-100);
    font-weight: var(--fw-500);
    color: var(--text-muted) !important;
    background: none !important;
    text-align: left;
}
/* Our numbered chip replaces Fluent's ::before decoration (same pseudo);
   Fluent's ::after connector line is suppressed. */
.hzo-form .ff-step-titles li::before {
    content: counter(hzo-ff-step) !important;
    position: static !important;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    margin: 0 !important;
    border: 2px solid var(--border) !important;
    border-radius: var(--radius-full) !important;
    background: var(--surface, transparent) !important;
    color: var(--text-muted) !important;
    font-size: var(--fs-75);
    font-weight: var(--fw-600);
    line-height: var(--lh-none) !important;
}
.hzo-form .ff-step-titles li::after { content: none !important; }
.hzo-form .ff-step-titles li.ff_active {
    color: var(--text-heading) !important;
    font-weight: var(--fw-600);
}
.hzo-form .ff-step-titles li.ff_active::before {
    border-color: var(--brand) !important;
    background: var(--brand) !important;
    color: var(--text-on-brand) !important;
}
.hzo-form .ff-step-titles li.ff_completed { color: var(--text-body) !important; }
.hzo-form .ff-step-titles li.ff_completed::before {
    content: "\2713" !important;     /* checkmark */
    border-color: var(--accent-text) !important;
    background: var(--accent-pale) !important;
    color: var(--accent-text) !important;
}
/* Step pane entrance — a soft fade+rise on the pane Fluent activates.
   Additive (Fluent's own step switch still runs); animation, not
   transition, so it can't fight Fluent's inline-style animation of the
   container. Reduced motion → no animation, instant swap. */
@media (prefers-reduced-motion: no-preference) {
    .hzo-form .ff-step-body-content .fluentform-step.active {
        animation: hzo-ff-step-in var(--duration-base) var(--ease-out) both;
    }
}
@keyframes hzo-ff-step-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}
/* Prev / next — the same tokenized language as .ff-btn-submit above: next
   is the primary (accent) action, previous the quiet outline. Same
   double-flash prophylactics as the submit button (Fluent animates
   transform/shadow/opacity on .ff-btn hover with its own curves). */
.hzo-form .step-nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-md);
    margin-top: var(--space-md);
}
.hzo-form .ff-btn-next,
.hzo-form .ff-btn-prev,
.hzo-form .ff-btn-next:hover,
.hzo-form .ff-btn-prev:hover,
.hzo-form .ff-btn-next:focus,
.hzo-form .ff-btn-prev:focus,
.hzo-form .ff-btn-next:active,
.hzo-form .ff-btn-prev:active {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    padding: calc(var(--btn-padding-y, 14px) - 2px) calc(var(--btn-padding-x, 32px) - 6px) !important;
    font-family: var(--font-body) !important;
    font-size: 1rem !important;
    font-weight: var(--btn-weight, var(--fw-semibold, 600)) !important;
    text-decoration: none !important;
    border-radius: var(--btn-radius, var(--radius-pill)) !important;
    min-height: 44px;
    line-height: var(--lh-none);
    cursor: pointer;
    box-shadow: none !important;
    transform: none !important;
    text-shadow: none !important;
    filter: none !important;
    background-image: none !important;
    transition: background-color 120ms linear, border-color 120ms linear, color 120ms linear !important;
    animation: none !important;
    opacity: 1 !important;
}
.hzo-form .ff-btn-next {
    background-color: var(--btn-primary-bg, var(--accent)) !important;
    border: 1px solid var(--btn-primary-bg, var(--accent)) !important;
    color: var(--btn-primary-text, var(--text-on-accent)) !important;
}
.hzo-form .ff-btn-next:hover,
.hzo-form .ff-btn-next:focus,
.hzo-form .ff-btn-next:active {
    background-color: var(--btn-primary-hover, var(--accent-hover-on-light)) !important;
    border-color: var(--btn-primary-hover, var(--accent-hover-on-light)) !important;
    color: var(--btn-primary-hover-text, #fff) !important;   /* bg darkens on hover → flip text light (matches .hzo-btn--primary) */
}
.hzo-form .ff-btn-prev {
    background-color: transparent !important;
    border: 1px solid var(--btn-outline-color, var(--brand)) !important;
    color: var(--btn-outline-color, var(--brand)) !important;
}
.hzo-form .ff-btn-prev:hover,
.hzo-form .ff-btn-prev:focus,
.hzo-form .ff-btn-prev:active {
    background-color: var(--btn-outline-color, var(--brand)) !important;
    border-color: var(--btn-outline-color, var(--brand)) !important;
    color: var(--btn-secondary-text, var(--text-on-brand)) !important;
}
.hzo-form .ff-btn-next:focus-visible,
.hzo-form .ff-btn-prev:focus-visible {
    outline: 3px solid var(--color-focus) !important;
    outline-offset: 2px;
}


/* --- form/fluent-forms-file-upload.css --- */
/* ─── Fluent Forms — file upload ───────────────────────────────────────
   Fluent's uploader: .ff_file_upload_holder wraps the trigger + hidden
   input; uploaded files render as .ff-upload-preview rows holding
   .ff-upload-thumb / .ff-upload-details (.ff-upload-filename,
   .ff-upload-filesize, .ff-upload-progress-inline, .ff-upload-error) and
   a remove control. The holder becomes a dashed drop zone with a brand
   lift on hover/focus/drag-over; uploaded rows echo the .hzo-tag chip
   language (quiet border, pale field, muted meta). Best-effort on the
   drag-over class — Fluent has shipped both spellings. */
.hzo-form .ff_file_upload_holder {
    padding: var(--space-lg);
    border: 2px dashed var(--border);
    border-radius: var(--radius-control);
    background: var(--bg-alt);
    text-align: center;
    transition: border-color var(--duration-base) var(--ease-standard),
                background var(--duration-base) var(--ease-standard);
}
.hzo-form .ff_file_upload_holder:hover,
.hzo-form .ff_file_upload_holder:focus-within,
.hzo-form .ff_file_upload_holder.ff_dragover,
.hzo-form .ff_file_upload_holder.drag-over {
    border-color: var(--brand);
    background: var(--brand-pale);
}
@media (prefers-reduced-motion: reduce) {
    .hzo-form .ff_file_upload_holder { transition: none; }
}
/* The visible "choose file" trigger inside the zone — outline-button language. */
.hzo-form .ff_file_upload_holder .ff_upload_btn {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    padding: 10px 20px !important;
    font-family: var(--font-body) !important;
    font-size: 15px !important;
    font-weight: var(--fw-600) !important;
    border: 1px solid var(--btn-outline-color, var(--brand)) !important;
    border-radius: var(--btn-radius, var(--radius-pill)) !important;
    background: transparent !important;
    color: var(--btn-outline-color, var(--brand)) !important;
    box-shadow: none !important;
    cursor: pointer;
    transition: background-color 120ms linear, color 120ms linear !important;
}
.hzo-form .ff_file_upload_holder .ff_upload_btn:hover {
    background: var(--btn-outline-color, var(--brand)) !important;
    color: var(--btn-secondary-text, var(--text-on-brand)) !important;
}
/* Uploaded-file rows — .hzo-tag visual language (chip field + hairline). */
.hzo-form .ff_file_upload_holder .ff-upload-preview {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-top: var(--space-sm);
    padding: 6px 12px;
    text-align: left;
    font-size: var(--fs-75);
    background: var(--surface, #fff);
    border: 1px solid var(--surface-border, var(--border)) !important;
    border-radius: var(--radius-control);
}
.hzo-form .ff-upload-filename {
    color: var(--text-body);
    font-weight: var(--fw-500);
    overflow-wrap: anywhere;
}
.hzo-form .ff-upload-filesize { color: var(--text-muted); }
.hzo-form .ff-upload-error { color: var(--error); font-size: var(--fs-75); }
/* Inline progress bar during upload — quiet track, accent fill. */
.hzo-form .ff-upload-progress-inline {
    height: 4px;
    border-radius: var(--radius-full);
    background: var(--border);
    overflow: hidden;
}
.hzo-form .ff-upload-progress-inline .ff-el-progress-bar {
    height: 100%;
    background: var(--accent) !important;
}
/* Remove ✕ on an uploaded row — echoes .hzo-tag__remove. */
.hzo-form .ff-upload-preview .ff-upload-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: 50%;
    cursor: pointer;
}
.hzo-form .ff-upload-preview .ff-upload-remove:hover {
    background: var(--border);
    color: var(--text-heading);
}


/* --- form/choices.css --- */
/* ─── Choices.js (multi-select widget) ──────────────────────────────────
   Fluent enhances <select multiple> via Choices.js — replacing the native
   widget with its own div-based DOM (.choices > .choices__inner >
   .choices__list--multiple + .choices__input). Without these overrides,
   the wrapper renders with Choices.js defaults: .25rem radius, light gray
   border, and a nested .choices__input that draws its OWN border + bg —
   producing the "box within a box" effect. Style the wrapper to look
   exactly like our text inputs, and dissolve the inner input into the
   wrapper. */

/* Outer container — kill Choices' margin (we control via .ff-el-group) */
.hzo-form .choices {
    margin-bottom: 0 !important;
    font-size: 1rem;
}

/* The visible "field" surface — match .hzo-form input styling. Padding
   here mirrors `.hzo-form input` (12px 16px) with right-pad expanded to
   44px to clear the chevron. min-height 50px = 48px content + 2px border
   so single + multi selects vertically match the text inputs above/below. */
.hzo-form .choices .choices__inner {
    background: var(--input-bg, #fff) !important;
    border: 1px solid var(--input-border, var(--border)) !important;
    border-radius: var(--input-radius, var(--radius-input)) !important;
    padding: 12px 44px 12px 16px !important;
    min-height: 50px !important;
    font-family: var(--font-body) !important;
    font-size: 1rem !important;
    color: var(--text-body) !important;
    line-height: var(--lh-normal) !important;
    transition: border-color 0.15s, box-shadow 0.15s;
    box-sizing: border-box;
}

/* Single-select value/placeholder — Choices wraps the selected value in
   .choices__list--single > .choices__item. Strip the list's default
   inline-block padding (6px 24px 6px 12px) so .choices__inner controls
   all spacing — otherwise the value text appears double-padded. */
.hzo-form .choices[data-type*="select-one"] .choices__inner {
    padding: 12px 44px 12px 16px !important;
}

.hzo-form .choices .choices__list--single {
    padding: 0 !important;
    width: 100% !important;
    display: block !important;
}

.hzo-form .choices .choices__list--single .choices__item {
    font-family: var(--font-body) !important;
    font-size: 1rem !important;
    color: var(--text-body) !important;
    line-height: var(--lh-normal) !important;
    padding: 0 !important;
}

/* Placeholder option in single-select reads dimmer */
.hzo-form .choices .choices__list--single .choices__item--placeholder {
    color: var(--color-placeholder) !important;
    opacity: 1 !important;
}

/* Focused / open state — match our text-input focus ring */
.hzo-form .choices.is-focused .choices__inner,
.hzo-form .choices.is-open .choices__inner {
    border-color: var(--color-focus) !important;
    box-shadow: 0 0 0 2px var(--brand-pale) !important;
}

/* When dropdown is open, Choices' default flips the wrapper border-radius
   to square at the bottom. Keep it rounded — the dropdown popup gets its
   own radius below. */
.hzo-form .choices.is-open .choices__inner {
    border-radius: var(--radius-control) !important;
}

/* The typing/search input nested inside the wrapper. Choices.js styles it
   as a separate boxed input by default (border + bg + radius) — strip
   that so it visually blends into .choices__inner. */
.hzo-form .choices .choices__input {
    background: transparent !important;
    border: none !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    margin: 0 !important;
    padding: 4px 0 !important;
    font-family: var(--font-body) !important;
    font-size: 1rem !important;
    color: var(--text-body) !important;
    width: 100% !important;
    min-width: 100px;
}

.hzo-form .choices .choices__input:focus {
    outline: none !important;
    border: none !important;
    box-shadow: none !important;
}

/* Placeholder — Choices.js renders "Select all that apply" via either
   the search input's placeholder attr or a .choices__placeholder span */
.hzo-form .choices .choices__placeholder {
    opacity: 1 !important;
    color: var(--color-placeholder) !important;
}
.hzo-form .choices .choices__input::placeholder {
    color: var(--color-placeholder) !important;
    opacity: 1 !important;
}

/* Selected items in the multi-select control — chips/tags
   Match the .ff_list_buttons selected state so multi-select chips visually
   tie back to the connected-bar style (light brand fill + brand border +
   body-text color). */
.hzo-form .choices .choices__list--multiple .choices__item {
    background: var(--brand-pale) !important;
    border: 1px solid var(--brand) !important;
    border-radius: var(--radius-full) !important;
    color: var(--text-body) !important;
    padding: 4px 12px !important;
    font-size: var(--fs-100) !important;
    font-weight: var(--fw-500) !important;
    margin: 2px 4px 2px 0 !important;
    line-height: 1.4 !important;
}

/* The "x" remove-button on each chip. Choices.js paints the X as an SVG
   background-image baked white into its core CSS — invisible against our
   light --brand-pale chip background. Override the background-image with
   a brand-colored X. No left divider; tight spacing against the chip
   label. */
.hzo-form .choices .choices__list--multiple .choices__button {
    border-left: none !important;
    margin: 0 0 0 6px !important;
    padding-left: 0 !important;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23114899' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3C/svg%3E") !important;
    background-position: center !important;
    background-repeat: no-repeat !important;
    background-size: 10px 10px !important;
    opacity: 0.7 !important;
    width: 14px !important;
    height: 14px !important;
}
.hzo-form .choices .choices__list--multiple .choices__button:hover {
    opacity: 1 !important;
}

/* Dropdown popup container */
.hzo-form .choices .choices__list--dropdown,
.hzo-form .choices .choices__list[aria-expanded] {
    background: #fff !important;
    border: 1px solid var(--border) !important;
    border-top-left-radius: var(--radius-control) !important;
    border-top-right-radius: var(--radius-control) !important;
    border-bottom-left-radius: var(--radius-control) !important;
    border-bottom-right-radius: var(--radius-control) !important;
    margin-top: 4px !important;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
    overflow: hidden;
    z-index: var(--z-popover);   /* G19: popover-class overlay, BELOW --z-toast — never covers cart/status toasts */
}

/* Items in dropdown popup */
.hzo-form .choices .choices__list--dropdown .choices__item,
.hzo-form .choices .choices__list[aria-expanded] .choices__item {
    padding: 10px 16px !important;
    font-family: var(--font-body) !important;
    font-size: 15px !important;
    color: var(--text-body) !important;
    line-height: 1.4 !important;
}

/* Hover / keyboard-highlighted item */
.hzo-form .choices .choices__list--dropdown .choices__item--selectable.is-highlighted,
.hzo-form .choices .choices__list[aria-expanded] .choices__item--selectable.is-highlighted {
    background: var(--brand-pale) !important;
    color: var(--text-body) !important;
}

/* Selected (already-picked) item in dropdown — dimmed */
.hzo-form .choices .choices__list--dropdown .choices__item--selectable[data-select-text]::after,
.hzo-form .choices .choices__list[aria-expanded] .choices__item--selectable[data-select-text]::after {
    display: none; /* Hide Choices' "Press to select" hint — feels noisy */
}

/* Custom chevron — Choices.js paints a CSS-triangle (border hack) via
   :after on the wrapper. Replace it with our V-chevron SVG to match the
   single-select. */
.hzo-form .choices[data-type*="select-multiple"]::after,
.hzo-form .choices[data-type*="select-one"]::after {
    content: "" !important;
    border: none !important;
    background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231F2937' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>") no-repeat center / 18px 18px !important;
    width: 18px !important;
    height: 18px !important;
    right: 16px !important;
    top: 50% !important;
    margin-top: -9px !important;
    pointer-events: none !important;
    transition: transform 0.15s !important;
}

/* Flip chevron when dropdown is open */
.hzo-form .choices.is-open::after {
    transform: rotate(180deg) !important;
    margin-top: -9px !important;
    border: none !important;
}

/* Submit button — Fluent emits .ff-btn.ff-btn-submit.ff-btn-md.
   Match the .hzo-btn--primary visual language exactly (accent bg, pill
   radius, heading-weight typography). Override every transform / shadow /
   transition Fluent may set on this button so the hover state is a
   single bg-color change, not a double-flash from competing defaults.

   The "double hover" bug came from Fluent's own ff-btn hover styles
   (transform: translateY + box-shadow on hover) firing at the same time
   as ours but with different transition curves — visually reading as
   two animations stacking. Explicitly negate them. */
.hzo-form .ff-btn.ff-btn-submit,
.hzo-form .ff-btn.ff-btn-submit:hover,
.hzo-form .ff-btn.ff-btn-submit:focus,
.hzo-form .ff-btn.ff-btn-submit:focus-visible,
.hzo-form .ff-btn.ff-btn-submit:active {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: var(--btn-padding-y, 14px) var(--btn-padding-x, 32px) !important;
    font-family: var(--font-body) !important;
    font-size: 1rem !important;
    font-weight: var(--btn-weight, var(--fw-semibold, 600)) !important;
    text-decoration: none !important;
    border-radius: var(--btn-radius, var(--radius-pill)) !important;
    border: 1px solid var(--btn-primary-bg, var(--accent)) !important;
    color: var(--btn-primary-text, var(--text-on-accent)) !important;
    cursor: pointer;
    min-height: 48px;
    line-height: var(--lh-none);
    white-space: nowrap;
    margin-top: var(--space-md);
    /* Nuke every property Fluent might animate on hover that could
       cause perceived double-flash (separate transition curves for
       background-color vs background-image vs box-shadow vs transform
       all firing at once). Single uniform transition on background-color
       only, fast (120ms) so any cascade-cleanup is imperceptible. */
    box-shadow: none !important;
    transform: none !important;
    text-shadow: none !important;
    filter: none !important;
    background-image: none !important;
    /* Both bg AND border animate together at identical timing — otherwise
       border-color snaps instantly (no transition) while bg fades over 120ms,
       which the eye reads as a 1px border flash followed by a separate bg
       shift = the "double-flash" the user kept reporting. */
    transition: background-color 120ms linear, border-color 120ms linear !important;
    animation: none !important;
}

.hzo-form .ff-btn.ff-btn-submit {
    background-color: var(--btn-primary-bg, var(--accent)) !important;
    opacity: 1 !important;
}

.hzo-form .ff-btn.ff-btn-submit:hover,
.hzo-form .ff-btn.ff-btn-submit:focus,
.hzo-form .ff-btn.ff-btn-submit:active {
    background-color: var(--btn-primary-hover, var(--accent-hover-on-light)) !important;
    border-color: var(--btn-primary-hover, var(--accent-hover-on-light)) !important;
    /* Negate `.ff-default .ff_btn_style:hover { opacity: .8 }` from
       fluentform-public-default.css. That rule fires INSTANTLY on hover
       (no transition declared by Fluent) while our background-color
       fades over 120ms — the eye reads the instant 80% dim as a first
       "flash," then the bg-color shift as a second separate event. Lock
       opacity at 1 so the only visible hover change is the smooth
       bg/border fade. Diagnosed via Claude-for-Chrome Animations panel
       trace 2026-05-29. */
    opacity: 1 !important;
}

.hzo-form .ff-btn.ff-btn-submit:focus-visible {
    outline: 3px solid var(--color-focus) !important;
    outline-offset: 2px;
}

/* Mobile: submit becomes full-width by default (matches .hzo-btn behavior) */
@media (max-width: 767px) {
    .hzo-form .ff-btn.ff-btn-submit {
        display: flex !important;
        width: 100%;
    }
}

/* G49 (ship-review) — visible in-flight state for the Fluent submit button.
   form.js's submit-lock adds .is-loading + aria-busy on a real submit, but the
   .hzo-btn spinner recipe never matches .ff-btn-submit, so a user submitting the
   most common form type saw the button go inert with NO feedback. Paint the
   spinner on ::after (the double-flash rule above sets animation:none on the
   ELEMENT, not the pseudo, so it isn't nuked) and hide the label via
   color:transparent (higher specificity than the base color rule). Own keyframe
   because spinner.css is gated and may be absent on a form-only page. */
@keyframes hzo-ff-submit-spin { to { transform: rotate(360deg); } }
.hzo-form .ff-btn.ff-btn-submit.is-loading {
    color: transparent !important;
    position: relative;
    pointer-events: none;
}
.hzo-form .ff-btn.ff-btn-submit.is-loading::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 18px;
    height: 18px;
    margin: -9px 0 0 -9px;
    border-radius: 50%;
    border: 2px solid color-mix(in srgb, var(--btn-primary-text, var(--text-on-accent)) 35%, transparent);
    border-top-color: var(--btn-primary-text, var(--text-on-accent));
    animation: hzo-ff-submit-spin 0.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
    .hzo-form .ff-btn.ff-btn-submit.is-loading::after { animation-duration: 1.5s; }
}

/* Fluent's screen-reader-only form title — hide visually (duplicates
   the page heading; only meaningful to AT users when there is no
   visible heading on the page). */
.hzo-form .ff_screen_reader_title {
    position: absolute !important;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
}

/* Validation error states — Fluent adds .error to inputs on validation
   failure and emits .text-danger for the error message text. Match the
   existing .hzo-form__input--error / .hzo-form__message--error rules. */
.hzo-form .ff-el-form-control.error {
    border-color: var(--error) !important;
    box-shadow: 0 0 0 2px var(--error-bg) !important;
}

.hzo-form .text-danger {
    color: var(--error);
    font-size: 15px;
    line-height: 1.4;
    margin-top: var(--space-sm);
    display: block;
}


/* --- form/flatpickr.css --- */
/* ─── Flatpickr (date/time picker popup) ────────────────────────────────
   Fluent Forms' `input_date` field renders a regular text input with the
   `.ff-el-datepicker` class. On focus, Fluent enqueues the Flatpickr
   library which paints a popup calendar (`.flatpickr-calendar`) anchored
   to the input. The closed-state input is already styled by our base
   `.hzo-form input[type="text"]` rules — this block targets the OPEN
   popup so the calendar matches our design language instead of
   Flatpickr's stock blue-on-white look.

   Library reference: github.com/flatpickr/flatpickr (class catalog
   extracted from src/style/flatpickr.styl). Loaded conditionally — these
   rules are inert on pages without a date field. */

/* Popup container — sits outside .hzo-form in the DOM (Flatpickr
   appends it to <body>), so selectors here are unscoped. Use specific
   class names to avoid bleed. */
.flatpickr-calendar {
    background: #fff !important;
    border: 1px solid var(--border) !important;
    border-radius: var(--radius-control) !important;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.10) !important;
    font-family: var(--font-body) !important;
    color: var(--text-body) !important;
    /* IMPORTANT: no padding here — Fluent's CSS sets `.fluentform * {
       box-sizing: border-box }`, so any padding on .flatpickr-calendar
       eats into the content area. Flatpickr's JS sets the calendar's
       inline width to 307.875px (set to fit its dayContainer at
       307.875px). Padding + border-box = content area shrinks to
       291.875px → dayContainer overflows by 16px → Saturday column
       clipped. Box-shadow + border-radius give the popup its visual
       chrome without padding. */
    box-sizing: content-box !important;
}

/* Month/year nav row — explicit min-height so the absolutely-positioned
   .flatpickr-current-month inside it has enough vertical content area
   to render its 38px-tall children (month <select> + year wrapper) without
   clipping. Without this min-height, Flatpickr's row content area is
   ~35px (padding 8px × 2 = 16px overhead) — too small to fit 38px boxes,
   so the boxes overflow vertically and the calendar looks broken. */
.flatpickr-months {
    background: transparent !important;
    border-bottom: 1px solid var(--border-light, var(--border)) !important;
    padding: 8px 4px !important;
    margin-bottom: 8px !important;
    min-height: 54px !important;
    box-sizing: border-box !important;
}

/* `.flatpickr-month` (singular) is the inner row that holds the prev
   arrow + the current-month container + the next arrow. Flatpickr's
   default is `height: 34px` + `overflow: hidden`. The engine bumped
   the .numInputWrapper + .flatpickr-monthDropdown-months children to
   38px tall to match the form's other dropdown chrome — but the 34px
   parent with overflow:hidden shears 4px off the bottom of the
   children, including the year input's bottom border. Raise to 38px
   to match. Diagnosed via Claude-for-Chrome DevTools session
   2026-05-29. */
.flatpickr-month {
    height: 38px !important;
}

.flatpickr-current-month {
    font-family: var(--font-body) !important;
    font-size: var(--fs-100) !important;
    font-weight: var(--fw-700) !important;
    color: var(--text-heading) !important;
    /* Flex layout for predictable horizontal spacing between month
       dropdown + year input + prev/next arrows. Generous gap so
       elements have breathing room. `height: auto` overrides Flatpickr's
       core 28px height default; absolute top:0/bottom:0 still stretches
       it to fill the parent's content area, which we sized to 38px above. */
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 8px !important;
    padding: 0 8px !important;
    height: auto !important;
}

/* Month <select> + year input — Flatpickr emits a real <select> for the
   month and a numeric <input type="number"> wrapper for the year. Both
   must match the form's other dropdowns visually AND must visually align
   (same height + same bottom-edge Y).

   IMPORTANT — selector specificity:
   The base form rules `.hzo-form select` (0,0,1,1) and `.hzo-form
   input[type="number"]` (0,0,2,1) both ship `!important` declarations
   (border, padding, height). A flat `.flatpickr-monthDropdown-months`
   (0,0,1,0) loses the cascade despite `!important`, because the base
   rule's element selector tips specificity. Scope the Flatpickr rules
   under `.hzo-form` to add one class layer + reach (0,0,2,0) for the
   select and (0,0,3,0) for the year input — both win the cascade. */
.hzo-form .flatpickr-monthDropdown-months {
    font-family: var(--font-body) !important;
    font-weight: var(--fw-600) !important;
    font-size: var(--fs-100) !important;
    line-height: 1.2 !important;
    color: var(--text-heading) !important;
    appearance: none !important;
    -webkit-appearance: none !important;
    -moz-appearance: none !important;
    background-color: #fff !important;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231b1f23' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") !important;
    background-repeat: no-repeat !important;
    background-position: right 8px center !important;
    background-size: 14px !important;
    border: 1px solid var(--border) !important;
    border-radius: var(--radius-control) !important;
    /* Explicit height + matching padding so month select bottom-aligns with
       year wrapper. Both elements: 38px box, identical inner content area. */
    height: 38px !important;
    padding: 0 30px 0 12px !important;
    margin: 0 !important;
    box-sizing: border-box !important;
    width: auto !important;
    min-width: 0 !important;
    cursor: pointer !important;
    transition: border-color 150ms ease, box-shadow 150ms ease !important;
}
.hzo-form .flatpickr-monthDropdown-months:hover {
    border-color: var(--brand) !important;
}
.hzo-form .flatpickr-monthDropdown-months:focus {
    border-color: var(--brand) !important;
    outline: none !important;
    box-shadow: 0 0 0 2px var(--brand-pale) !important;
}

/* Year input wrapper — the visible "box" the user sees for the year. All
   border/background chrome lives here; the inner <input> is transparent
   and unbordered so there are no competing visible borders. Wrapper has
   an explicit width (76px) so it visually balances the month dropdown,
   and an explicit height (38px) to bottom-align with the month select.
   `overflow: hidden` clips Flatpickr's absolutely-positioned arrow spans
   inside the rounded border. */
.hzo-form .flatpickr-current-month .numInputWrapper {
    background-color: #fff !important;
    border: 1px solid var(--border) !important;
    border-radius: var(--radius-control) !important;
    padding: 0 !important;
    margin: 0 !important;
    width: 76px !important;
    height: 38px !important;
    overflow: hidden !important;
    box-sizing: border-box !important;
    position: relative !important;
    transition: border-color 150ms ease, box-shadow 150ms ease !important;
}
.hzo-form .flatpickr-current-month .numInputWrapper:hover {
    border-color: var(--brand) !important;
}
.hzo-form .flatpickr-current-month .numInputWrapper:focus-within {
    border-color: var(--brand) !important;
    box-shadow: 0 0 0 2px var(--brand-pale) !important;
}
/* Year <input> — fills wrapper exactly; no visible border of its own (the
   `.hzo-form input[type="number"]` base rule paints one at 1px solid var
   (--border) with !important, so we negate it at higher specificity here).
   Right padding leaves room for the spinner arrows pinned to the wrapper
   right edge. */
.hzo-form .flatpickr-current-month .numInput {
    font-family: var(--font-body) !important;
    font-weight: var(--fw-600) !important;
    font-size: var(--fs-100) !important;
    line-height: 1.2 !important;
    color: var(--text-heading) !important;
    background: transparent !important;
    border: 0 !important;
    border-radius: 0 !important;
    padding: 0 22px 0 10px !important;
    margin: 0 !important;
    width: 100% !important;
    height: 100% !important;
    box-shadow: none !important;
    box-sizing: border-box !important;
    -moz-appearance: textfield !important;
    appearance: textfield !important;
}
.hzo-form .flatpickr-current-month .numInput::-webkit-outer-spin-button,
.hzo-form .flatpickr-current-month .numInput::-webkit-inner-spin-button {
    -webkit-appearance: none !important;
    margin: 0 !important;
}
.hzo-form .flatpickr-current-month .numInput:focus {
    outline: none !important;
    box-shadow: none !important;
}
/* Spinner arrows — keep inside wrapper, divide its 38px height in half */
.hzo-form .flatpickr-current-month .numInputWrapper .arrowUp,
.hzo-form .flatpickr-current-month .numInputWrapper .arrowDown {
    height: 19px !important;
    border: 0 !important;
}

/* Prev/next arrow buttons */
.flatpickr-prev-month,
.flatpickr-next-month {
    color: var(--text-body) !important;
    fill: var(--text-body) !important;
    padding: 8px !important;
    border-radius: var(--radius-control) !important;
    transition: background var(--transition-base), color var(--transition-base) !important;
}
.flatpickr-prev-month:hover,
.flatpickr-next-month:hover {
    background: var(--brand-pale) !important;
    color: var(--accent-text) !important;
    fill: var(--accent-text) !important;
}
.flatpickr-prev-month svg,
.flatpickr-next-month svg {
    fill: inherit !important;
}

/* Time picker — hour/minute inputs + AM/PM toggle hover/focus wash (G59: moved
   here from form/textarea-resize.css, where it was a stray non-textarea rule). */
.flatpickr-time input:hover,
.flatpickr-time .flatpickr-am-pm:hover,
.flatpickr-time input:focus,
.flatpickr-time .flatpickr-am-pm:focus {
    background: var(--brand-pale) !important;
}

/* Weekday header row (Su Mo Tu We Th Fr Sa) */
.flatpickr-weekdaycontainer .flatpickr-weekday {
    color: var(--text-muted) !important;
    font-family: var(--font-body) !important;
    font-weight: var(--fw-600) !important;
    font-size: var(--fs-75) !important;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Day cells */
.flatpickr-day {
    color: var(--text-body) !important;
    font-family: var(--font-body) !important;
    font-size: var(--fs-100) !important;
    border-radius: var(--radius-control) !important;
    border: 1px solid transparent !important;
    transition: background var(--transition-base), border-color var(--transition-base), color var(--transition-base) !important;
}
.flatpickr-day:hover {
    background: var(--brand-pale) !important;
    color: var(--text-body) !important;
    border-color: var(--brand-pale) !important;
}
.flatpickr-day.today {
    border-color: var(--accent) !important;
    color: var(--accent-text) !important;
    background: #fff !important;
}
.flatpickr-day.today:hover {
    background: var(--brand-pale) !important;
}
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.endRange,
.flatpickr-day.selected.inRange,
.flatpickr-day.startRange.inRange,
.flatpickr-day.endRange.inRange,
.flatpickr-day.selected:hover {
    background: var(--accent) !important;
    border-color: var(--accent) !important;
    color: var(--text-on-accent) !important;
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay {
    color: var(--text-muted) !important;
    opacity: var(--opacity-disabled);
}

/* Time picker section — shown when `is_time_enabled: true` */
.flatpickr-time {
    border-top: 1px solid var(--border-light, var(--border)) !important;
    padding-top: 8px !important;
    margin-top: 8px !important;
}
.flatpickr-time input,
.flatpickr-time .flatpickr-am-pm {
    color: var(--text-body) !important;
    font-family: var(--font-body) !important;
    font-size: var(--fs-100) !important;
}

/* Calendar icon on the date-picker input — signals to users that clicking
   opens a calendar (without it the field looks like any other text input).
   Inline SVG (no extra HTTP request). Padding-right on the input prevents
   the icon from overlapping the user's typed text. */
.hzo-form input.ff-el-datepicker {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E") !important;
    background-repeat: no-repeat !important;
    background-position: right 14px center !important;
    background-size: 18px 18px !important;
    padding-right: 44px !important;
    cursor: pointer;
}

/* Flatpickr wrapper — in static:true mode Flatpickr wraps the input
   in a .flatpickr-wrapper that defaults to `display: inline-block`,
   which shrinks both the wrapper AND the input inside it to content
   width instead of filling the form column. Force the wrapper to
   block + 100% width so the date input sits at the same full-width
   as every other input in the form. */
.hzo-form .flatpickr-wrapper {
    display: block !important;
    width: 100% !important;
    position: relative !important;
}

/* Calendar popup — let it take its natural width (~308px, set by
   Flatpickr's dayContainer to fit 7 × 39px day cells). Don't clamp
   width; don't hide overflow. The static-mode positioning places it
   under the input, and the form column is wide enough to contain
   the popup at its natural size. */


/* --- form/format-validation.css --- */
/* ─── Real-time format validation feedback ──────────────────────────────
   Companion to the JS hook in `components/form/form.js` (the real-time
   format-validation enhancement). The JS toggles `.hzo-input-valid` on
   email + masked-phone inputs as the user types; this CSS paints the
   positive cue — green check icon outside the input, subtle green
   border. Negative state is unstyled — browser red outline on blur via
   HTML5 + Fluent's server-side error on submit handle that. */

.hzo-form .ff-el-input--content {
    position: relative;
}

.hzo-form input.hzo-input-valid {
    border-color: #2ecc71 !important;
    padding-right: 44px !important;
}

.hzo-form .ff-el-input--content:has(input.hzo-input-valid)::after {
    content: '';
    position: absolute;
    right: 14px;
    top: 50%;
    transform: translateY(-50%);
    width: 20px;
    height: 20px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232ecc71' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center;
    pointer-events: none;
}


/* --- form/switch-toggle.css --- */
/* ─── Switch / toggle (styled NATIVE checkbox — zero JS) ──────────────
   A toggle is just a checkbox re-skinned as a track + thumb. The native
   <input type="checkbox"> keeps ALL behavior + accessibility for free:
   Tab focus, Space toggles, checked state exposed to assistive tech — so
   NO role / aria-checked is needed (it IS a checkbox). Opt in by wrapping
   the checkbox in <label class="hzo-switch">.

   These rules sit AFTER the base checkbox rules above and at higher
   specificity (.hzo-form .hzo-switch input = 0,0,3,1 vs base 0,0,2,1), so
   they win cleanly. The thumb is drawn with ::before; the base checkbox's
   ::after checkmark is suppressed (content:none). */
.hzo-form .hzo-switch {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    font-size: 15px;
    color: var(--text-body);
    line-height: 1.4;
}
.hzo-form .hzo-switch input[type="checkbox"] {
    appearance: none !important;
    -webkit-appearance: none !important;
    -moz-appearance: none !important;
    flex-shrink: 0;
    width: 44px !important;
    height: 24px !important;
    margin: 0 !important;
    padding: 0 !important;
    border: none !important;
    border-radius: var(--radius-full) !important;   /* geometric capsule — must stay round under square-corner archetypes */
    background: var(--border) !important;
    position: relative;
    cursor: pointer;
    transition: background 0.2s ease;
}
/* Suppress the base checkbox checkmark (drawn via ::after) — the switch uses
   ::before for its thumb instead. */
.hzo-form .hzo-switch input[type="checkbox"]::after { content: none !important; }
.hzo-form .hzo-switch input[type="checkbox"]::before {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #fff;
    box-shadow: var(--shadow-sm);
    transition: transform 0.2s ease;
}
.hzo-form .hzo-switch input[type="checkbox"]:checked { background: var(--brand) !important; }
.hzo-form .hzo-switch input[type="checkbox"]:checked::before { transform: translateX(20px); }
.hzo-form .hzo-switch input[type="checkbox"]:focus-visible {
    outline: none !important;
    box-shadow: 0 0 0 3px var(--brand-pale) !important;
}
@media (prefers-reduced-motion: reduce) {
    .hzo-form .hzo-switch input[type="checkbox"],
    .hzo-form .hzo-switch input[type="checkbox"]::before { transition: none; }
}


/* --- form/textarea-resize.css --- */
/* ─── Textarea — disable horizontal resize ──────────────────────────────
   User can drag the bottom-right corner to grow vertically (helpful for
   long messages), but horizontal resize is locked. Prevents users from
   dragging the textarea narrow + visually breaking the form layout. */
.hzo-form textarea,
.hzo-form textarea.ff-el-form-control {
    resize: vertical !important;
}


/* --- form/disabled-fields.css --- */
/* ─── Disabled fields ─────────────────────────────────────────────────
   The browser default washes disabled controls to a low-opacity gray that
   reads as "broken." Use the muted surface + muted text + not-allowed
   cursor at full opacity instead. Same-specificity-as-base + later source
   order wins the bg/color (both carry !important). */
.hzo-form input:disabled,
.hzo-form textarea:disabled,
.hzo-form select:disabled {
    background: var(--bg-alt) !important;
    color: var(--text-muted) !important;
    -webkit-text-fill-color: var(--text-muted); /* Safari keeps its own disabled text color without this */
    cursor: not-allowed;
    opacity: 1;
}
.hzo-form input:disabled::placeholder,
.hzo-form textarea:disabled::placeholder { color: #b8bec9; }
.hzo-form input[type="checkbox"]:disabled,
.hzo-form input[type="radio"]:disabled,
.hzo-form .hzo-switch input[type="checkbox"]:disabled {
    opacity: var(--opacity-disabled);
    cursor: not-allowed;
}


/* --- form/ff-list-buttons.css --- */
/* ─── .ff_list_buttons — radio/checkbox button rows ──────────────────
   Fluent's `ff_list_buttons` layout renders each option as a clickable
   button styled on the inner <span> (background, border, padding, font).
   Fluent's defaults produce a compact connected-button-bar look that
   already matches our design language well — the only thing we need to
   override is the SELECTED state, which Fluent paints in bright blue
   (#1a7efb) and reads identically to a CTA. Replace just the selected
   span colors with a light brand tint + accent border so selection is
   visible but less prominent than the submit button.

   Fluent marks the selected option via the `ff_item_selected` class on
   the .ff-el-form-check wrapper (managed via Fluent's own JS), which
   makes the override straightforward. */
.hzo-form .ff-el-group.ff_list_buttons .ff-el-form-check.ff_item_selected label > span,
.hzo-form .ff-el-group.ff_list_buttons .ff-el-form-check.ff_item_selected:first-child label > span,
.hzo-form .ff-el-group.ff_list_buttons .ff-el-form-check.ff_item_selected:last-child label > span {
    background-color: var(--brand-pale) !important;
    color: var(--text-body) !important;
    /* Don't override the border — keep it at Fluent's default gray so
       the connected-bar layout stays consistent (no item shifts when
       toggled). Paint the brand outline via box-shadow instead, which
       draws OVER neighbor edges + doesn't double-up on shared edges. */
    box-shadow: 0 0 0 1.5px var(--brand) !important;
    position: relative;
    z-index: 2;
}

/* Suppress Fluent's :focus-within blue (#b3d4fc) on un-selected pills.
   After a mouse click that toggles a checkbox OFF, the underlying input
   keeps focus until the user clicks elsewhere — so Fluent's focus-within
   rule paints the span blue and reads as a stuck "active" state. Force
   the span back to white when the option isn't selected, regardless of
   focus. Selected items keep their --brand-pale via the rule above
   (specificity wins via the .ff_item_selected class). */
.hzo-form .ff-el-group.ff_list_buttons .ff-el-form-check:not(.ff_item_selected) label:focus-within > span {
    background-color: #fff !important;
}

/* Mobile: full-width stacked pills.
   Fluent's mobile media query already sets `.ff-el-form-check { display:
   block; width: 100% }` and `.ff-el-form-check label > span { display:
   block; width: 100% }` — but Fluent leaves the LABEL between them as
   inline (its CSS default), so the span's `width: 100%` resolves
   against the label's content width and pills end up shrunk to text
   width with awkward right-side gaps. Forcing the label to block fills
   the cascade. Also: Fluent's mobile span rule sets `border: 1px solid`
   with NO color, falling back to currentColor — re-specify --border so
   the outline is the expected light gray, and add a vertical rhythm gap
   between stacked options. */
@media only screen and (max-width: 768px) {
    .hzo-form .ff-el-group.ff_list_buttons .ff-el-form-check {
        margin: 0 0 8px 0 !important;
    }
    .hzo-form .ff-el-group.ff_list_buttons .ff-el-form-check:last-child {
        margin-bottom: 0 !important;
    }
    .hzo-form .ff-el-group.ff_list_buttons .ff-el-form-check-label {
        display: block !important;
        width: 100% !important;
        margin: 0 !important;
    }
    .hzo-form .ff-el-group.ff_list_buttons .ff-el-form-check-label > span {
        display: block !important;
        width: 100% !important;
        border: 1px solid var(--border) !important;
        border-radius: var(--radius-control) !important;
        box-sizing: border-box;
        text-align: center;
        padding: 12px 16px !important;
    }
    /* Selected state at mobile — re-assert box-shadow outline + bg so
       the media-query specificity doesn't drop the override. */
    .hzo-form .ff-el-group.ff_list_buttons .ff-el-form-check.ff_item_selected label > span {
        background-color: var(--brand-pale) !important;
        box-shadow: 0 0 0 1.5px var(--brand) !important;
    }
}

/* Multi-select — Fluent emits .ff_has_multi_select on <select multiple>.
   Hide the chevron (only meaningful on single-select). Height left to
   render naturally — the browser sizes the box from the option count. */
.hzo-form select.ff_has_multi_select {
    background-image: none !important;
    padding-right: 16px !important;
}

/* Single-select chevron override — Fluent's .ff-default .ff-el-form-control
   (0,0,2,0) and select.ff-el-form-control:not([size]):not([multiple]) at
   (0,0,3,1) both target this element. Mine at (0,0,3,1) needs !important
   on every property to win source-order ties (Fluent's CSS loads after
   primitives.css on the engine enqueue chain). Critically: Fluent sets
   `height: var(--fluentform-input-select-height)` which collapses the
   select to a fixed height different from text inputs (which size via
   padding). `height: auto !important` restores padding-driven sizing so
   the select matches text inputs above/below. */
.hzo-form select.ff-el-form-control:not(.ff_has_multi_select) {
    appearance: none !important;
    -webkit-appearance: none !important;
    -moz-appearance: none !important;
    height: auto !important;
    padding: 12px 44px 12px 16px !important;
    background: var(--input-bg, #fff) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231F2937' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>") no-repeat right 16px center / 18px 18px !important;
    cursor: pointer;
}


/* --- form/form.css --- */
/* Variant sheets — @imported here so the component loader, which enqueues
   only form.css for this folder, pulls them on the same request (widget
   convention; precedent: carousel.css, tooltip.css). Order: Fluent wrapper/
   layout first, then the field-level enhancement overlays. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */

/* ─── Forms (opt-in via .hzo-form wrapper) ──────────────────────
   Rules activate only inside a .hzo-form container. Targets two
   surfaces:

   1. Native HTML form elements (input/textarea/select) — also catches
      Fluent Forms' rendered output (Fluent Forms emits standard HTML
      inputs inside .ff-el-form-control wrappers; the .hzo-form wrapper
      style applies via tag-name match, no plugin-specific selectors
      required).
   2. Hozio Studio BEM classes (.hzo-form__label, .hzo-form__description,
      .hzo-form__option, .hzo-form__message--error/success,
      .hzo-form__input--error) — direct authoring + validation states.

   !important is retained on field-level rules so the styles win
   against equal-specificity plugin output (Fluent Forms, or any form
   plugin we swap to later) when source order isn't guaranteed. NOT
   carried into structural rules. */
.hzo-form { max-width: 640px; }

.hzo-form input[type="text"],
.hzo-form input[type="email"],
.hzo-form input[type="tel"],
.hzo-form input[type="number"],
.hzo-form input[type="url"],
.hzo-form input[type="password"],
.hzo-form input[type="date"],
.hzo-form textarea,
.hzo-form select {
    width: 100%;
    padding: var(--input-padding, 12px 16px) !important;
    font-family: var(--font-body) !important;
    font-size: 1rem !important;
    color: var(--text-body) !important;
    background: var(--input-bg, #fff) !important;
    border: 1px solid var(--input-border, var(--border)) !important;
    border-radius: var(--input-radius, var(--radius-input)) !important;
    line-height: var(--lh-normal);
    transition: border-color 0.15s, box-shadow 0.15s;
    box-sizing: border-box;
}

.hzo-form input:focus,
.hzo-form textarea:focus,
.hzo-form select:focus {
    outline: none;
    border-color: var(--color-focus) !important;
    box-shadow: 0 0 0 2px var(--brand-pale) !important;
}

.hzo-form input::placeholder,
.hzo-form textarea::placeholder { color: var(--color-placeholder); }

/* Validation error state — opt-in modifier class. Applied to the input
   element itself (e.g. <input class="hzo-form__input--error">). */
.hzo-form .hzo-form__input--error,
.hzo-form input.hzo-form__input--error,
.hzo-form textarea.hzo-form__input--error,
.hzo-form select.hzo-form__input--error {
    border-color: var(--error) !important;
    box-shadow: 0 0 0 2px var(--error-bg) !important;
}

/* G49: mirror the same red border + ring onto Fluent's live-validation hook.
   Fluent flags an invalid field's group with .ff-el-group.ff-el-is-error; the
   engine's own modifier never lands on plugin-rendered fields, so without this
   a Fluent form's client-side errors would show the plugin's default styling
   instead of ours. Checkboxes/radios keep their control box untouched. */
.hzo-form .ff-el-is-error input:not([type="checkbox"]):not([type="radio"]),
.hzo-form .ff-el-is-error textarea,
.hzo-form .ff-el-is-error select {
    border-color: var(--error) !important;
    box-shadow: 0 0 0 2px var(--error-bg) !important;
}
.hzo-form .ff-el-is-error .text-danger,
.hzo-form .ff-el-is-error .error { color: var(--error) !important; }

/* PF-069 — `.is-loading` must always PAINT something. form.js's submit-lock puts
   the class on whatever the form's submit control is: `.ff-btn-submit` (Fluent —
   spinner recipe in choices.css), `.hzo-btn`/`.hzo-button` (buttons.css spinner),
   or, in a hand-authored .hzo-form, a bare <button type="submit"> that matches
   NEITHER — which rendered nothing at all: the class hid no label, painted no
   spinner, and left an inert-looking button as the only feedback. Cover the
   remainder here. Own keyframe because spinner.css is gated and may be absent on
   a form-only page (same reason choices.css declares its own). */
@keyframes hzo-form-submit-spin { to { transform: rotate(360deg); } }
.hzo-form button[type="submit"].is-loading:not(.hzo-btn):not(.hzo-button):not(.ff-btn-submit) {
    color: transparent !important;
    position: relative;
    pointer-events: none;
}
.hzo-form button[type="submit"].is-loading:not(.hzo-btn):not(.hzo-button):not(.ff-btn-submit)::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 18px;
    height: 18px;
    margin: -9px 0 0 -9px;
    border-radius: 50%;
    /* NOT currentColor — the rule above sets the element's own color to
       transparent to hide the label, and a pseudo-element inherits it, so
       currentColor would paint an invisible spinner. Same reason choices.css
       names the token explicitly. */
    border: 2px solid color-mix(in srgb, var(--btn-primary-text, var(--text-on-accent)) 30%, transparent);
    border-top-color: var(--btn-primary-text, var(--text-on-accent));
    animation: hzo-form-submit-spin 0.6s linear infinite;
}
/* <input type="submit"> has no ::after to spin — a dim + progress cursor is the
   only in-element cue the element type can carry. */
.hzo-form input[type="submit"].is-loading {
    opacity: 0.6;
    cursor: progress;
    pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
    .hzo-form button[type="submit"].is-loading:not(.hzo-btn):not(.hzo-button):not(.ff-btn-submit)::after {
        animation-duration: 1.5s;
    }
}

/* Select dropdown: hide native arrow, paint a custom chevron (Hozio-style).
   Uses the `background` shorthand to override the .hzo-form input/select
   rule above (which also uses the shorthand with !important).
   Stroke color #1F2937 matches --text-heading for stronger contrast. */
.hzo-form select {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    padding-right: 44px !important;
    background: var(--input-bg, #fff) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231F2937' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>") no-repeat right 16px center / 16px 16px !important;
    cursor: pointer;
}

/* Field labels — BEM class (direct authoring). Matches the Fluent
   .ff-el-input--label label rule below so direct-authored and plugin-
   rendered forms look identical. */
.hzo-form .hzo-form__label {
    display: block;
    font-family: var(--font-body);
    font-size: 15px;
    font-weight: var(--fw-600);
    line-height: 1.4;
    color: var(--text-body);
    margin-bottom: 6px;
}

/* Field descriptions — BEM class. */
.hzo-form .hzo-form__description {
    font-size: 15px;
    color: var(--text-muted);
    margin-top: 6px;
}

/* Validation messages — BEM modifier classes (rendered by the form handler
   or hand-placed). */
.hzo-form .hzo-form__message--error   { color: var(--error); }
.hzo-form .hzo-form__message--success { color: var(--success); }

/* Radio/checkbox option row layout — BEM class. */
.hzo-form .hzo-form__option {
    display: inline-flex;
    align-items: center;
    font-size: 15px;
    color: var(--text-body);
    line-height: 1.4;
    padding: 6px 0;
    cursor: pointer;
}

/* Custom-styled checkboxes and radios.
   Hozio-style: 20px square/round, border --border, filled --brand when checked
   with white check/dot indicator. !important kept on appearance/visual
   properties because browser-default rendering + plugin form output may
   apply equal-specificity styling that source order does not guarantee
   defeating. */
.hzo-form input[type="checkbox"],
.hzo-form input[type="radio"] {
    appearance: none !important;
    -webkit-appearance: none !important;
    -moz-appearance: none !important;
    width: 20px !important;
    height: 20px !important;
    padding: 0 !important;
    margin-right: 10px !important;
    border: 1.5px solid var(--border) !important;
    background: #fff !important;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
    flex-shrink: 0;
    position: relative;
    display: inline-block;
}
.hzo-form input[type="checkbox"] { border-radius: 4px !important; }
.hzo-form input[type="radio"]    { border-radius: 50% !important; }

.hzo-form input[type="checkbox"]:hover,
.hzo-form input[type="radio"]:hover {
    border-color: var(--color-focus) !important;
}

.hzo-form input[type="checkbox"]:focus-visible,
.hzo-form input[type="radio"]:focus-visible {
    outline: none !important;
    border-color: var(--color-focus) !important;
    box-shadow: 0 0 0 3px var(--brand-pale) !important;
}

.hzo-form input[type="checkbox"]:checked,
.hzo-form input[type="radio"]:checked {
    background: var(--brand) !important;
    border-color: var(--brand) !important;
}

/* Checkmark for checked checkbox */
.hzo-form input[type="checkbox"]:checked::after {
    content: "";
    position: absolute;
    left: 5px;
    top: 1px;
    width: 6px;
    height: 11px;
    border: solid #fff;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

/* Dot for selected radio */
.hzo-form input[type="radio"]:checked::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #fff;
    transform: translate(-50%, -50%);
}


/* --- free-ship/free-ship.css --- */
/* ─── Free-Shipping Progress Bar (.hzo-free-ship) — W-134 ──────────────────
   A live-cart meter: a message line above a track whose fill grows toward the
   free-shipping threshold, flipping to an unlocked state at 100%. Flow-level
   component (no z-index / no positioning of its own) so it composes inside a
   page section, a banner slot, or the mini-cart body. Tokens only — no bare hex,
   px-where-a-token-exists, or bare motion literals (interaction-standards §9).

   The fill width is driven by --hzo-free-ship-pct (0–100), which free-ship.js
   sets from the live subtotal; the server bakes the same value into first paint,
   so the bar is correct with JS off (progressive enhancement). */

.hzo-free-ship {
	display: block;
	width: 100%;
}

/* The server-truth data node — machine-readable only, never shown. Not a live
   region, so display:none is safe (announcements route through hzo.announce). */
.hzo-free-ship__data {
	display: none !important;
}

.hzo-free-ship__message {
	margin: 0 0 var(--space-sm);
	font-size: var(--fs-100);
	font-weight: var(--fw-600);
	line-height: var(--lh-snug);
	color: var(--text-body);
}

/* The emphasised money figure reads as brand-forward once unlocked. */
.hzo-free-ship.is-unlocked .hzo-free-ship__message {
	color: var(--success-ink);
}

.hzo-free-ship__track {
	position: relative;
	width: 100%;
	height: 8px;
	background: var(--bg-alt);
	border: var(--border-1) solid var(--border);
	border-radius: var(--radius-pill);
	overflow: hidden;
}

.hzo-free-ship__fill {
	/* --hzo-free-ship-pct set by JS / first paint; 0 fallback keeps it sane. */
	width: clamp(0%, calc(var(--hzo-free-ship-pct, 0) * 1%), 100%);
	height: 100%;
	border-radius: inherit;
	background: var(--brand);
	transition: width var(--dur-4) var(--ease-out), background-color var(--dur-3) var(--ease-standard);
}

.hzo-free-ship.is-unlocked .hzo-free-ship__fill {
	background: var(--success);
}

/* ── Variant: slim — a thinner track, tighter message ──────────────────────*/
.hzo-free-ship--slim .hzo-free-ship__message {
	font-size: var(--fs-75);
	margin-bottom: var(--space-xs);
}
.hzo-free-ship--slim .hzo-free-ship__track {
	height: 4px;
}

/* ── Variant: pill — a filled card wrapper around the meter ────────────────*/
.hzo-free-ship--pill {
	padding: var(--space-smd) var(--space-md);
	background: var(--surface);
	border: var(--border-1) solid var(--border);
	border-radius: var(--radius-panel);
	box-shadow: var(--elevation-rest);
}
.hzo-free-ship--pill.is-unlocked {
	border-color: var(--success);
	background: var(--success-bg);
}

/* ── Pulse-on-unlock (JS adds .is-pulsing for ~900ms) ──────────────────────
   A one-shot celebratory pulse on the track when the threshold is crossed.
   Ambient/decorative motion → fully removed under reduced-motion (PRESERVE-5);
   the state change itself remains conveyed by color + message. */
@keyframes hzo-free-ship-pulse {
	0%   { transform: scaleX(1);    }
	40%  { transform: scaleX(1.02); }
	100% { transform: scaleX(1);    }
}

.hzo-free-ship.is-pulsing .hzo-free-ship__track {
	transform-origin: left center;
	animation: hzo-free-ship-pulse var(--dur-4) var(--ease-spring);
}

/* A soft glow ring on the whole bar as it unlocks — decorative, motion-gated. */
@keyframes hzo-free-ship-glow {
	0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--success) 40%, transparent); }
	100% { box-shadow: 0 0 0 var(--space-sm) color-mix(in srgb, var(--success) 0%, transparent); }
}
.hzo-free-ship.is-pulsing {
	border-radius: var(--radius-pill);
	animation: hzo-free-ship-glow var(--dur-4) var(--ease-out);
}

/* ── Reduced motion — stop the ambient pulse/glow AND the fill slide; the fill
   still snaps to the correct width, state still conveyed by color + text. ──*/
@media (prefers-reduced-motion: reduce) {
	.hzo-free-ship__fill {
		transition: none;
	}
	.hzo-free-ship.is-pulsing,
	.hzo-free-ship.is-pulsing .hzo-free-ship__track {
		animation: none;
	}
}

/* ── Forced colors (Windows High Contrast) — an opacity/paint-only fill
   vanishes, so express the track/fill with system colors + a visible boundary
   so progress stays perceivable (interaction-standards §12.2). ──*/
@media (forced-colors: active) {
	.hzo-free-ship__track {
		border-color: CanvasText;
	}
	.hzo-free-ship__fill {
		background: Highlight;
		forced-color-adjust: none;
	}
	.hzo-free-ship.is-unlocked .hzo-free-ship__fill {
		background: LinkText;
	}
}


/* --- hamburger/hamburger.css --- */
/* ─── Hamburger trigger (below 1250px) ─────────────────────────── */
.hzo-mobile-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    margin-left: auto;
    /* Visual right-edge alignment: the SVG has ~10px of internal whitespace
       padding to its icon, so the icon appears inset from the button's right
       edge. Pull the button -10px right so the icon's visible edge aligns
       with the container's right edge (matching the logo's left alignment). */
    margin-right: -10px;
    border: 0;
    background: transparent;
    cursor: pointer;
    color: var(--nav-mobile-trigger-color, var(--text-heading));
    flex-shrink: 0;
}
.hzo-mobile-trigger:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
@media (min-width: 1250px) { .hzo-mobile-trigger { display: none; } }


/* --- icon-button/icon-button.css --- */
/* icon-button — @tokens: --icon-button-size, --icon-button-icon-size, --icon-button-radius, --icon-button-color, --icon-button-hover-bg, --icon-button-hover-color

   The general-purpose square/round icon control: a bare <button> (or <a>) whose
   only content is an icon (svg / icon font). Sibling to the specialized
   `.hzo-close` (which is the dismiss-specific variant); use `.hzo-icon-button`
   for toolbar actions, card actions, pagination arrows, share/like toggles, etc.

   Token-only: control DIMENSIONS (box + icon size) have no semantic token in the
   scale, so they are component tokens with a literal fallback — the same dial
   pattern buttons.css uses (`var(--btn-padding-y, 14px)`). Everything else reads
   a semantic/L1 token. */
.hzo-icon-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: var(--icon-button-size, 40px);
    height: var(--icon-button-size, 40px);
    padding: 0;
    border: var(--border-1) solid transparent;
    background: transparent;
    color: var(--icon-button-color, var(--text-muted));
    border-radius: var(--icon-button-radius, var(--radius-control));
    cursor: pointer;
    transition: background var(--transition-base) ease, color var(--transition-base) ease, border-color var(--transition-base) ease;
}
.hzo-icon-button > svg,
.hzo-icon-button > .hzo-icon {
    display: block;
    width: var(--icon-button-icon-size, 20px);
    height: var(--icon-button-icon-size, 20px);
}

/* Hover / active — ghost fill on the alt surface with heading-ink foreground. */
.hzo-icon-button:hover,
.hzo-icon-button.is-active {
    background: var(--icon-button-hover-bg, var(--bg-alt));
    color: var(--icon-button-hover-color, var(--text-heading));
}

/* Variants — solid (brand) and outline. Default (no modifier) is the ghost above. */
.hzo-icon-button--solid {
    background: var(--brand);
    color: var(--text-on-brand);
    border-color: var(--brand);
}
.hzo-icon-button--solid:hover,
.hzo-icon-button--solid.is-active {
    background: var(--brand-hover-on-light);
    color: var(--text-on-brand);
    border-color: var(--brand-hover-on-light);
}
.hzo-icon-button--outline {
    border-color: var(--border);
    color: var(--text-body);
}
.hzo-icon-button--outline:hover,
.hzo-icon-button--outline.is-active {
    border-color: var(--brand);
    color: var(--brand-text);
    background: transparent;
}

/* Sizes — small / large dial the box + icon together. */
.hzo-icon-button--sm { --icon-button-size: 32px; --icon-button-icon-size: 16px; }
.hzo-icon-button--lg { --icon-button-size: 48px; --icon-button-icon-size: 24px; }

/* A11y — required. Focus ring; disabled state. Reduced-motion handled globally
   by components/_base/a11y.css. */
.hzo-icon-button:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}
.hzo-icon-button[disabled],
.hzo-icon-button[aria-disabled="true"],
.hzo-icon-button.is-disabled {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-icon-button { transition: none; }
}


/* --- icon-grid/icon-grid.css --- */
/* icon-grid — @tokens: --icon-grid-min, --icon-grid-icon-size

   A responsive grid of icon + title + text cells (services / features / benefits).
   A CONTENT BLOCK dropped INSIDE a `.hzo-section` > `.hzo-container` (it is NOT
   full-bleed — the section owns the vertical rhythm + background). Primitive:
   CSS-only, not interactive (carries no control, so no focus/disabled idioms).

   Token-only: the responsive column floor + the icon box have no dedicated
   semantic role in the scale, so they're component tokens with a literal
   fallback (the icon-button.css dial pattern). Everything else reads a
   semantic/L1 token — so an archetype re-themes the ink + accent from :root. */
.hzo-icon-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(var(--icon-grid-min, 240px), 1fr));
    gap: var(--space-xl);
}

/* Each cell — a stacked flex column: icon over title over text. */
.hzo-icon-grid__item {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}

/* Accent-tinted glyph in a fixed box so ragged svg viewBoxes align on the grid.
   flex: 0 0 auto keeps it from stretching in the column. */
.hzo-icon-grid__icon {
    flex: 0 0 auto;
    width: var(--icon-grid-icon-size, 40px);
    height: var(--icon-grid-icon-size, 40px);
    color: var(--accent);
}
.hzo-icon-grid__icon > svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* Cell heading — heading ink, semibold, a heading-scale size. */
.hzo-icon-grid__title {
    margin: 0;
    font-size: var(--fs-400);
    font-weight: var(--fw-600);
    line-height: var(--lh-snug);
    color: var(--text-heading);
}

/* Supporting copy — muted ink, comfortable leading. */
.hzo-icon-grid__text {
    margin: 0;
    font-size: var(--fs-200);
    line-height: var(--lh-normal);
    color: var(--text-muted);
}

/* Center modifier — centers each cell's contents + its text block. */
.hzo-icon-grid--center .hzo-icon-grid__item {
    align-items: center;
    text-align: center;
}


/* --- icon-list/icon-list.css --- */
/* icon-list — @tokens: --icon-list-icon-size

   An unordered list with a leading icon per row (feature bullets, spec sheets,
   "what's included" / checklists). The list resets its native marker and lays
   the rows out on a grid; each row is a flex line with a fixed-size leading
   icon and top-aligned text. The `--checklist` modifier recolours the icon to
   the success role for "✓ included" lists.

   Token-only: gap/colour read the semantic scale; the fixed icon box has no
   semantic token, so it is a component token with a literal fallback (the
   icon-button.css dial pattern). */
.hzo-icon-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: grid;
    gap: var(--space-sm);
    color: var(--text-body);
}

.hzo-icon-list__item {
    display: flex;
    gap: var(--space-sm);
    align-items: flex-start;
}

/* Leading icon — accent-tinted, fixed square, never shrinks under long text. */
.hzo-icon-list__icon {
    flex: 0 0 auto;
    width: var(--icon-list-icon-size, 20px);
    height: var(--icon-list-icon-size, 20px);
    color: var(--accent);
}

/* Checklist variant — swap the accent tint for the success (✓ done) role. */
.hzo-icon-list--checklist .hzo-icon-list__icon {
    color: var(--success);
}


/* --- img/img.css --- */
/* ─── LQIP Progressive Image (.hzo-img) ────────────────────────────────────
   W-023 · closes audit gap G51 (missing aspect/dimensions → CLS). The fleet's
   photo primitive: an aspect-ratio box that reserves EXACT layout space (zero
   CLS) with a tiny LQIP placeholder that blurs up to the full image on decode.

   FAIL-VISIBLE BY CONSTRUCTION (mirrors the reveal orchestrator, W-020): the
   armed leg that hides the full image (opacity:0) applies ONLY under the
   JS-set `html.hzo-img-ready` flag. No JS, a thrown init, reduced-motion,
   forced-colors, or print → the flag is absent → the FULL image is fully
   visible and the whole widget degrades to a plain <img> in a reserved box.
   The box + placeholder are server-rendered and paint on the first frame with
   zero layout dependency on JS — the reserved box IS the sub-100ms guarantee.

   Single-cell grid overlay stacks the LQIP under the full image WITHOUT a
   z-index (§9.3): grid-area:1/1 + DOM order (LQIP first, full second) is the
   paint order. No new z-index token is introduced. */

.hzo-img {
    display: grid;
    width: 100%;
    max-width: 100%;
    /* Reserved box → zero CLS. `auto` (unset aspect) falls back to the intrinsic
       ratio the width/height attrs on .hzo-img__full provide. */
    aspect-ratio: var(--hzo-img-aspect, auto);
    overflow: hidden;
    /* A neutral frame shows through until the LQIP/full paints (and stays as the
       error frame). Token only — no hardcoded color. */
    background-color: var(--surface-2);
}

/* Both layers occupy the one grid cell and fill it — object-fit does the crop. */
.hzo-img > .hzo-img__lqip,
.hzo-img > .hzo-img__full {
    grid-area: 1 / 1;
    display: block;
    width: 100%;
    height: 100%;
    margin: 0;
    object-fit: var(--hzo-img-fit, cover);
    object-position: var(--hzo-img-pos, center);
}

/* ── The full image. Default opacity:1 so NO-JS / reduced-motion / forced-colors
      / print all show it. The fade is declared here so the armed→revealed
      transition (opacity 0→1) animates once the armed leg stops matching. ── */
.hzo-img__full {
    opacity: 1;
    transition: opacity var(--hzo-img-fade, var(--dur-3)) var(--ease-out);
}

/* ── The LQIP placeholder (decorative, aria-hidden in markup). ── */
.hzo-img__lqip {
    opacity: 1;
    transition: opacity var(--hzo-img-fade, var(--dur-3)) var(--ease-out);
}
/* base64 / blurhash: a tiny bitmap scaled up + blurred. The slight scale hides
   the blur's soft edges against the box. */
.hzo-img__lqip--blur {
    filter: blur(var(--hzo-img-blur, 12px));
    transform: scale(1.04);
}
/* color: a flat dominant-color wash (no bitmap). */
.hzo-img__lqip--color {
    background-color: var(--hzo-img-lqip-color, var(--surface-2));
}

/* ═══ Armed (hidden) leg — ONLY under the JS-ready flag ════════════════════
   Before decode, hide the full image so the LQIP shows through; img.js flips
   .hzo-img--loading → .hzo-img--loaded on decode, this selector stops matching,
   and the full image fades in over --hzo-img-fade. */
.hzo-img-ready .hzo-img--loading .hzo-img__full {
    opacity: 0;
}

/* Decoded: the full image is visible (default opacity:1 via the rule above) and
   the LQIP fades out beneath it, then is inert. */
.hzo-img--loaded .hzo-img__lqip {
    opacity: 0;
}

/* ═══ Error state — reconcile the optimistic blur to a real fallback ════════
   img.js sets .hzo-img--error on the <img> `error` event (or a never-resolving
   decode watchdog): drop the frozen blur, keep the neutral frame, and show the
   alt as visible text so a broken source is never a silent empty box. */
.hzo-img--error .hzo-img__lqip {
    display: none;
}
.hzo-img--error .hzo-img__full {
    opacity: 0; /* the broken <img> icon is suppressed; the fallback text carries meaning */
}
.hzo-img__fallback {
    grid-area: 1 / 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-md);
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-100);
    line-height: var(--lh-snug);
}

/* ═══ Fail-visible environment branches (spec §12) ═════════════════════════ */

/* reduced-motion [PRESERVE-5 · G13/G60]: the blur-up cross-fade is AMBIENT, so
   it fully STOPS — the full image appears instantly on decode, never stranded
   at opacity:0. (img.js also skips the ready flag under reduce; this is the CSS
   belt to its suspenders.) */
@media (prefers-reduced-motion: reduce) {
    .hzo-img__full,
    .hzo-img__lqip {
        transition: none !important;
    }
    .hzo-img--loading .hzo-img__full {
        opacity: 1 !important;
    }
}

/* forced-colors / High Contrast [§12.2]: opacity/blur intermediates are
   unreliable, so show the full image instantly and drop the decorative blur.
   The error frame uses SYSTEM colors (not opacity) so it stays visible. */
@media (forced-colors: active) {
    .hzo-img--loading .hzo-img__full {
        opacity: 1 !important;
    }
    .hzo-img__lqip--blur {
        display: none;
    }
    .hzo-img--error {
        background-color: Canvas;
        border: var(--border-1) solid GrayText;
    }
    .hzo-img--error .hzo-img__fallback {
        color: CanvasText;
    }
}

/* @media print [G13 — the reveal-hidden-content culprit class, applied to the
      decode-fade]: a printed estimate/service page must NEVER show a blurred
      thumbnail. Force the full image visible and hide the placeholder. The
      beforeprint JS handler additionally flushes any still-loading instance. */
@media print {
    .hzo-img__full {
        opacity: 1 !important;
    }
    .hzo-img__lqip {
        display: none !important;
    }
}


/* --- input-group/input-group.css --- */
/* ─── Input group (input + button) ────────────────────────────────────
   Joins a text input + a .hzo-btn into one control (header search, footer
   newsletter). Use STANDALONE — not inside .hzo-form, which stacks full-width
   fields; the group styles its own input. */
.hzo-input-group { display: flex; width: 100%; max-width: 480px; }
.hzo-input-group > input {
    flex: 1;
    min-width: 0;
    padding: var(--input-padding, 13px 16px);
    font-family: var(--font-body);
    font-size: 1rem;
    color: var(--text-body);
    background: var(--input-bg, #fff);
    border: 1px solid var(--input-border, var(--border));
    border-right: 0;
    border-radius: var(--input-radius, var(--radius-input)) 0 0 var(--input-radius, var(--radius-input));
}
.hzo-input-group > input:focus {
    outline: none;
    border-color: var(--color-focus);
    box-shadow: 0 0 0 2px var(--brand-pale);
    position: relative;
    z-index: 1;
}
.hzo-input-group > input::placeholder { color: var(--color-placeholder); }
.hzo-input-group > .hzo-btn {
    flex-shrink: 0;
    border-radius: 0 var(--btn-radius, var(--radius-input)) var(--btn-radius, var(--radius-input)) 0;
}
/* The base mobile rule sets non-inline buttons to width:100%, which would crush
   the input in this flex row. Keep input-group buttons intrinsic on mobile —
   specificity (0,0,5,0) beats the base rule (0,0,4,0) regardless of source order. */
@media (max-width: 767px) {
    .hzo-input-group > .hzo-btn:not(.hzo-btn--inline):not(.hzo-btn--icon):not(.hzo-btn--block) { width: auto; }
}


/* --- kbd/kbd.css --- */
/* kbd — @tokens: (none — all semantic/L1)

   An inline keyboard-key hint (`<kbd>Ctrl</kbd>`) — the little raised key cap
   used in docs / shortcut lists / tooltips. A thicker BOTTOM border than the
   other sides gives the key-depth (pressable-cap) look. Static; no interaction.

   Token-only: type/spacing/surface/border all read the semantic scale;
   monospace is a generic family keyword (not an aesthetic literal). */
.hzo-kbd {
    display: inline-block;
    font-family: monospace;
    font-size: var(--fs-50);
    line-height: var(--lh-none);
    padding: var(--space-2xs) var(--space-xs);
    background: var(--bg-alt);
    color: var(--text-heading);
    border: var(--border-1) solid var(--border);
    border-bottom-width: var(--border-2);
    border-radius: var(--radius-control);
}


/* --- key-value-row/key-value-row.css --- */
/* key-value-row — @tokens: (none — all values map to L1/L2 tokens)

   Flexible label→value rows for specs / details / summary panels. Distinct from
   `definition-list` (which is <dl>/dt/dd semantics): this is a flex spec-row layout
   — key on the left, value pushed to the right, a hairline rule between rows.
   Root class = folder name per the authoring standard, so the container is
   `.hzo-key-value-row` and each pair is a `__row` (BEM avoids the flat-sibling
   `.hzo-key-value-row-row` drift).

   Token-only: every border / spacing / weight / colour reads a semantic or L1
   token directly, so no component tokens are needed. Static/structural primitive
   — nothing is focusable, so no focus/disabled a11y idioms. */
.hzo-key-value-row {
    font-size: var(--fs-200);
    line-height: var(--lh-normal);
    color: var(--text-body);
}

/* Row — key/value on a baseline, value pushed to the far edge, hairline below. */
.hzo-key-value-row__row {
    display: flex;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-sm) 0;
    border-bottom: var(--border-1) solid var(--border);
}
.hzo-key-value-row__row:last-child {
    border-bottom: 0;
}

/* Key — muted label ink; never shrinks below its content. */
.hzo-key-value-row__key {
    color: var(--text-muted);
    flex-shrink: 0;
}

/* Value — heading ink, semibold, right-aligned so numbers/values line up. */
.hzo-key-value-row__value {
    color: var(--text-heading);
    font-weight: var(--fw-600);
    text-align: right;
}

/* Plain — drop the row rules for a borderless list (padding rhythm kept). */
.hzo-key-value-row--plain .hzo-key-value-row__row {
    border-bottom: 0;
}


/* --- list-group/list-group.css --- */
/* list-group — @tokens: (none — all semantic/L1)

   A bordered, divided vertical list (Bootstrap list-group idiom): settings
   rows, nav lists, summary line-items. The panel owns one outer border +
   radius and clips its corners; each item carries a bottom divider (the last
   one drops it). The `--interactive` modifier turns items into hoverable /
   focusable rows (links or buttons); the base list is static text.

   Token-only: border/radius/spacing/colour all read the semantic scale. */
.hzo-list-group {
    display: flex;
    flex-direction: column;
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-panel);
    overflow: hidden;
    font-size: var(--fs-200);
    line-height: var(--lh-normal);
}

.hzo-list-group__item {
    padding: var(--space-md);
    border-bottom: var(--border-1) solid var(--border);
    color: var(--text-body);
}
.hzo-list-group__item:last-child { border-bottom: 0; }

/* Selected/current row — brand fill with its paired on-brand ink. */
.hzo-list-group__item.is-active {
    background: var(--brand);
    color: var(--text-on-brand);
}

/* Interactive modifier — items become hoverable / focusable controls.
   Guard the hover fill transition behind reduced-motion. */
.hzo-list-group--interactive .hzo-list-group__item {
    cursor: pointer;
    transition: background var(--transition-base) ease, color var(--transition-base) ease;
}
.hzo-list-group--interactive .hzo-list-group__item:hover {
    background: var(--bg-alt);
}
/* A11y — focus ring + disabled idiom on the interactive rows. */
.hzo-list-group--interactive .hzo-list-group__item:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}
.hzo-list-group--interactive .hzo-list-group__item[disabled],
.hzo-list-group--interactive .hzo-list-group__item[aria-disabled="true"],
.hzo-list-group--interactive .hzo-list-group__item.is-disabled {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-list-group--interactive .hzo-list-group__item { transition: none; }
}


/* --- load-more/load-more.css --- */
/* load-more — @tokens: --load-more-gap */
/* ─── Load more ───────────────────────────────────────────────────────
   A centered "Load more" trigger + a count line. The button is a composed
   `.hzo-btn` and reuses the existing `.hzo-btn.is-loading` spinner — this
   file styles ONLY the wrapper + count and never touches `.hzo-btn` (its
   focus ring + disabled state are carried by the button component). The
   actual result-append is app-level; the JS just toggles the loading state
   and emits the integration event. */
.hzo-load-more {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--load-more-gap, var(--space-sm));
    margin-top: var(--space-lg);
    text-align: center;
}

.hzo-load-more__count {
    font-size: var(--fs-75);
    color: var(--text-muted);
    margin: 0;
}


/* --- loading-overlay/loading-overlay.css --- */
/* loading-overlay — @tokens: --space-xs, --surface, --z-overlay, --z-modal, --transition-base, --fs-50, --text-body */
/* Async-lock veil that covers a position:relative host (a card/section) and centers a composed
   .hzo-spinner. The host MUST be position:relative — or use the --fixed modifier for a
   full-viewport lock. Styles ONLY its own classes; never the composed .hzo-spinner. */
.hzo-loading-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    background: color-mix(in srgb, var(--surface, #fff) 65%, transparent);
    z-index: var(--z-overlay);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--transition-base) ease, visibility var(--transition-base) ease;
}
.hzo-loading-overlay.is-active {
    opacity: 1;
    visibility: visible;
}
.hzo-loading-overlay__label {
    font-size: var(--fs-50);
    color: var(--text-body, currentColor);
}

/* Full-viewport variant — pins to the viewport and stacks at modal level. */
.hzo-loading-overlay--fixed {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
}

@media (prefers-reduced-motion: reduce) {
    .hzo-loading-overlay { transition: none; }
}


/* --- logo/logo.css --- */
/* ─── Logo ─────────────────────────────────────────────────────────── */
.hzo-site-logo {
    flex-shrink: 1;
    min-width: 0;
    font-family: var(--font-heading);
    font-size: 22px;
    font-weight: var(--fw-700, 700);
    color: var(--brand-text);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
}
.hzo-site-logo img { height: 40px; width: auto; max-width: 260px; display: block; }
@media (min-width: 1250px) {
    .hzo-site-logo img { height: 46px; max-width: 300px; }
}


/* --- mega-menu/mega-menu-wide.css --- */
/* ─── Wide mega menu (opt-in via the WP menu UI — see [hzo_nav]) ──────
   Structure comes from the menu itself: 3rd-level items make column groups,
   a child with menu-item class "hzo-mega-featured" becomes a photo card,
   parent class "hzo-mega-wide" forces the wide panel for a flat list. The
   panel spans the nav bar (the parent li goes position:static so the panel
   anchors to .hzo-site-nav). --mega-cols is set inline by the shortcode. */
.hzo-site-nav { position: relative; }
.hzo-nav-item--static { position: static; }
.hzo-mega-menu--wide {
    left: 50%;
    right: auto;
    transform: translateX(-50%) translateY(-8px);
    width: max-content;
    max-width: min(92vw, 1080px);
    min-width: 0;
    display: grid;
    /* min column width so link labels never wrap ("On-Page SEO" stays one line);
       the panel sizes to content (max-content) and centres under the nav. */
    grid-template-columns: repeat(var(--mega-cols, 3), minmax(160px, 1fr));
    gap: var(--space-lg);
    padding: var(--space-lg);
}
.hzo-nav-item:hover > .hzo-mega-menu--wide,
.hzo-nav-item.is-open > .hzo-mega-menu--wide { transform: translateX(-50%) translateY(0); }
.hzo-mega-menu--wide .hzo-mega-menu__link {
    padding: 10px var(--space-smd);
    border-radius: var(--radius-control);
    white-space: normal;
}
/* Column group heading — small-caps label; a link when the group page exists. */
.hzo-mega-menu__group-title {
    display: block;
    margin: 0 0 var(--space-xs);
    padding: 10px var(--space-smd) 0;
    font-family: var(--font-body);
    font-size: var(--fs-50);
    font-weight: var(--fw-600, 600);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted) !important;
    text-decoration: none;
}
a.hzo-mega-menu__group-title:hover { color: var(--nav-link-hover) !important; }
/* Optional one-line description under a dropdown link (WP menu-item
   "Description" field). */
.hzo-mega-menu__desc {
    display: block;
    margin-top: 2px;
    font-size: 12.5px;
    font-weight: var(--fw-400, 400);
    color: var(--text-muted);
    white-space: normal;
}
/* A lone long flat list in a wide panel flows into columns. */
.hzo-mega-menu__list--columns { columns: 2; column-gap: var(--space-lg); }
.hzo-mega-menu__list--columns li { break-inside: avoid; }

/* Featured photo card — linked page's featured image + bottom scrim + label.
   main.js crossfades the image/label to whichever [data-hzo-mega-img] link
   is hovered/focused (pointer-only enhancement; card itself stays a link). */
.hzo-mega-cta {
    position: relative;
    display: flex;
    align-items: flex-end;
    min-height: 200px;
    border-radius: var(--radius-control);
    overflow: hidden;
    padding: var(--space-md);
    text-decoration: none;
    background-image: var(--hzo-mega-bg, linear-gradient(135deg, var(--brand), var(--brand-deep)));
    background-size: cover;
    background-position: center;
    transition: opacity 150ms ease;
}
.hzo-mega-cta::before {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to top,
        color-mix(in srgb, var(--brand-deep) 85%, transparent) 0%,
        color-mix(in srgb, var(--brand-deep) 35%, transparent) 45%,
        transparent 75%);
}
.hzo-mega-cta.is-fading { opacity: 0.35; }
.hzo-mega-cta:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.hzo-mega-cta__body { position: relative; display: block; }
.hzo-mega-cta__label {
    display: block;
    font-family: var(--font-heading);
    font-size: 1.15rem;
    font-weight: var(--fw-700, 700);
    line-height: var(--lh-snug);
    color: #fff !important;
}
.hzo-mega-cta__desc {
    display: block;
    margin-top: 2px;
    font-size: var(--fs-75);
    color: rgba(255, 255, 255, 0.85);
}
@media (prefers-reduced-motion: reduce) {
    .hzo-mega-cta { transition: none; }
}


/* --- mega-menu/mega-menu.css --- */
/* Aggregator sheet — the component loader (includes/component-loader.php)
   enqueues ONLY <name>.css per folder, so the wide-panel variant sheet is
   @imported here to ride the same request. This is its ONLY front-end load
   path: on the front end hzostudio_enqueue_components() REPLACES the whole-file
   primitives.css enqueue, so primitives.css's own @import of mega-menu-wide.css
   loads in the block-editor context only. Without this @import the wide mega
   menu (featured card, column grid) renders UNSTYLED on the front end. (Same
   fix pattern as footer.css @importing footer-typography + footer-chrome.)
   @imports must precede all rules — keep this first. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */

/* ─── Dropdown (mega menu) ─────────────────────────────────────────
   Centered under the parent nav item. Fades + slides in on hover/open.
   Uses opacity+visibility+transform so it animates cleanly both ways. */
.hzo-mega-menu {
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%) translateY(-8px);
    min-width: 220px;
    background: var(--nav-dropdown-bg, var(--surface, #fff));
    border-top: var(--border-2) solid var(--accent);
    border-radius: 0 0 var(--radius-control) var(--radius-control);
    box-shadow: var(--nav-dropdown-shadow, 0 12px 40px rgba(0,0,0,0.10), 0 2px 8px rgba(0,0,0,0.04));
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    overflow: hidden; /* clip link hover backgrounds to the rounded corners */
    transition: opacity 200ms ease, transform 200ms ease, visibility 200ms;
}
.hzo-nav-item:hover > .hzo-mega-menu,
.hzo-nav-item.is-open > .hzo-mega-menu {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: translateX(-50%) translateY(0);
}

.hzo-mega-menu__list { list-style: none; margin: 0; padding: 0; }
/* !important on color: fights a:visited. Matching :visited rule at bottom-of-file. */
.hzo-mega-menu__link {
    display: block;
    padding: 14px var(--space-lg);
    font-family: var(--font-body);
    font-size: var(--fs-100);
    font-weight: var(--fw-500, 500);
    color: var(--nav-link) !important;
    text-decoration: none;
    white-space: nowrap;
    transition: color 200ms ease, background-color 200ms ease;
}
.hzo-mega-menu__link:hover {
    color: var(--nav-link-hover) !important;
    background: var(--nav-dropdown-link-hover-bg, var(--bg-alt));
}
/* INSET focus ring (offset -2px) so the ring hugs inside the dropdown row rather
   than painting outside/clipping the panel edge (the global base.css ring is
   +2px outset). Restored from OURS during the unified merge. */
.hzo-mega-menu__link:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }

/* Reduced-motion: kill the dropdown fade/slide + link color transitions (belt-
   and-braces on top of mega-menu-wide.css's own .hzo-mega-cta kill — per the
   convention the aggregator ends with a transition-kill; mirrors footer.css). */
@media (prefers-reduced-motion: reduce) {
    .hzo-mega-menu,
    .hzo-mega-menu__link { transition: none; }
}


/* --- metric-tile/metric-tile.css --- */
/* metric-tile — @tokens: --metric-tile-gap, --metric-tile-padding, --metric-tile-radius

   A KPI tile: one big number + a label + an optional signed delta. A small
   elevated panel — sibling to `.hzo-card` but purpose-built for a single metric
   (dashboards, stat rows, pricing highlights). Primitive: CSS-only, not
   interactive (carries no control, so no focus/disabled idioms).

   Token-only: the panel surface + ink read semantic tokens so a dark archetype
   re-themes it from :root. The internal stack gap + padding have no dedicated
   semantic role, so they're component tokens with a literal-token fallback (the
   icon-button.css dial pattern). The value uses --font-number so the metric
   renders in the numeral face. */
.hzo-metric-tile {
    display: flex;
    flex-direction: column;
    gap: var(--metric-tile-gap, var(--space-sm));
    padding: var(--metric-tile-padding, var(--space-lg));
    background: var(--surface);
    border: var(--border-1) solid var(--border);
    border-radius: var(--metric-tile-radius, var(--radius-panel));
    box-shadow: var(--shadow-sm);   /* soft rest elevation — matches card default */
}

/* The headline metric — largest, heaviest, numeral face, tight leading. */
.hzo-metric-tile__value {
    font-family: var(--font-number);
    font-size: var(--fs-700);
    font-weight: var(--fw-700);
    line-height: var(--lh-none);
    color: var(--text-heading);
}

/* The caption paired with the value — small, muted, tracked-out caps. */
.hzo-metric-tile__label {
    font-size: var(--fs-100);
    line-height: var(--lh-snug);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Optional change indicator. Base is muted; direction modifiers recolor it with
   the matching state colour. Pair the sign/arrow in markup so meaning never
   rides on colour alone. */
.hzo-metric-tile__delta {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    font-size: var(--fs-100);
    font-weight: var(--fw-600);
    line-height: var(--lh-none);
    color: var(--text-muted);
}
.hzo-metric-tile__delta--positive { color: var(--success); }
.hzo-metric-tile__delta--negative { color: var(--error); }


/* --- mini-cart/mini-cart.css --- */
/* ─── Mini-cart ────────────────────────────────────────────────────────
   A .hzo-drawer COMPOSITION (it is a drawer — all open/close/trap/lock/Esc/
   focus-return machinery is reused). Only the line-item layout + totals are new
   here. Empty state reuses .hzo-empty; remove reuses the tag-removal focus-move
   pattern (mini-cart.js, Phase-2 widget system). Zero new tokens.
   Contract: component-patterns §46. */
.hzo-minicart__items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-lg); }
.hzo-minicart__item {
    display: grid;
    grid-template-columns: 56px 1fr auto;
    gap: var(--space-md);
    align-items: start;
}
.hzo-minicart__thumb {
    width: 56px;
    height: 56px;
    border-radius: var(--radius-control);
    overflow: hidden;
    background: var(--bg-alt);
    flex-shrink: 0;
}
.hzo-minicart__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hzo-minicart__info { min-width: 0; display: flex; flex-direction: column; gap: var(--space-sm); }
.hzo-minicart__title { margin: 0; font-size: var(--fs-100); font-weight: 600; color: var(--text-heading); line-height: 1.3; }
/* The live cart fragment (includes/wc-ajax.php) wraps the title in <a>; without
   this the global `a` color/underline would restyle every product title. */
.hzo-minicart__title a { color: inherit; text-decoration: none; background-image: none; }
.hzo-minicart__title a:hover { text-decoration: underline; }
.hzo-minicart__qty {
    display: inline-flex;
    align-items: center;
    width: max-content;
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
}
.hzo-minicart__step {
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    background: transparent;
    color: var(--text-body);
    font-size: var(--fs-200);
    line-height: var(--lh-none);
    cursor: pointer;
}
.hzo-minicart__step:hover { color: var(--brand-text); }
.hzo-minicart__step:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
.hzo-minicart__step[disabled], .hzo-minicart__step[aria-disabled="true"] { opacity: var(--opacity-disabled); pointer-events: none; }
.hzo-minicart__count { min-width: 30px; text-align: center; font-size: var(--fs-100); font-variant-numeric: tabular-nums; }
.hzo-minicart__line { display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-sm); }
.hzo-minicart__linetotal { font-size: var(--fs-100); font-weight: 600; color: var(--text-heading); font-variant-numeric: tabular-nums; }
.hzo-minicart__remove {
    width: 24px;
    height: 24px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: 50%;
    cursor: pointer;
    transition: background var(--transition-base) ease, color var(--transition-base) ease;
}
.hzo-minicart__remove:hover { background: var(--bg-alt); color: var(--text-heading); }
.hzo-minicart__remove:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 1px; }
.hzo-minicart__remove > svg { width: 14px; height: 14px; display: block; }
/* Footer overrides the drawer's row footer → stacked subtotal + full-width CTA. */
.hzo-minicart__footer { flex-direction: column; align-items: stretch; gap: var(--space-md); }
.hzo-minicart__subtotal { display: flex; justify-content: space-between; align-items: baseline; font-size: 15px; color: var(--text-body); }
.hzo-minicart__subtotal-value { font-size: var(--fs-300); font-weight: 700; color: var(--text-heading); font-family: var(--font-number); }
/* These elements set display: flex, which would beat the UA [hidden] rule — restore
   it so the JS empty/footer/list toggles actually hide (specificity 0,0,2,0 > 0,0,1,0). */
.hzo-minicart__items[hidden],
.hzo-minicart__empty[hidden],
.hzo-minicart__footer[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .hzo-minicart__remove { transition: none; } }


/* --- mobile-menu/mobile-menu.css --- */
/* ─── Mobile menu overlay (backdrop) ──────────────────────────── */
.hzo-mobile-menu {
    position: fixed;
    inset: 0;
    background: var(--nav-mobile-overlay-bg, rgba(0, 0, 0, 0.5));
    z-index: var(--z-overlay);
    opacity: 0;
    visibility: hidden;
    overflow: hidden; /* clip the translateX'd panel so it can't cause horizontal scroll */
    transition: opacity 300ms ease, visibility 300ms;
}
.hzo-mobile-menu.is-open { opacity: 1; visibility: visible; }

/* Mobile menu panel (slides from the right) */
.hzo-mobile-menu__panel {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    max-width: 360px;
    background: var(--nav-mobile-panel-bg, var(--surface, #fff));
    display: flex;
    flex-direction: column;
    transform: translateX(100%);
    transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}
.hzo-mobile-menu.is-open .hzo-mobile-menu__panel { transform: translateX(0); }

/* Sidebar header: logo + close */
.hzo-mobile-menu__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-md) var(--space-lg);
    border-bottom: var(--border-1) solid var(--nav-mobile-divider-color, var(--border));
    flex-shrink: 0;
    min-height: 60px;
}
.hzo-mobile-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
    color: var(--text-body);
    flex-shrink: 0;
}
.hzo-mobile-close:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

/* Sidebar nav */
.hzo-mobile-nav {
    flex: 1;
    padding: var(--space-xs) 0;
    overflow-y: auto;
}

/* Base mobile menu link — context wrappers (.hzo-mobile-simple, .hzo-mobile-panel)
   layer padding + background on top; this rule guarantees color + no underline
   even when the class is used standalone. */
.hzo-mobile-menu__link,
.hzo-mobile-menu__link:visited {
    color: var(--text-body);
    text-decoration: none;
    display: block;
    font-weight: var(--fw-600, 600);
}
.hzo-mobile-menu__link:hover,
.hzo-mobile-menu__link:visited:hover { color: var(--nav-link-hover); }
.hzo-mobile-menu__link:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }

/* Simple (non-accordion) row — a single link.
   !important on color: fights a:visited from per-client main.css. */
.hzo-mobile-simple { border-bottom: var(--border-1) solid var(--nav-mobile-divider-color, var(--border)); }
.hzo-mobile-simple > .hzo-mobile-menu__link {
    display: block;
    padding: var(--space-md) var(--space-lg);
    font-family: var(--font-body);
    font-weight: var(--fw-600, 600);
    font-size: 15px;
    color: var(--nav-mobile-link-color, var(--text-body)) !important;
    text-decoration: none;
    transition: color 200ms ease;
}
.hzo-mobile-simple > .hzo-mobile-menu__link:hover { color: var(--nav-link-hover) !important; }

/* Accordion row (nav item with sub-items) */
.hzo-mobile-accordion { border-bottom: var(--border-1) solid var(--nav-mobile-divider-color, var(--border)); }
.hzo-mobile-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: var(--space-md) var(--space-lg);
    border: 0;
    background: transparent;
    font-family: var(--font-body);
    font-weight: var(--fw-600, 600);
    font-size: 15px;
    color: var(--text-body);
    cursor: pointer;
    text-align: left;
}
.hzo-mobile-accordion.is-expanded > .hzo-mobile-toggle { color: var(--nav-link-hover); }
.hzo-mobile-toggle:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }

.hzo-mobile-chevron {
    flex-shrink: 0;
    display: block;
    opacity: 0.4;
    margin-left: var(--space-smd);
    transition: transform 300ms ease, opacity 200ms ease;
}
.hzo-mobile-accordion.is-expanded > .hzo-mobile-toggle > .hzo-mobile-chevron {
    transform: rotate(180deg);
    opacity: 0.7;
}

/* Accordion collapsible panel */
.hzo-mobile-panel {
    max-height: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    transition: max-height 300ms ease;
}
/* 2400px (not 600) so an expanded nested sub-accordion — or a long flat
   service list — still fits without silent clipping. The overlay itself
   scrolls, so the cap only needs to exceed real content heights. */
.hzo-mobile-accordion.is-expanded > .hzo-mobile-panel { max-height: 2400px; }

/* Nested sub-accordion (3rd menu level — mirrors the desktop column groups).
   main.js already binds .hzo-mobile-subtoggle. */
.hzo-mobile-subaccordion { border-bottom: var(--border-1) solid var(--nav-mobile-divider-color, var(--border)); }
.hzo-mobile-subaccordion:last-child { border-bottom: 0; }
.hzo-mobile-subtoggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: var(--space-smd) var(--space-lg) var(--space-smd) var(--space-lg-plus);
    background: transparent;
    border: 0;
    font-family: var(--font-body);
    font-size: var(--fs-100);
    font-weight: var(--fw-600, 600);
    color: var(--text-body);
    cursor: pointer;
    text-align: left;
}
.hzo-mobile-subaccordion.is-expanded > .hzo-mobile-subtoggle { color: var(--nav-link-hover); }
.hzo-mobile-subtoggle:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
.hzo-mobile-subaccordion.is-expanded > .hzo-mobile-subtoggle > .hzo-mobile-chevron {
    transform: rotate(180deg);
    opacity: 0.7;
}
.hzo-mobile-subaccordion.is-expanded > .hzo-mobile-panel { max-height: 600px; }
.hzo-mobile-subaccordion .hzo-mobile-panel > .hzo-mobile-menu__link { padding-left: 44px; }

/* Featured tile (child with menu-item class hzo-mega-featured) — thumbnail +
   label row; the mobile counterpart of the desktop photo card. */
.hzo-mobile-featured {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-smd) var(--space-lg) var(--space-smd) var(--space-lg-plus);
    font-size: var(--fs-100);
    font-weight: var(--fw-600, 600);
    color: var(--text-body);
    text-decoration: none;
    border-bottom: var(--border-1) solid var(--nav-mobile-divider-color, var(--border));
}
.hzo-mobile-featured img {
    width: 56px;
    height: 56px;
    object-fit: cover;
    border-radius: var(--radius-control);
    flex-shrink: 0;
}
.hzo-mobile-featured:hover { color: var(--nav-link-hover); }

/* !important on color: fights a:visited from per-client main.css. */
.hzo-mobile-panel > .hzo-mobile-menu__link {
    display: block;
    padding: var(--space-smd) var(--space-lg) var(--space-smd) var(--space-lg-plus);
    font-size: var(--fs-100);
    font-weight: var(--fw-500, 500);
    color: var(--nav-mobile-link-color, var(--text-body)) !important;
    text-decoration: none;
    border-bottom: var(--border-1) solid var(--nav-mobile-divider-color, var(--border));
    transition: color 200ms ease, background 200ms ease;
}
.hzo-mobile-panel > .hzo-mobile-menu__link:last-child { border-bottom: 0; }
.hzo-mobile-panel > .hzo-mobile-menu__link:hover {
    color: var(--nav-link-hover) !important;
    background: var(--nav-mobile-link-hover-bg, var(--bg-alt));
}

/* Sidebar footer (contact info) */
.hzo-mobile-menu__footer {
    padding: var(--space-lg);
    border-top: var(--border-1) solid var(--nav-mobile-divider-color, var(--border));
    background: var(--nav-mobile-footer-bg, var(--bg-alt));
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 14px;
}
/* !important on color: fights a:visited from per-client main.css. */
.hzo-mobile-menu__contact {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    font-size: var(--fs-100);
    font-weight: var(--fw-500, 500);
    color: var(--text-body) !important;
    text-decoration: none;
    transition: color 200ms ease;
}
.hzo-mobile-menu__contact:hover { color: var(--nav-link-hover) !important; }
.hzo-mobile-menu__contact svg { color: var(--accent); flex-shrink: 0; }

/* Prevent body scroll when menu is open */
body.hzo-menu-open { overflow: hidden; }

.hzo-utility-bar {
    background: var(--utility-bar-bg, var(--brand));
    color: var(--utility-bar-text, #fff);
    font-size: var(--fs-75);
    padding: var(--utility-bar-padding, 7px 0);
}
.hzo-utility-bar__inner {
    display: flex;
    align-items: center;
    justify-content: flex-end; /* desktop/tablet: right-aligned */
    gap: var(--space-smd);
}
.hzo-utility-bar__links {
    display: flex;
    align-items: center;
    gap: var(--space-smd);
}
.hzo-utility-bar__divider { color: var(--utility-bar-divider-color, rgba(255,255,255,0.3)); user-select: none; }

/* !important on color: fights a:visited from per-client main.css. */
.hzo-utility-bar a,
.hzo-utility-bar a:visited {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--utility-link) !important;
    text-decoration: none;
    font-weight: var(--fw-700, 700);
    white-space: nowrap;
    transition: color 200ms ease;
}
.hzo-utility-bar a:hover,
.hzo-utility-bar a:visited:hover { color: var(--utility-link-hover) !important; }
.hzo-utility-bar a svg { flex-shrink: 0; }

/* Mobile: center the links (no hours on mobile) */
@media (max-width: 767px) {
    .hzo-utility-bar__inner { justify-content: center; }
}

/* Light utility bar variant — pale background with dark text/links.
   CSS custom properties are scoped to this selector, so --utility-link
   and --utility-link-hover resolve to the lighter-context values for
   everything inside. Apply via: <div class="hzo-utility-bar hzo-utility-bar--light"> */
.hzo-utility-bar--light {
    background: var(--utility-bar-light-bg, var(--bg-alt));
    color: var(--utility-bar-light-text, var(--text-body));
    border-bottom: var(--border-1) solid var(--utility-bar-light-border-color, var(--border));
    --utility-link: var(--utility-light-link, var(--text-muted));
    --utility-link-hover: var(--utility-light-link-hover, var(--brand));
}

.hzo-mega-menu__close:hover { color: var(--brand); }

/* G38: "View all {Section}" — the parent landing link that leads an expanded
   accordion panel. The toggle only expands, so this restores reachability of the
   parent's own page on mobile. Brand-toned + semibold + a trailing arrow so it
   reads as the section overview, distinct from the child links below it. */
.hzo-mobile-panel > .hzo-mobile-viewall {
    color: var(--nav-link-active, var(--brand-text, var(--brand))) !important;
    font-weight: var(--fw-semibold, 600);
}
.hzo-mobile-panel > .hzo-mobile-viewall::after {
    content: "\2192";
    margin-left: 6px;
    display: inline-block;
}


/* --- modal/modal.css --- */
/* ─── Modal / dialog ──────────────────────────────────────────────────
   A backdrop (.hzo-modal) centers a panel (.hzo-modal__dialog, which carries
   role="dialog" + aria-modal="true"). Closed = opacity:0; visibility:hidden, so
   the panel's controls are non-focusable and click-inert until opened; the
   .is-open class reveals it (200ms fade + rise, reduced-motion-bounded). Focus
   trap, background inert, Esc/backdrop close, and focus-return live in modal.js
   (built on hzo.trapFocus in main.js). Modals are portaled to <body> on init
   so ancestor overflow/transform/z-index can't clip or mis-stack them. */
.hzo-modal {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-lg);
    background: rgba(0, 0, 0, 0.5);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--transition-base) ease, visibility var(--transition-base);
}
.hzo-modal.is-open { opacity: 1; visibility: visible; }

.hzo-modal__dialog {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 520px;
    max-height: calc(100vh - var(--space-2xl));
    overflow-y: auto;
    background: var(--surface, #fff);
    border-radius: var(--radius-panel);
    box-shadow: var(--shadow-xl, var(--shadow-lg));
    transform: translateY(12px);
    transition: transform var(--transition-base) ease;
}
.hzo-modal.is-open .hzo-modal__dialog { transform: none; }
.hzo-modal__dialog--sm { max-width: 400px; }
.hzo-modal__dialog--lg { max-width: 760px; }

.hzo-modal__header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-lg) var(--space-lg) 0;
}
.hzo-modal__title {
    margin: 0;
    font-family: var(--font-heading);
    font-size: 22px;
    line-height: 1.3;
    color: var(--text-heading);
}
.hzo-modal__close {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin: -10px -10px 0 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: var(--radius-control);
    cursor: pointer;
    transition: background var(--transition-base) ease, color var(--transition-base) ease;
}
.hzo-modal__close:hover { background: var(--bg-alt); color: var(--text-heading); }
.hzo-modal__close:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

.hzo-modal__body {
    padding: var(--space-md) var(--space-lg) var(--space-lg);
    color: var(--text-body);
}
.hzo-modal__body > :first-child { margin-top: 0; }
.hzo-modal__body > :last-child { margin-bottom: 0; }

.hzo-modal__footer {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--space-sm);
    padding: 0 var(--space-lg) var(--space-lg);
}

/* Background scroll-lock (`body.hzo-overlay-open`) moved to the always-loaded
   base bucket — `components/_base/overlay-lock.css` — so it applies on pages
   that open a drawer/mini-cart without a modal present (audit gap G2). Do not
   re-add it here: this sheet is gated (only loads where a modal is used). */

/* Phone: dock as a bottom sheet with full-width stacked actions. */
@media (max-width: 480px) {
    .hzo-modal { padding: 0; align-items: flex-end; }
    .hzo-modal__dialog,
    .hzo-modal__dialog--sm,
    .hzo-modal__dialog--lg {
        max-width: none;
        max-height: 92vh;
        border-radius: var(--radius-panel) var(--radius-panel) 0 0;
    }
    .hzo-modal__footer .hzo-btn { flex: 1 1 auto; }
}

@media (prefers-reduced-motion: reduce) {
    .hzo-modal,
    .hzo-modal__dialog,
    .hzo-modal__close { transition: none; }
    .hzo-modal__dialog { transform: none; }
}


/* --- nap/nap.css --- */
/* ─── LocalBusiness NAP Block (.hzo-nap) ───────────────────────────────────
   The canonical NAP card. render="card" styles live here; render="footer"
   reuses the legacy .hzo-footer-nap rules (primitives.css / footer styling —
   untouched by this widget so the migration stays byte-identical), and
   render="schema-only" emits no DOM at all.

   Scoped to .hzo-nap*; every value pulls from a design token — no raw hex/px
   where a token exists. The optional open-now pill (.hzo-nap__status, driven by
   nap.js consuming hzo:hourschange) is additive over the server-rendered <dl>. */

.hzo-nap--card {
    display: block;
    font-style: normal; /* <address> defaults to italic */
    color: var(--text-body);
}

.hzo-nap__heading {
    margin: 0 0 var(--space-md);
    color: var(--text-heading, var(--text-body));
}

.hzo-nap__name {
    margin: 0 0 var(--space-sm);
    font-weight: var(--fw-bold, 700);
    color: var(--text-heading, var(--text-body));
}

.hzo-nap__row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    margin: 0 0 var(--space-sm);
    line-height: 1.5;
}

.hzo-nap__icon {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    margin-top: 0.15em; /* optical align to the first text line */
    color: var(--accent-text, var(--brand));
}

/* Nav-link role: color + underline shift on hover/focus; focus-visible rings
   (WCAG 2.4.13). Never color-alone — the underline carries state for
   forced-colors / color-blind users. */
.hzo-nap__link {
    color: var(--text-link, var(--brand));
    text-decoration: none;
    border-bottom: 1px solid transparent;
    transition: color var(--dur-1, 150ms) var(--ease-standard, ease),
                border-color var(--dur-1, 150ms) var(--ease-standard, ease);
}
.hzo-nap__link:hover {
    color: var(--text-link-hover, var(--accent-text, var(--brand)));
    border-bottom-color: currentColor;
}
.hzo-nap__link:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
    border-radius: var(--radius-xs, 2px);
}

/* ── Hours ── */
.hzo-nap__hours {
    margin-top: var(--space-md);
}
.hzo-nap__hours-heading {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin: 0 0 var(--space-xs);
    font-weight: var(--fw-medium, 500);
    color: var(--text-heading, var(--text-body));
}

/* Open-now pill — hidden until nap.js reveals it (progressive enhancement).
   Words ("Open now" / "Closed") carry the meaning; color is decorative. */
.hzo-nap__status {
    margin-left: auto;
    padding: 0.1em 0.6em;
    border-radius: var(--radius-full, 999px);
    font-size: 0.8em;
    font-weight: var(--fw-medium, 500);
    line-height: 1.6;
    transition: background-color var(--dur-1, 150ms) var(--ease-standard, ease),
                color var(--dur-1, 150ms) var(--ease-standard, ease);
}
.hzo-nap__status.is-open {
    background: var(--bg-success, rgba(22, 163, 74, 0.12));
    color: var(--text-success, #15803d);
}
.hzo-nap__status.is-closed {
    background: var(--bg-muted, rgba(0, 0, 0, 0.06));
    color: var(--text-muted, currentColor);
}

.hzo-nap .hzo-hours {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-2xs, 0.25rem) var(--space-lg);
    margin: 0;
}
.hzo-nap .hzo-hours dt {
    font-weight: var(--fw-medium, 500);
    color: var(--text-body);
}
.hzo-nap .hzo-hours dd {
    margin: 0;
    color: var(--text-muted, var(--text-body));
}
/* Today's row highlight — nap.js sets [aria-current] over the static <dl>. */
.hzo-nap .hzo-hours dt[aria-current="true"],
.hzo-nap .hzo-hours dt[aria-current="true"] + dd {
    color: var(--text-heading, var(--text-body));
    font-weight: var(--fw-bold, 700);
}

/* ── Service-area towns (native <details> disclosure) ── */
.hzo-nap__towns {
    margin-top: var(--space-md);
}
.hzo-nap__towns-summary {
    margin: 0 0 var(--space-xs);
    font-weight: var(--fw-medium, 500);
    cursor: pointer;
    color: var(--text-body);
}
.hzo-nap__towns-summary:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
    border-radius: var(--radius-xs, 2px);
}
.hzo-nap__towns-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2xs, 0.25rem) var(--space-sm);
    margin: 0;
    padding: 0;
    list-style: none;
    color: var(--text-muted, var(--text-body));
}
.hzo-nap__towns-list > li:not(:last-child)::after {
    content: "\00B7"; /* middot separator */
    margin-left: var(--space-sm);
    color: var(--text-muted, currentColor);
}

/* Forced-colors: box-shadow rings are unreliable; ensure a system outline. */
@media (forced-colors: active) {
    .hzo-nap__link:focus-visible,
    .hzo-nap__towns-summary:focus-visible {
        outline: 2px solid CanvasText;
    }
    .hzo-nap__status {
        border: 1px solid CanvasText;
    }
}

/* @media print — the NAP is what home-service customers print. Keep it fully
   visible, expand the town list, and never let a reveal hook hide it. */
@media print {
    .hzo-nap__towns[open] > .hzo-nap__towns-list,
    .hzo-nap__towns > .hzo-nap__towns-list {
        display: flex !important;
    }
    .hzo-nap.hzo-reveal {
        opacity: 1 !important;
        transform: none !important;
    }
}

/* Reduced motion — kill the pill/link transitions. */
@media (prefers-reduced-motion: reduce) {
    .hzo-nap__link,
    .hzo-nap__status {
        transition: none;
    }
}


/* --- nav-link/nav-link.css --- */
/* ─── Navigation link ───────────────────────────────────────────────── */
.hzo-nav-link {
    font-family: var(--font-body);
    font-size: 15px;
    font-weight: var(--fw-500, 500);
    color: var(--nav-link);
    text-decoration: none;
    transition: color 0.15s;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.hzo-nav-link:hover {
    color: var(--brand-hover-on-dark); /* brighter brand — a clear jump, not one step */
}
.hzo-nav-link:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

/* Simple underline default (Track-2 design pass) — a restrained hover underline
   grows in from the left. Token-driven color (var(--link-underline, currentColor)).
   A client can replace it with a richer treatment: its own .hzo-nav-link::after
   in custom.css loads last and fully overrides this default (byte-identical). */
.hzo-nav-link { position: relative; }
.hzo-nav-link::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -2px;
    height: 2px;
    background: var(--link-underline, currentColor);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform .2s ease;
    pointer-events: none;
}
.hzo-nav-link:hover::after,
.hzo-nav-link:focus-visible::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
    .hzo-nav-link::after { transition: none; }
}


/* --- newsletter/newsletter.css --- */
/* newsletter — @tokens: --newsletter-padding-y, --newsletter-input-min

   An email-capture band: a FULL-BLEED section (owns its own vertical padding +
   optional background) wrapping a `.hzo-container` with a title, supporting
   line, and an inline email input + submit button. Sibling to blocks/cta-band —
   same section shape, but the CTA here is a capture form rather than a link.

   NOTE: real submission wires to a Fluent Form (the .php handler / block swaps
   the inner control for the form embed). This is the STYLED capture band — the
   `<input>` is a visual/native control, NOT a live subscribe endpoint. The
   input still gets a proper label (aria-label in the template) + its own focus
   ring so the static band is accessible and demo-able on its own.

   Token-only: the band's vertical padding + the input's responsive min-width
   have no dedicated semantic role, so they're component tokens with a literal
   fallback (the icon-button.css dial pattern). Everything else reads a
   semantic/L1 token. */
.hzo-newsletter {
    padding-top: var(--newsletter-padding-y, var(--space-3xl));
    padding-bottom: var(--newsletter-padding-y, var(--space-3xl));
    background: var(--bg-alt);   /* light default surface; --dark flips it below */
}

/* Band heading — visual-h2 scale, heading ink, serif display face. */
.hzo-newsletter__title {
    margin: 0 0 var(--space-sm);
    font-family: var(--font-heading);
    font-size: var(--fs-600);
    line-height: var(--lh-tight);
    color: var(--text-heading);
}

/* Supporting line — muted subtitle, measure-capped for readability. */
.hzo-newsletter__text {
    margin: 0 0 var(--space-lg);
    max-width: var(--prose-max-width);
    font-size: var(--fs-300);
    line-height: var(--lh-normal);
    color: var(--text-muted);
}

/* Inline row: input + submit. Wraps → stacks on narrow viewports. */
.hzo-newsletter__form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
}

/* Native text input styled as a proper control (mirrors form.css / search.css).
   Grows to fill the row but never below its min so it stacks cleanly on mobile. */
.hzo-newsletter__input {
    flex: 1 1 var(--newsletter-input-min, 260px);
    min-width: 0;
    padding: var(--space-smd) var(--space-md);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-control);
    background: var(--surface);
    color: var(--text-body);
    font-family: var(--font-body);
    font-size: var(--fs-200);
    line-height: var(--lh-normal);
    transition: border-color var(--transition-base) ease, box-shadow var(--transition-base) ease;
}
.hzo-newsletter__input::placeholder { color: var(--text-muted); }

/* A11y — focus ring on the input (its own control). Reduced-motion handled
   globally by components/_base/a11y.css. */
.hzo-newsletter__input:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
    border-color: var(--color-focus);
}
.hzo-newsletter__input[disabled],
.hzo-newsletter__input[aria-disabled="true"] {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

/* Dark modifier — composes the .hzo-section--dark look: dark surface + on-dark
   ink. The submit button should ALSO get `.hzo-btn--on-dark` in this context
   (buttons.css owns that variant; this modifier only re-themes the band). */
.hzo-newsletter--dark {
    /* --surface-dark-bg lives in the (currently inert) L3 block, so fall back to
       the live --bg-dark-section — the exact idiom _base/surface-dark.css uses. */
    background: var(--surface-dark-bg, var(--bg-dark-section));
}
.hzo-newsletter--dark .hzo-newsletter__title {
    color: var(--heading-color-on-dark, #fff);
}
.hzo-newsletter--dark .hzo-newsletter__text {
    color: rgba(255, 255, 255, 0.85);
}

@media (prefers-reduced-motion: reduce) {
    .hzo-newsletter__input { transition: none; }
}


/* --- notification-dot/notification-dot.css --- */
/* notification-dot — @tokens: --radius-full, --error, --border-2, --card-bg, --space-2xs, --fs-50, --fw-600, --brand, --text-on-brand, --accent, --text-on-accent */
/* Generic count / activity indicator (cart count, unread) — distinct from the inventory-bound
   stock-status dot. Bare dot, or a numbered pill via --count; the --corner modifier overlays
   an icon on a position:relative host. Not animated → no reduced-motion block needed. */
.hzo-notification-dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--notification-dot-size, 8px);
    height: var(--notification-dot-size, 8px);
    border-radius: var(--radius-full);
    background: var(--error);
    box-shadow: 0 0 0 var(--border-2, 2px) var(--card-bg, #fff); /* ring so it reads over an icon */
}

/* Numbered pill variant. */
.hzo-notification-dot--count {
    min-width: var(--notification-dot-count-size, 18px);
    height: var(--notification-dot-count-size, 18px);
    padding: 0 var(--space-2xs);
    font-size: var(--fs-50);
    font-weight: var(--fw-600);
    line-height: 1;
    color: #fff; /* white on the coloured badge — the badge.css convention */
}

/* Colour modifiers — default stays --error. */
.hzo-notification-dot--brand  { background: var(--brand);  color: var(--text-on-brand); }
.hzo-notification-dot--accent { background: var(--accent); color: var(--text-on-accent); }

/* Overlay an icon — host MUST be position:relative. */
.hzo-notification-dot--corner {
    position: absolute;
    top: 0;
    right: 0;
    transform: translate(50%, -50%);
}


/* --- page-header/page-header.css --- */
/* page-header — @tokens: --page-header-max-width

   The lightweight title band at the top of an interior page, sitting directly
   below the site nav (a services / about / legal page…). NOT the marketing hero
   (`.hzo-hero` is the top-of-homepage composition with actions + media); this is
   the quieter "you are here" header: eyebrow + h1 + one supporting line, with an
   optional breadcrumb slot. Full-bleed band — the root OWNS the vertical padding
   and the optional tint, and wraps a `.hzo-container` for the horizontal gutter.
   Primitive: CSS-only, not interactive (carries no control, so no focus/disabled
   idioms — a breadcrumb dropped in the slot styles + focuses itself).

   Token-only: the band padding reads --space-3xl, the tint reads --bg-alt, and
   the type reads the shared L1 --fs-* ladder + semantic inks so a dark archetype
   re-themes it from :root. The subtitle's readability measure has no semantic
   token, so it's a component token with a literal fallback (the icon-button.css
   dial pattern). */
.hzo-page-header {
    padding-block: var(--space-3xl);
    background: transparent;
}

/* Tinted variant — pale alt surface to separate the header from white content. */
.hzo-page-header--alt {
    background: var(--bg-alt);
}

/* Small tracked label above the title. Mirrors the .hzo-eyebrow look (uppercase,
   tracked, small) but scoped to this block so it never borrows another class. */
.hzo-page-header__eyebrow {
    display: inline-block;
    font-size: var(--fs-100);
    font-weight: var(--fw-600);
    line-height: var(--lh-snug);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--brand-text);
    margin: 0 0 var(--space-sm);
}

/* The page h1 — large heading ink. Sizes from the L1 ladder rather than a fixed
   px so it re-scales with the type system; tight leading for a display line. */
.hzo-page-header__title {
    font-family: var(--font-heading);
    font-size: var(--fs-700);
    font-weight: var(--fw-700);
    line-height: var(--lh-tight);
    letter-spacing: var(--tracking-tight);
    color: var(--text-heading);
    margin: 0;
}

/* One supporting line — muted, capped to a readable measure. */
.hzo-page-header__subtitle {
    max-width: var(--page-header-max-width, 60ch);
    font-size: var(--fs-300);
    line-height: var(--lh-normal);
    color: var(--text-muted);
    margin: var(--space-md) 0 0;
}

/* Breadcrumb slot — spacing only. The breadcrumb component styles + focuses
   itself; this just seats it above the eyebrow with rhythm. */
.hzo-page-header__breadcrumb {
    margin: 0 0 var(--space-md);
}

/* Center-aligned variant — also centers the capped subtitle measure. */
.hzo-page-header--center {
    text-align: center;
}
.hzo-page-header--center .hzo-page-header__subtitle {
    margin-inline: auto;
}


/* --- pagination/pagination.css --- */
/* ─── Pagination ──────────────────────────────────────────────────────
   Archive / search page nav. 44px tap targets; current page brand-filled;
   disabled prev/next muted + non-interactive. !important on color fights
   per-client a:visited (matching :visited rule below). */
.hzo-pagination { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--space-sm); list-style: none; margin: var(--space-xl) 0 0; padding: 0; }
.hzo-pagination__link {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 44px; min-height: 44px; padding: 0 var(--space-smd);
    font-family: var(--font-body); font-weight: var(--fw-600, 600); font-size: 15px;
    color: var(--brand-text) !important; text-decoration: none;
    border: var(--border-1) solid var(--border); border-radius: var(--radius-control);
    transition: background var(--transition-base), border-color var(--transition-base), color var(--transition-base);
}
.hzo-pagination__link:hover { background: var(--brand-pale); border-color: var(--brand); color: var(--brand-text) !important; }
.hzo-pagination__link:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.hzo-pagination__link.is-current { background: var(--brand) !important; border-color: var(--brand); color: var(--text-on-brand) !important; pointer-events: none; }
.hzo-pagination__link.is-disabled,
.hzo-pagination__link[aria-disabled="true"] { color: var(--text-muted) !important; pointer-events: none; opacity: 0.6; }
.hzo-pagination__link:visited { color: var(--brand-text) !important; }

/* ─── G20: WP query-loop + WooCommerce archive pagination ─────────────────────
   WP emits a.page-numbers / span.page-numbers.current inside
   .wp-block-query-pagination (block) and .woocommerce-pagination (WC). Give them
   the SAME 44px-tap, brand-current pager look instead of raw theme defaults. */
.wp-block-query-pagination,
.woocommerce-pagination .page-numbers:not(li) {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
    gap: var(--space-sm); margin: var(--space-xl) 0 0; list-style: none; padding: 0;
}
.wp-block-query-pagination-numbers { display: inline-flex; flex-wrap: wrap; gap: var(--space-sm); }
.page-numbers,
.wp-block-query-pagination-previous,
.wp-block-query-pagination-next {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 44px; min-height: 44px; padding: 0 12px;
    font-family: var(--font-body); font-weight: var(--fw-semibold, 600); font-size: 15px;
    color: var(--brand-text) !important; text-decoration: none;
    border: 1px solid var(--border); border-radius: var(--radius-input);
    transition: background var(--transition-base), border-color var(--transition-base), color var(--transition-base);
}
a.page-numbers:hover,
.wp-block-query-pagination-previous:hover,
.wp-block-query-pagination-next:hover { background: var(--brand-pale); border-color: var(--brand); color: var(--brand-text) !important; }
a.page-numbers:focus-visible,
.wp-block-query-pagination-previous:focus-visible,
.wp-block-query-pagination-next:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.page-numbers.current { background: var(--brand) !important; border-color: var(--brand); color: var(--text-on-brand) !important; }
.page-numbers.dots { border-color: transparent; min-width: auto; padding: 0 4px; pointer-events: none; }
.page-numbers:visited { color: var(--brand-text) !important; }


/* --- price/price.css --- */
/* price — @tokens: (none — fully semantic/L1)

   A WooCommerce product-price display atom for custom card / product layouts.
   Non-interactive; tabular figures so aligned prices in a list stay in column.
   Token-only: every colour / size / weight reads a semantic or L1 token. */
.hzo-price {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-2xs);
    color: var(--text-heading);
    font-size: var(--fs-200);
    font-weight: var(--fw-600);
    font-variant-numeric: tabular-nums;
}

/* The effective / current price — inherits the block's size, weight and colour. */
.hzo-price__amount {
    color: inherit;
}

/* The struck original price when on sale — muted + line-through, lighter weight. */
.hzo-price__regular {
    color: var(--text-muted);
    font-size: var(--fs-75);
    font-weight: var(--fw-500);
    text-decoration: line-through;
}

/* The emphasised sale price — accent ink, heavier weight. */
.hzo-price__sale {
    color: var(--accent-text);
    font-weight: var(--fw-700);
}

/* Small muted affixes ("From", "/mo"). */
.hzo-price__prefix,
.hzo-price__suffix {
    color: var(--text-muted);
    font-size: var(--fs-75);
    font-weight: var(--fw-500);
}

/* Product-page hero price. */
.hzo-price--lg {
    font-size: var(--fs-400);
}


/* --- product-card/product-card.css --- */
/* product-card — @tokens: --card-bg, --card-border, --card-radius, --card-shadow, --card-hover-shadow */
/* ─── Product card ───────────────────────────────────────────────────────────
   Self-contained card for CUSTOM commerce grids (the [wc_cards] path). It
   DELIBERATELY reads the same shared card-surface tokens the native WC product
   loop uses (assets/css/woocommerce.css → .woocommerce ul.products li.product,
   and components/card/card.css → .hzo-card): --card-bg, --card-border,
   --card-radius, --card-shadow, --card-hover-shadow, with the exact inline
   fallbacks those call-sites use. This is intentional cross-surface consistency
   — a custom card must read byte-identically to a native loop card and respond
   to the same archetype dials — NOT token-borrowing drift.
   Composes foreign primitives (.hzo-price, .hzo-stock-status, .hzo-badge--sale,
   .hzo-wishlist, .hzo-btn) inside its slots but styles ONLY its own
   .hzo-product-card__* structure — never those foreign classes. */
.hzo-product-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    padding: var(--space-md);
    background: var(--card-bg, #fff);
    border: var(--border-1) solid var(--card-border, var(--border));
    border-radius: var(--card-radius, var(--radius-card));
    box-shadow: var(--card-shadow, var(--shadow-sm));
    position: relative;
    transition: box-shadow var(--transition-base), transform var(--transition-base);
}
.hzo-product-card:hover {
    box-shadow: var(--card-hover-shadow, var(--shadow-lg));
    transform: translateY(-4px);
}

/* Image slot */
.hzo-product-card__media {
    position: relative;
    border-radius: var(--radius-control);
    overflow: hidden;
    margin: 0;
}
.hzo-product-card__media img {
    display: block;
    width: 100%;
    height: auto;
    object-fit: cover;
}

/* Sale-badge slot (holds .hzo-badge--sale) */
.hzo-product-card__badge {
    position: absolute;
    top: var(--space-sm);
    left: var(--space-sm);
}

/* Wishlist slot (holds .hzo-wishlist) */
.hzo-product-card__wishlist {
    position: absolute;
    top: var(--space-sm);
    right: var(--space-sm);
}

.hzo-product-card__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    flex: 1 1 auto;
}

.hzo-product-card__title {
    font-family: var(--font-heading);
    font-size: var(--fs-300);
    font-weight: var(--fw-700);
    color: var(--text-heading);
    line-height: var(--lh-tight);
    margin: 0;
}
.hzo-product-card__title a {
    color: inherit;
    text-decoration: none;
    background-image: none; /* a title link is a heading — never let a client content-link highlight ride onto it */
}
.hzo-product-card__title a:hover {
    text-decoration: underline;
}

/* Price + stock row; margin-top:auto pins it to the bottom above actions.
   Holds .hzo-price + .hzo-stock-status. */
.hzo-product-card__meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-top: auto;
}

/* Add-to-cart row (holds the .hzo-btn) */
.hzo-product-card__actions {
    display: flex;
    gap: var(--space-sm);
    margin-top: var(--space-sm);
}

/* Compact density */
.hzo-product-card--compact {
    padding: var(--space-sm);
    gap: var(--space-xs);
}

@media (prefers-reduced-motion: reduce) {
    .hzo-product-card {
        transition: none;
    }
    .hzo-product-card:hover {
        transform: none;
    }
}


/* --- product-grid/product-grid.css --- */
/* ═════════════════════════════════════════════════════════════════════════
   WOOCOMMERCE PRODUCT GRID (W-130) — styles

   Scoped to .hzo-product-grid*. Tokens only (no bare hex / px / z-index where a
   token exists). The container marker [data-hzo-pg] already carries an
   aria-busy dim rule in woocommerce.css (loaded on WC pages); these rules add
   the ENGINE card (.hzo-product-card), the responsive grid geometry driven by
   the --hzo-pg-cols* custom properties the PHP sets, the Load-more control, the
   stock pill, and the skeleton loading state.

   Motion: durations/easings are --dur-*/--ease-* tokens only. Reduced-motion
   (PRESERVE-5) — ambient shimmer STOPS, the status spinner SLOWS (never freezes),
   content stays visible. Forced-colors gets an explicit branch.
   ═════════════════════════════════════════════════════════════════════════ */

.hzo-product-grid {
    --hzo-pg-cols: 4;
    --hzo-pg-cols-md: 3;
    --hzo-pg-cols-sm: 2;
    --hzo-pg-gap: var(--space-lg);
    display: block;
}

.hzo-product-grid .hzo-eyebrow,
.hzo-product-grid .hzo-visual-h2 {
    text-align: center;
}

/* ── Grid geometry (container-aware column counts) ─────────────────────────── */
.hzo-product-grid__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--hzo-pg-gap);
    grid-template-columns: repeat(var(--hzo-pg-cols), minmax(0, 1fr));
    margin-top: var(--space-xl);
}
.hzo-product-grid--empty .hzo-empty { margin-top: var(--space-xl); }

/* Heading absent → no top gap eats the section rhythm. */
.hzo-product-grid__list:first-child { margin-top: 0; }

@media (max-width: 1024px) {
    .hzo-product-grid__list { grid-template-columns: repeat(var(--hzo-pg-cols-md), minmax(0, 1fr)); }
}
@media (max-width: 640px) {
    .hzo-product-grid__list {
        grid-template-columns: repeat(var(--hzo-pg-cols-sm), minmax(0, 1fr));
        gap: var(--space-md);
    }
}

/* ── The card ──────────────────────────────────────────────────────────────── */
.hzo-product-card {
    /* Card-integrated add-to-cart: a moderate radius reads more considered than a
       full pill inside a card (per-client themes still override --btn-radius). */
    --btn-radius: var(--radius-1);
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--space-smd);
    background: var(--card-bg, var(--surface));
    border: var(--border-1) solid var(--card-border, var(--border));
    border-radius: var(--card-radius, var(--radius-2));
    padding: var(--space-smd);
    box-shadow: var(--card-shadow, var(--shadow-sm));
    transition: box-shadow var(--dur-3) var(--ease-out),
                transform var(--dur-3) var(--ease-out),
                border-color var(--dur-3) var(--ease-out);
    text-align: left;
}
/* Decorative lift only — the card is NOT a single click target (it holds a real
   title link AND an add-to-cart control), so no false whole-card affordance
   (PRESERVE-4 / G50). */
.hzo-product-card:hover {
    box-shadow: var(--card-hover-shadow, var(--shadow-lg));
    transform: translateY(-4px);
    border-color: color-mix(in srgb, var(--brand) 22%, var(--border));
}

.hzo-product-card__media {
    display: block;
    position: relative;
    border-radius: var(--radius-1);
    overflow: hidden;
    aspect-ratio: 1 / 1;          /* zero-CLS media box (G51) */
    background: var(--surface-2);
}
.hzo-product-card__media img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    margin: 0;
    transition: transform var(--dur-4) var(--ease-out);
}
/* Subtle image zoom on card hover — a touch of life, not a gimmick. */
.hzo-product-card:hover .hzo-product-card__media img { transform: scale(1.05); }

/* Sale flash — a crisp uppercase brand pill at the media corner. */
.hzo-product-card__flash {
    position: absolute;
    top: var(--space-sm);
    left: var(--space-sm);
    z-index: 1;
    padding: 5px 11px;
    background: var(--brand);
    color: var(--text-on-brand, #fff);
    font-size: 0.68rem;
    font-weight: var(--fw-bold, 700);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1.3;
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-sm);
}

.hzo-product-card__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    flex: 1 1 auto;
}

.hzo-product-card__title {
    font-family: var(--font-heading);
    font-size: 1.05rem;
    font-weight: var(--fw-bold, 700);
    line-height: 1.35;
    color: var(--text-heading);
    margin: 0;
    padding: 0;
    display: -webkit-box;
    -webkit-line-clamp: 2;          /* even card heights; long names never blow out the row */
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.hzo-product-card__title a {
    color: inherit;
    text-decoration: none;
    background-image: none; /* a title link is a heading — never let a client content-link highlight ride onto it */
}
.hzo-product-card__title a:hover { color: var(--brand-text, var(--brand)); }

.hzo-product-card__rating { font-size: 0.9rem; margin-top: calc(-1 * var(--space-2xs)); }

/* Price hierarchy: the current price is the loudest thing on the card; a sale
   price takes the brand color, the original strikes through smaller + muted. */
.hzo-product-card__price {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-xs);
    color: var(--text-heading);
    font-size: 1.2rem;
    font-weight: var(--fw-extrabold, 800);
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.01em;
}
.hzo-product-card__price del {
    color: var(--text-muted);
    font-size: 0.9rem;
    font-weight: var(--fw-medium, 500);
    opacity: 1;
    margin: 0;
}
.hzo-product-card__price ins {
    color: var(--brand-text, var(--brand));
    text-decoration: none;
    font-weight: var(--fw-extrabold, 800);
}

/* ── Stock pill ────────────────────────────────────────────────────────────── */
.hzo-product-card__stock {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 10px;
    font-size: 0.75rem;
    font-weight: var(--fw-semibold, 600);
    line-height: 1.4;
    border-radius: var(--radius-pill);
}
.hzo-product-card__stock::before {
    content: "";
    width: 7px;
    height: 7px;
    border-radius: var(--radius-full);
    background: currentColor;
}
.hzo-product-card__stock--in   { color: var(--success-ink); background: var(--success-bg); }
.hzo-product-card__stock--low  { color: var(--warning-ink); background: var(--warning-bg); }
.hzo-product-card__stock--out  { color: var(--error-ink);   background: var(--error-bg); }

/* ── Add-to-cart control (engine .hzo-btn inherits its state visuals from
   buttons.css: .is-loading spinner / .is-added check — do NOT redefine them). ── */
.hzo-product-card__add {
    margin-top: auto;              /* pin to card bottom regardless of body height */
    align-self: stretch;
    justify-content: center;
    min-height: 44px;              /* substantial, comfortable tap (§11.2) */
    font-weight: var(--fw-bold, 700);
    letter-spacing: 0.01em;
}

/* ── Load-more control ─────────────────────────────────────────────────────── */
.hzo-product-grid__foot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-sm);
    margin-top: var(--space-xl);
}
.hzo-product-grid__more {
    position: relative;
    min-height: 44px;
    min-width: 220px;
    justify-content: center;
}
.hzo-product-grid__more-spinner {
    display: none;
    width: 1em;
    height: 1em;
    margin-left: var(--space-sm);
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: var(--radius-full);
    animation: hzo-pg-spin var(--dur-4) linear infinite;
}
.hzo-product-grid__more.is-loading .hzo-product-grid__more-spinner { display: inline-block; }
.hzo-product-grid__more[aria-disabled="true"] { pointer-events: none; opacity: 0.7; }

.hzo-product-grid__error {
    margin: 0;
    color: var(--error-ink);
    font-size: 0.9rem;
    font-weight: var(--fw-semibold, 600);
}

@keyframes hzo-pg-spin { to { transform: rotate(360deg); } }

/* ── Skeleton loading state (G47 — a visible cue that works on FIRST load, not
   an opacity dim). Shimmer is AMBIENT motion → stops under reduced-motion. ──── */
.hzo-product-card--skeleton {
    pointer-events: none;
    gap: var(--space-sm);
}
.hzo-product-card--skeleton:hover { transform: none; box-shadow: var(--card-shadow, var(--shadow-sm)); }
.hzo-skel {
    display: block;
    border-radius: var(--radius-input);
    background: linear-gradient(
        100deg,
        var(--surface-2) 30%,
        var(--border) 50%,
        var(--surface-2) 70%
    );
    background-size: 200% 100%;
    animation: hzo-pg-shimmer var(--dur-4) var(--ease-standard) infinite;
}
.hzo-skel.hzo-product-card__media { aspect-ratio: 1 / 1; }
.hzo-skel--line { height: 0.9rem; border-radius: var(--radius-pill); }
.hzo-skel--short { width: 55%; }

@keyframes hzo-pg-shimmer {
    from { background-position: 200% 0; }
    to   { background-position: -200% 0; }
}

/* ── Reduced motion (PRESERVE-5): ambient shimmer + hover lift stop; the status
   spinner SLOWS but keeps moving; content stays fully visible. ─────────────── */
@media (prefers-reduced-motion: reduce) {
    .hzo-product-card,
    .hzo-product-card:hover { transition: none; transform: none; }
    .hzo-skel { animation: none; background: var(--surface-2); }
    .hzo-product-grid__more-spinner { animation-duration: 1.5s; }
}

/* ── Forced-colors / High Contrast (§12.2): the skeleton conveys nothing and the
   spinner border must remain visible; stock pills fall back to system colors. ─ */
@media (forced-colors: active) {
    .hzo-skel { background: Canvas; border: var(--border-1) solid CanvasText; animation: none; }
    .hzo-product-grid__more-spinner { border-color: CanvasText; border-top-color: transparent; }
    .hzo-product-card__stock { border: var(--border-1) solid CanvasText; }
    .hzo-product-card__flash { border: var(--border-1) solid CanvasText; }
}

/* ── Print (§12.1): drop the interactive chrome; keep the catalog readable. ─── */
@media print {
    .hzo-product-grid__foot,
    .hzo-product-card__add,
    .hzo-product-card--skeleton { display: none !important; }
    .hzo-product-card { box-shadow: none; break-inside: avoid; }
}

/* ─── Static block-pattern grids (merge graft from Track-1/2) ────────────────
   blocks/product-card.html renders cards DIRECTLY inside .hzo-product-grid with
   a --cols-N modifier and no __list child, so for that markup the grid must
   live on the element itself. The [hzo_product_grid] widget above never emits a
   --cols-N class (it drives columns via inline --hzo-pg-cols on __list), so
   these rules cannot collide with it — the two layouts stay independent.
   Base is auto-fill so mobile stays fluid; counts lock in at >=768px. */
.hzo-product-grid--cols-2,
.hzo-product-grid--cols-3,
.hzo-product-grid--cols-4 {
    display: grid;
    gap: var(--product-grid-gap, var(--space-lg));
    grid-template-columns: repeat(auto-fill, minmax(var(--product-grid-min, 240px), 1fr));
}

@media (min-width: 768px) {
    .hzo-product-grid--cols-2 { grid-template-columns: repeat(2, 1fr); }
    .hzo-product-grid--cols-3 { grid-template-columns: repeat(3, 1fr); }
    .hzo-product-grid--cols-4 { grid-template-columns: repeat(4, 1fr); }
}


/* --- progress-bar/progress-bar.css --- */
/* progress-bar — @tokens: --progress-bar-height, --progress-bar-height-sm, --progress-bar-height-lg */
/* ─── Progress bar ────────────────────────────────────────────────────
   Linear progress indicator, determinate + indeterminate. role="progressbar"
   + aria-valuenow/min/max live in markup (not focusable). The __fill width
   is set inline for the determinate case; the --indeterminate modifier
   pins width and slides the fill via a transform-only keyframe. */
.hzo-progress-bar {
    display: block;
    width: 100%;
    height: var(--progress-bar-height, 8px);
    background: var(--bg-alt);
    border-radius: var(--radius-pill);
    overflow: hidden;
}
.hzo-progress-bar__fill {
    height: 100%;
    background: var(--brand);
    border-radius: var(--radius-pill);
    transition: width var(--transition-base) ease;
}

/* ── Colour modifiers (recolour the fill) ── */
.hzo-progress-bar--accent  .hzo-progress-bar__fill { background: var(--accent); }
.hzo-progress-bar--success .hzo-progress-bar__fill { background: var(--success); }
.hzo-progress-bar--warning .hzo-progress-bar__fill { background: var(--warning); }
.hzo-progress-bar--error   .hzo-progress-bar__fill { background: var(--error); }
.hzo-progress-bar--info    .hzo-progress-bar__fill { background: var(--info); }

/* ── Size modifiers ── */
.hzo-progress-bar--sm { height: var(--progress-bar-height-sm, 4px); }
.hzo-progress-bar--lg { height: var(--progress-bar-height-lg, 12px); }

/* ── Indeterminate ── */
.hzo-progress-bar--indeterminate .hzo-progress-bar__fill {
    width: 40%;
    animation: hzo-progress-indeterminate 1.4s ease-in-out infinite;
}
@keyframes hzo-progress-indeterminate {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(250%); }
}

/* Reduced motion — disable the slide (static bar) + the width transition. */
@media (prefers-reduced-motion: reduce) {
    .hzo-progress-bar--indeterminate .hzo-progress-bar__fill {
        animation: none;
        transform: none;
    }
    .hzo-progress-bar__fill { transition: none; }
}


/* --- pullquote/pullquote.css --- */
/* pullquote — @tokens: --pullquote-mark-size, --pullquote-mark-offset, --pullquote-measure

   A large editorial quote block pulled out of running copy — a designer drops it
   inside a `.hzo-container` (it is NOT a full-bleed band; the section supplies the
   padding + gutter). Root is a semantic <blockquote>. Distinct from
   `.hzo-testimonial` (a card with an avatar/rating for social proof); a pullquote
   is bare typographic emphasis. Primitive: CSS-only, not interactive (no
   focus/disabled idioms).

   Token-only: the quote reads --font-heading + the big end of the L1 --fs-*
   ladder + heading ink; the decorative mark + accent bar read --border / --brand
   so a per-client recolor + a dark archetype re-theme from :root. Two values have
   no semantic role — the oversized quote-mark glyph and the centered readability
   measure — so they're component tokens with literal fallbacks (the icon-button.css
   dial pattern). The glyph's optical baseline nudge is a component token too. */
.hzo-pullquote {
    margin: 0;
    color: var(--text-heading);
}

/* Optional oversized opening quote mark — low-opacity chrome in the border ink
   so it reads as decoration, not content. Set the glyph in markup (an aria-hidden
   ::before glyph carries no meaning a screen reader needs). */
.hzo-pullquote__quote::before {
    content: "\201C";               /* “ — decorative opening double-quote */
    font-family: var(--font-heading);
    font-size: var(--pullquote-mark-size, var(--fs-900));
    line-height: var(--lh-none);
    color: var(--border);
    opacity: var(--opacity-muted);
    margin-right: var(--space-2xs);
    vertical-align: var(--pullquote-mark-offset, -0.15em);
}

/* The quote itself — large, serif display face, relaxed leading for readability
   at size. Sizes from the L1 ladder rather than a fixed px. */
.hzo-pullquote__quote {
    font-family: var(--font-heading);
    font-size: var(--fs-500);
    font-weight: var(--fw-500);
    line-height: var(--lh-relaxed);
    color: var(--text-heading);
    margin: 0;
}

/* Attribution — muted, small, upright (not italic). Prefix the em-dash in markup. */
.hzo-pullquote__cite {
    display: block;
    font-family: var(--font-body);
    font-size: var(--fs-200);
    font-style: normal;
    line-height: var(--lh-normal);
    color: var(--text-muted);
    margin: var(--space-md) 0 0;
}

/* Centered variant — also caps + centers the quote to a readable measure. */
.hzo-pullquote--center {
    text-align: center;
}
.hzo-pullquote--center .hzo-pullquote__quote {
    max-width: var(--pullquote-measure, 32ch);
    margin-inline: auto;
}

/* Brand variant — a quieter inline treatment: a brand accent bar on the left
   (the callout.css idiom) instead of the oversized mark. */
.hzo-pullquote--brand {
    padding-left: var(--space-lg);
    border-left: var(--border-3) solid var(--brand);
}
.hzo-pullquote--brand .hzo-pullquote__quote::before {
    content: none;
}


/* --- quantity-stepper/quantity-stepper.css --- */
/* ─── Quantity stepper — fused capsule ─────────────────────────────────
   minus / numeric input / plus in one pill. quantity-stepper.js
   ([data-hzo-qty]) clamps to data-min / data-max (or the input's min/max),
   disables the button at each bound, and emits a bubbling `hzo:qty-change`
   CustomEvent — it never POSTs (parity with the add-to-cart surface).
   Expected markup (emitted by the [hzo_quantity_stepper] shortcode):
     <div class="hzo-qty" data-hzo-qty data-min="1" data-max="9">
       <button type="button" class="hzo-qty__btn hzo-qty__btn--minus" data-hzo-qty-minus aria-label="Decrease quantity">…</button>
       <input class="hzo-qty__input" type="text" inputmode="numeric" value="1" aria-label="Quantity">
       <button type="button" class="hzo-qty__btn hzo-qty__btn--plus" data-hzo-qty-plus aria-label="Increase quantity">…</button>
     </div>
   Commerce control — no standalone block. */
.hzo-qty {
    display: inline-flex;
    align-items: center;
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-pill);
    overflow: hidden;
    background: var(--card-bg, #fff);
}
.hzo-qty__btn {
    appearance: none;
    border: 0;
    background: transparent;
    width: 44px;
    height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    color: var(--text-heading);
    line-height: var(--lh-none);
    transition: background var(--transition-base), color var(--transition-base);
}
.hzo-qty__btn svg { width: 18px; height: 18px; display: block; }
.hzo-qty__btn:hover:not(:disabled) { background: var(--bg-alt); }
.hzo-qty__btn:focus-visible { outline: 3px solid var(--color-focus); outline-offset: -3px; }
.hzo-qty__btn:disabled { opacity: 0.4; cursor: not-allowed; }
.hzo-qty__input {
    width: var(--qty-input-width, 3ch);
    text-align: center;
    border: 0;
    background: transparent;
    font-family: var(--font-body);
    font-weight: var(--fw-600);
    color: var(--text-heading);
    -moz-appearance: textfield;
}
.hzo-qty__input::-webkit-outer-spin-button,
.hzo-qty__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.hzo-qty__input:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 1px; border-radius: var(--radius-input); }
@media (prefers-reduced-motion: reduce) {
    .hzo-qty__btn { transition: none; }
}


/* --- quick-view/quick-view.css --- */
/* ═══════════════════════════════════════════════════════════════════════════
   PRODUCT QUICK-VIEW MODAL (W-132) — styles

   Scoped to .hzo-quick-view*. The OVERLAY chrome (backdrop, panel, open/close
   motion, scroll-lock, z-index) is inherited from components/modal (.hzo-modal /
   .hzo-modal__dialog) — this sheet only styles the trigger affordance and the
   quick-view BODY (gallery, details, skeleton, error). Tokens only: no bare hex,
   no bare z-index, no bare ms/ease. z-index comes entirely from the modal layer
   (--z-modal). Reduced-motion stops the skeleton shimmer (PRESERVE-5: the
   aria-busy state + hzo.announce carry "loading", so no motion is required).
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Trigger affordance (composes on .hzo-btn) ─────────────────────────────── */
.hzo-quick-view {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
}
.hzo-quick-view__icon {
    width: 1.15em;
    height: 1.15em;
    flex-shrink: 0;
}
/* Compact card-corner variant: icon only, but keeps a real accessible name. */
.hzo-quick-view--icon-only {
    gap: 0;
    min-width: 44px;
    min-height: 44px;
    padding-left: var(--space-sm);
    padding-right: var(--space-sm);
}
.hzo-quick-view__sr-only {
    position: absolute !important;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ── Quick-view dialog sizing (wider than the base modal for two columns) ──── */
.hzo-quick-view__dialog {
    width: 100%;
    max-width: 860px;
}
.hzo-quick-view__header {
    /* No title row — the close button sits top-right over the body. Keep it in
       the flow but let the grid start at the top. */
    padding-bottom: 0;
    justify-content: flex-end;
    min-height: 0;
}
.hzo-quick-view__body {
    padding-top: var(--space-sm);
}

/* ── Layout grid ───────────────────────────────────────────────────────────── */
.hzo-quick-view__grid {
    display: grid;
    grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
    gap: var(--space-lg);
    align-items: start;
}

/* ── Gallery ───────────────────────────────────────────────────────────────── */
.hzo-quick-view__stage {
    border-radius: var(--radius-card);
    overflow: hidden;
    background: var(--bg-alt);
}
.hzo-quick-view__main-wrap { display: block; }
.hzo-quick-view__main,
.hzo-quick-view__main-wrap img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
    object-fit: cover;
}
.hzo-quick-view__thumbs {
    list-style: none;
    margin: var(--space-sm) 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
}
.hzo-quick-view__thumb-item { margin: 0; }
.hzo-quick-view__thumb {
    display: block;
    width: 56px;
    height: 56px;
    padding: 0;
    border: 2px solid transparent;
    border-radius: var(--radius-input);
    background: var(--bg-alt);
    cursor: pointer;
    overflow: hidden;
    transition: border-color var(--dur-2) var(--ease-standard);
}
.hzo-quick-view__thumb img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.hzo-quick-view__thumb:hover { border-color: var(--text-muted); }
.hzo-quick-view__thumb.is-active { border-color: var(--brand); }
.hzo-quick-view__thumb:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
}

/* ── Details column ────────────────────────────────────────────────────────── */
.hzo-quick-view__details {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}
.hzo-quick-view__title {
    margin: 0;
    font-family: var(--font-heading);
    font-size: 24px;
    line-height: 1.25;
    color: var(--text-heading);
}
.hzo-quick-view__title:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 3px;
    border-radius: var(--radius-input);
}
.hzo-quick-view__rating { display: inline-flex; }
.hzo-quick-view__price {
    font-family: var(--font-number, var(--font-heading));
    font-size: 22px;
    font-weight: 700;
    color: var(--text-heading);
}
.hzo-quick-view__price del { color: var(--text-muted); font-weight: 400; margin-right: var(--space-xs); }
.hzo-quick-view__price ins { text-decoration: none; }
.hzo-quick-view__desc {
    color: var(--text-body);
    line-height: 1.6;
}
.hzo-quick-view__desc > :first-child { margin-top: 0; }
.hzo-quick-view__desc > :last-child { margin-bottom: 0; }

/* ── Buy row (qty + engine ATC button) ─────────────────────────────────────── */
.hzo-quick-view__buy {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
    margin-top: var(--space-xs);
}
.hzo-quick-view__buy--link { display: block; }
.hzo-quick-view__add { flex: 1 1 auto; }

.hzo-quick-view__full {
    margin-top: var(--space-xs);
    font-size: 14px;
    color: var(--text-muted);
    text-decoration: underline;
    text-underline-offset: 2px;
    align-self: flex-start;
}
.hzo-quick-view__full:hover { color: var(--brand); }

/* ── Loading skeleton (§2 async-gap; REDUCED-MOTION-SLOWS-STATUS / G47) ─────── */
.hzo-qv-skel {
    position: relative;
    overflow: hidden;
    border-radius: var(--radius-input);
    background: var(--bg-alt);
}
.hzo-qv-skel::after {
    content: "";
    position: absolute;
    inset: 0;
    transform: translateX(-100%);
    background: linear-gradient(90deg, transparent 0, var(--surface) 50%, transparent 100%);
    opacity: 0.55;
    animation: hzo-qv-shimmer calc(var(--dur-4) * 3) var(--ease-standard) infinite;
}
.hzo-qv-skel--stage { aspect-ratio: 1 / 1; border-radius: var(--radius-card); }
.hzo-qv-skel--title { height: 28px; width: 80%; }
.hzo-qv-skel--price { height: 24px; width: 40%; }
.hzo-qv-skel--line  { height: 14px; width: 100%; }
.hzo-qv-skel--short { width: 60%; }
.hzo-qv-skel--buy   { height: 48px; width: 100%; margin-top: var(--space-sm); }

@keyframes hzo-qv-shimmer {
    to { transform: translateX(100%); }
}

/* ── Error state (§1 error / §2 fail — names failure + recovery) ───────────── */
.hzo-quick-view__error {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    align-items: flex-start;
    padding: var(--space-lg) 0;
}
.hzo-quick-view__error-title {
    margin: 0;
    font-family: var(--font-heading);
    font-size: 20px;
    color: var(--text-heading);
}
.hzo-quick-view__error-text { margin: 0; color: var(--text-body); }
.hzo-quick-view__error-actions { display: flex; gap: var(--space-sm); }

/* ── Phone: single column, dialog docks as a bottom sheet (modal.css) ──────── */
@media (max-width: 640px) {
    .hzo-quick-view__grid { grid-template-columns: 1fr; }
    .hzo-quick-view__stage { max-width: 320px; margin-inline: auto; }
}

/* ── Reduced motion: freeze the shimmer to a static placeholder (no busy
     freeze — the aria-busy state + hzo.announce carry "loading"). ──────────── */
@media (prefers-reduced-motion: reduce) {
    .hzo-qv-skel::after { animation: none; }
    .hzo-quick-view__thumb { transition: none; }
}

/* ── Forced colors / High Contrast (§12.2): skeleton + active thumb readable
     without relying on tinted backgrounds. ─────────────────────────────────── */
@media (forced-colors: active) {
    .hzo-qv-skel { border: 1px solid CanvasText; background: Canvas; }
    .hzo-qv-skel::after { animation: none; background: none; }
    .hzo-quick-view__thumb.is-active { border-color: Highlight; }
}


/* --- range/range.css --- */
/* range — @tokens: --range-track-height, --range-thumb-size

   A styled `<input type="range">` slider. The root IS the input — `.hzo-range`
   sits directly on the element, so both vendor track + thumb pseudo-elements are
   painted from here. Native chrome is stripped (`appearance: none`) and rebuilt
   token-only.

   Token-only: the control's track HEIGHT and thumb DIAMETER have no semantic
   token in the scale, so they're component tokens with a literal fallback — the
   same dial pattern icon-button.css / chip.css use. Everything else reads a
   semantic/L1 token.

   Vendor caveat: WebKit and Firefox expose the track + thumb through DIFFERENT,
   non-groupable pseudo-elements (a browser drops the whole selector list if it
   doesn't recognise one pseudo), so each rule is written per-vendor. The focus
   ring goes on the THUMB via box-shadow — `outline` on a range input is unreliable
   across engines. */
.hzo-range {
    appearance: none;
    -webkit-appearance: none;
    width: 100%;
    background: transparent;
    cursor: pointer;
}

/* ─── Track ─── height + pill fill on the alt surface. Per-vendor (not groupable). */
.hzo-range::-webkit-slider-runnable-track {
    height: var(--range-track-height, 6px);
    background: var(--bg-alt);
    border-radius: var(--radius-full);
}
.hzo-range::-moz-range-track {
    height: var(--range-track-height, 6px);
    background: var(--bg-alt);
    border-radius: var(--radius-full);
}

/* ─── Thumb ─── brand circle. WebKit needs `-webkit-appearance: none` + a
   `margin-top` to vertically centre the thumb on the track (thumb-size vs
   track-height, halved and negated). Firefox centres its thumb natively. */
.hzo-range::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: var(--range-thumb-size, 18px);
    height: var(--range-thumb-size, 18px);
    margin-top: calc((var(--range-track-height, 6px) - var(--range-thumb-size, 18px)) / 2);
    background: var(--brand);
    border: 0;
    border-radius: var(--radius-full);
}
.hzo-range::-moz-range-thumb {
    width: var(--range-thumb-size, 18px);
    height: var(--range-thumb-size, 18px);
    background: var(--brand);
    border: 0;
    border-radius: var(--radius-full);
}

/* A11y — required. The ring is an OUTLINE on the input itself (offset out from
   the track): visible AND reliably detectable, unlike a thumb-only pseudo ring
   (which several engines don't expose to getComputedStyle). Disabled state. */
.hzo-range:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 4px;
    border-radius: var(--radius-full);
}
.hzo-range[disabled],
.hzo-range[aria-disabled="true"],
.hzo-range.is-disabled {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}


/* --- rate-menu/rate-menu.css --- */
/* ─── Service Rate Menu (W-080) ────────────────────────────────────────
   A categorized, scannable rate card: rows of "{service} … {price}" on
   dotted-leader lines, grouped under real <h3> category headings, with
   optional per-row badges + a book/quote/call CTA. Category B (no JS) — the
   "what's included" disclosure is a native <details>.

   Token-only: colors/space/radius/shadow all resolve to the design-token
   layer (assets/css/tokens.css). No raw hex/px where a token exists. The
   decorative leader is currentColor + aria-hidden (forced-colors safe); price
   emphasis is weight + font, never color-only.
   Shortcode: rate-menu.php [hzo_rate_menu] + child [hzo_rate_item]. */

.hzo-rate-menu {
    display: block;
}

/* Category heading — clusters the rows below it. First heading hugs the top;
   later ones get breathing room. (Real <h3> inherits the type ramp/color.) */
.hzo-rate-menu__category {
    margin: var(--space-xl) 0 var(--space-md);
}
.hzo-rate-menu__category:first-child {
    margin-top: 0;
}

.hzo-rate-menu__list {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* ── The leader row ──────────────────────────────────────────────────────
   grid: name | leader (fills) | price. Extra lines (desc / aside / details)
   span the full width beneath. */
.hzo-rate-row {
    display: grid;
    grid-template-columns: minmax(0, auto) 1fr minmax(0, auto);
    align-items: baseline;
    column-gap: var(--space-sm);
    row-gap: var(--space-xs);
    padding: var(--space-smd) 0;
}

.hzo-rate-row__name {
    grid-column: 1;
    font-weight: var(--fw-600);
    color: var(--text-heading);
    line-height: var(--lh-snug);
}

/* The dotted leader — decorative, currentColor so it survives forced-colors.
   align-self:end + a small nudge drops it onto the text baseline. */
.hzo-rate-row__leader {
    grid-column: 2;
    align-self: end;
    height: 0;
    border-bottom: var(--border-1) dotted currentColor;
    opacity: 0.4;
    transform: translateY(-0.28em);
    transition: opacity var(--dur-2) var(--ease-standard);
}

.hzo-rate-row__price {
    grid-column: 3;
    justify-self: end;
    font-family: var(--font-number);
    font-weight: var(--fw-700);
    color: var(--text-heading);
    white-space: nowrap;
    transition: color var(--dur-2) var(--ease-standard);
}

/* Below-name supporting copy */
.hzo-rate-row__desc {
    grid-column: 1 / -1;
    margin: 0;
    color: var(--text-muted);
    font-size: var(--fs-100);
    line-height: var(--lh-normal);
}

/* Badges + CTA aside */
.hzo-rate-row__aside {
    grid-column: 1 / -1;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-top: var(--space-xs);
}
.hzo-rate-row__badges {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
}

/* Per-row CTA — composes .hzo-btn .hzo-btn--outline .hzo-btn--sm; only the
   icon size is component-specific. min-height 36px (btn--sm) clears WCAG 2.5.8
   (24px min); comfortable tap target on touch. */
.hzo-rate-row__cta {
    margin-left: auto;
}
.hzo-rate-row__cta-icon {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
}

/* "What's included" — native <details> Disclosure (APG). */
.hzo-rate-row__details {
    grid-column: 1 / -1;
}
.hzo-rate-row__details-toggle {
    cursor: pointer;
    color: var(--brand-text);
    font-size: var(--fs-100);
    font-weight: var(--fw-500);
    padding: var(--space-xs) 0;
    width: fit-content;
}
.hzo-rate-row__details-toggle:focus-visible {
    outline: var(--border-3) solid var(--focus-ring);
    outline-offset: 2px;
    border-radius: var(--radius-1);
}
.hzo-rate-row__details-body {
    margin: var(--space-xs) 0 0;
    color: var(--text-body);
    font-size: var(--fs-100);
    line-height: var(--lh-normal);
}

/* ── Hover emphasis (has a resting + tap equivalent — G50; never a hover-only
   reveal) ──────────────────────────────────────────────────────────────── */
.hzo-rate-row:hover .hzo-rate-row__leader {
    opacity: 0.65;
}
.hzo-rate-row:hover .hzo-rate-row__price {
    color: var(--brand-text);
}

/* ── Leader-fill variants ──────────────────────────────────────────────── */
.hzo-rate-menu--leader-rule .hzo-rate-row__leader {
    border-bottom-style: solid;
    opacity: 0.25;
}
.hzo-rate-menu--leader-none .hzo-rate-row__leader {
    border-bottom: 0;
}

/* ── Cards layout ──────────────────────────────────────────────────────── */
.hzo-rate-menu--cards .hzo-rate-menu__list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: var(--space-md);
}
.hzo-rate-menu--cards .hzo-rate-row {
    grid-template-columns: 1fr;
    align-items: start;
    padding: var(--space-md);
    background: var(--surface);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-sm);
}
.hzo-rate-menu--cards .hzo-rate-row__leader {
    display: none;
}
.hzo-rate-menu--cards .hzo-rate-row__name {
    font-size: var(--fs-300);
}
.hzo-rate-menu--cards .hzo-rate-row__price {
    justify-self: start;
    font-size: var(--fs-400);
    margin-top: var(--space-xs);
}

/* ── Print — the rate card is exactly what customers print/PDF (G13). Reveal
   every disclosure, drop the decorative leaders + the interactive CTAs, keep
   all prices/rows. ─────────────────────────────────────────────────────── */
@media print {
    .hzo-rate-row__leader,
    .hzo-rate-row__cta {
        display: none !important;
    }
    /* Force-open closed <details> so nothing below the fold is hidden on paper. */
    .hzo-rate-row__details > *:not(summary) {
        display: block !important;
    }
    .hzo-rate-row__details-toggle {
        font-weight: var(--fw-600);
    }
}

/* ── Forced colors — leader/badge/price must not rely on color alone. ────── */
@media (forced-colors: active) {
    .hzo-rate-row__leader {
        opacity: 1;
        border-bottom-color: currentColor;
    }
    .hzo-rate-row__cta:focus-visible,
    .hzo-rate-row__details-toggle:focus-visible {
        outline: var(--border-2) solid;
    }
}

/* ── Reduced motion — kill this component's transitions (PRESERVE-5). ────── */
@media (prefers-reduced-motion: reduce) {
    .hzo-rate-row__leader,
    .hzo-rate-row__price {
        transition: none;
    }
}


/* --- rating/rating.css --- */
/* ─── Rating (static stars) ───────────────────────────────────────────
   Read-only star row for trust / review contexts.

   STAR FILL — --rating-star-color (PF-031). Defaults to --color-warning, the
   engine's amber, so this is byte-identical to the previous hardcoded value.
   The token exists so a site can darken its stars WITHOUT moving --color-warning
   and every warning surface with it.

   ⛔ Constraint, measured, and it is the reason the token exists. Stars are
   non-text content, so WCAG 1.4.11 wants 3:1 against their background. The
   default amber #D97706 against the three light section surfaces the engine
   ships:

       --bg-page   #FFFFFF                white   3.19:1   passes, narrowly
       --bg-alt    #F9FAFB                pale    3.05:1   passes, barely
       --brand-pale (10% brand + white)   tint     2.68:1   FAILS

   So: STAR RATINGS MUST NOT SIT ON .hzo-section--tint. The tint figure was
   computed for one client's brand (#004A3C -> --brand-pale ~#E6EDEC), but
   --brand-pale is a fixed 10% mix, so ANY brand dark enough to be usable as a
   brand colour lands in the same range. The amber has ~0.19 of headroom on
   white and a 10% tint consumes all of it.

   A site that needs stars on tint must set --rating-star-color to something
   darker and re-measure; the default is not safe there and no amount of
   token indirection changes that.

   Empty stars use --border. Author renders N filled + (5−N) empty star SVGs,
   optional count after. For AT, put role="img" + aria-label="Rated 4.8 out of
   5" on the .hzo-rating container and aria-hidden on the SVGs.
   Or generate all of it: [hzo_rating value="4" count="4.0 (128 reviews)"]
   (rating.php) emits this exact contract. */
.hzo-rating { display: inline-flex; align-items: center; gap: 2px; }
.hzo-rating svg { width: 18px; height: 18px; flex-shrink: 0; }
/* Whole-star classes — still emitted by testimonial.php's helper and by
   hand-authored markup (the [hzo_rating] shortcode now uses the fractional
   layers below). */
.hzo-rating__star { color: var(--rating-star-color, var(--color-warning)); }
.hzo-rating__star--empty { color: var(--border); }
.hzo-rating__count { margin-left: var(--space-sm); font-size: var(--fs-100); color: var(--text-muted); }

/* G42 — fractional star row (used by [hzo_rating]): a gray base row + an amber
   fill clipped to value/5, so 4.6 renders as 4.6 stars. Same two-layer clip the
   reviews widget uses; --hzo-stars-pct is set inline on the .hzo-rating root. */
.hzo-rating__stars { position: relative; display: inline-flex; line-height: 0; flex-shrink: 0; }
.hzo-rating__stars-base,
.hzo-rating__stars-fill { display: inline-flex; gap: 2px; }
.hzo-rating__stars-base { color: var(--border); }
.hzo-rating__stars-fill {
    position: absolute;
    inset: 0 auto 0 0;
    width: var(--hzo-stars-pct, 0%);
    overflow: hidden;
    white-space: nowrap;
    color: var(--rating-star-color, var(--color-warning));
}
/* Windows High-Contrast: clip layers collapse to system colors — keep the fill
   visible as Highlight so the rating isn't lost. */
@media (forced-colors: active) { .hzo-rating__stars-fill { color: Highlight; } }


/* --- rating-input/rating-input.css --- */
/* rating-input — @tokens: --rating-input-size, --rating-input-gap

   The accessible, keyboard-operable, form-submittable star-rating INPUT — a
   radio group styled as a row of stars. Sibling to the read-only `.hzo-rating`
   (static display stars); this is the WRITE version, so it lives on its own root
   with its own tokens (do NOT borrow `.hzo-rating`'s).

   Intended markup (the coordinator wires this into the kitchen-sink): five
   <input type="radio" name="…"> each immediately followed by its <label> star,
   emitted in DESCENDING value order (5→1). The container reverses them so DOM
   5→1 renders visually 1→5 left-to-right, which is what lets the pure-CSS sibling
   combinator fill a star + everything to its visual left:

     <span class="hzo-rating-input">
       <input type="radio" id="r5" name="demo-rating" value="5" class="hzo-rating-input__radio">
       <label for="r5" class="hzo-rating-input__star" aria-label="5 stars"><svg…/></label>
       …r4, r3, r2, r1…   (DESCENDING — 5 first, 1 last)
     </span>

   FILL DIRECTION (the fiddly part — verified): with row-reverse a later DOM
   sibling paints to the LEFT. A checked/hovered star at value N has later DOM
   siblings N-1…1, which render visually leftward = the lower values. So
   `:checked ~ __star` (plus the checked radio's own adjacent `+` label) fills
   value N and everything below it — the star + its leftward run. `:hover, :hover
   ~ __star` gives the same leftward preview run. This is exactly the star-rating
   convention (select 4 → stars 1–4 lit).

   Token-only: the star box has no semantic dimension token in the scale, so it's
   a component token with a literal fallback — the same dial pattern
   icon-button.css uses (`var(--icon-button-size, 40px)`). Empty stars read
   `--border`; filled stars read `--accent` (the engine's interactive brand-accent
   highlight; the display `.hzo-rating` uses `--warning` amber, but this control
   aligns to the accent). Everything else reads a semantic/L1 token. */
.hzo-rating-input {
    display: inline-flex;
    flex-direction: row-reverse;   /* DOM 5→1 renders visually 1→5 */
    justify-content: flex-end;
    align-items: center;
    gap: var(--rating-input-gap, var(--space-2xs));
}

/* Radio — visually hidden but still focusable & form-submittable (sr-only). Never
   display:none — that removes it from the tab order and the submitted form. */
.hzo-rating-input__radio {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    opacity: 0;
}

/* Star — the clickable <label> for its radio. Empty = --border. */
.hzo-rating-input__star {
    width: var(--rating-input-size, 28px);
    height: var(--rating-input-size, 28px);
    color: var(--border);
    cursor: pointer;
    transition: color var(--transition-base) ease;
}
.hzo-rating-input__star > svg {
    width: 100%;
    height: 100%;
    display: block;
}

/* Fill — a checked star and every star to its visual left light up. The checked
   radio's own label is the adjacent `+`; the lower-value stars are the later `~`
   siblings (leftward under row-reverse). */
.hzo-rating-input__radio:checked + .hzo-rating-input__star,
.hzo-rating-input__radio:checked ~ .hzo-rating-input__star {
    color: var(--accent);
}

/* Hover preview — same leftward run: the hovered star + its later `~` siblings
   (lower values, visually left). */
.hzo-rating-input__star:hover,
.hzo-rating-input__star:hover ~ .hzo-rating-input__star {
    color: var(--accent);
}

/* A11y — required. The radio is hidden, so the focus ring rides its label star. */
.hzo-rating-input__radio:focus-visible + .hzo-rating-input__star {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}
.hzo-rating-input__radio:disabled + .hzo-rating-input__star,
.hzo-rating-input.is-disabled {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-rating-input__star { transition: none; }
}


/* --- recent/recent.css --- */
/* recent — recent-posts layout for the [hzo_recent_posts] widget. Two layouts:
   a FEATURED card (image over a text stack) and a COMPACT list (thumbnail beside
   title + date). The shortcode renders the markup (includes/recent-posts-
   shortcode.php); this sheet is layout only. Token-only, brand-agnostic. */

/* ── Featured card ────────────────────────────────────────────────────────── */
.hzo-recent--featured .hzo-recent__card--featured {
    display: flex;
    flex-direction: column;
}
.hzo-recent__card--featured .hzo-recent__media {
    display: block;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    border-radius: var(--radius-card, 12px);
    background: var(--bg-alt);
    margin-bottom: var(--space-md);
}
.hzo-recent__card--featured .hzo-recent__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.hzo-recent__eyebrow {
    font-size: var(--eyebrow-size, 15px);
    font-weight: var(--fw-semibold, 600);
    text-transform: uppercase;
    letter-spacing: var(--eyebrow-tracking, 0.7px);
    color: var(--accent-text);
    margin-bottom: var(--space-xs);
}
.hzo-recent__excerpt {
    color: var(--text-muted);
    line-height: 1.6;
    margin: var(--space-sm) 0 0;
}
.hzo-recent__card--featured .hzo-recent__btn {
    margin-top: var(--space-md);
    align-self: flex-start;
}

/* ── Compact list ─────────────────────────────────────────────────────────── */
.hzo-recent--list {
    display: flex;
    flex-direction: column;
}
.hzo-recent__card--compact {
    display: flex;
    flex-direction: row;
    gap: var(--space-md);
    align-items: center;
    padding: var(--space-md) 0;
    border-bottom: 1px solid var(--border);
}
.hzo-recent__card--compact:first-child { padding-top: 0; }
.hzo-recent__card--compact .hzo-recent__media {
    flex: 0 0 92px;
    width: 92px;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: var(--radius-input, 8px);
    background: var(--bg-alt);
    display: block;
}
.hzo-recent__card--compact .hzo-recent__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.hzo-recent__card--compact .hzo-recent__body {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.hzo-recent__date {
    font-size: 12px;
    color: var(--text-muted);
    font-weight: var(--fw-semibold, 600);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.hzo-recent__title {
    margin: 0;
    font-size: 1.05rem;
    line-height: 1.35;
}
/* Title links are bare <a> (headings), so a client's broad bare-link skin
   (e.g. `:where(main) a:not([class])`) matches them at equal (0,1,1) specificity
   and would win by load order now that this rule lives in the engine. The __card
   ancestor lifts this to (0,2,1) so the component robustly owns its own
   heading-ink title color regardless of any client bare-link skin. A client
   highlight can also ride in via `background-image` (a highlight-wipe underline),
   which this reset neutralizes too — a title link is a heading, never an inline
   body link. */
.hzo-recent__card .hzo-recent__title a { color: var(--text-heading); text-decoration: none; background-image: none; }
.hzo-recent__card .hzo-recent__title a:hover { color: var(--brand-text); }


/* --- ribbon/ribbon.css --- */
/* ─── Ribbon — rotated corner label ───────────────────────────────────
   Diagonal badge across a card/tile corner ("Popular", "Sale"). The parent
   needs position:relative + overflow:hidden — .hzo-tile and .hzo-card__media
   already qualify. Meaningful text stays in the DOM (not aria-hidden). */
.hzo-ribbon {
    position: absolute;
    top: 18px;
    right: -34px;
    z-index: 2;
    transform: rotate(45deg);
    background: var(--ribbon-bg, var(--accent));
    color: var(--ribbon-color, var(--text-on-accent));
    font-family: var(--font-body);
    font-size: var(--fs-50);
    font-weight: var(--fw-700, 700);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    line-height: var(--lh-none);
    text-align: center;
    padding: 6px 40px;
    box-shadow: var(--shadow-sm);
    pointer-events: none;
}
.hzo-ribbon--tl { right: auto; left: -34px; transform: rotate(-45deg); }


/* --- scroll-progress/scroll-progress.css --- */
/* scroll-progress — @tokens: --scroll-progress-height */
/* ─── Scroll progress ─────────────────────────────────────────────────
   Thin reading-progress bar fixed at the top of the viewport, filling
   left→right as the page scrolls. Decorative (aria-hidden in markup, not
   focusable). JS sets the fill ratio via the --hzo-scroll-progress custom
   property (0..1); the bar is scaled on the X axis from that ratio. No
   transition — the value updates every scroll frame, so a transition would
   lag behind the pointer; hence no reduced-motion block is needed (this is
   scroll-driven, not autonomous motion). */
.hzo-scroll-progress {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--scroll-progress-height, 3px);
    background: transparent;
    z-index: var(--z-overlay);
    pointer-events: none;
}

.hzo-scroll-progress__bar {
    display: block;
    height: 100%;
    width: 100%;
    background: var(--brand);
    transform-origin: left center;
    transform: scaleX(var(--hzo-scroll-progress, 0));
}

/* ── Colour modifier (recolour the bar) ── */
.hzo-scroll-progress--accent .hzo-scroll-progress__bar {
    background: var(--accent);
}


/* --- section-divider/section-dividers.css --- */
/* ─── Section dividers — wave + mosaic ────────────────────────────────
   Decorative seams between sections; both are zero-JS. Place a divider
   BETWEEN two <section>s: its fill color should match the NEXT section's
   background so the shape reads as that section's top edge.

   Wave: inline SVG (fill="currentColor") drifting slowly sideways.
     .hzo-wave-divider          color = --wave-fill (default --bg-alt)
     .hzo-wave-divider--flip    mirror vertically (use above a section end)
     .hzo-wave-divider--static  no drift even with motion allowed
   Drift is ambient background motion → always disabled under reduced motion
   (do NOT copy the old client build's opt-out). Markup: blocks/section-divider-wave.html.

   Mosaic: two thin rows of "tiles" from pure repeating-gradients — free
   texture, no images, auto-themes from --brand (override --mosaic-c1..c4).
   Markup: blocks/section-divider-mosaic.html. */
.hzo-wave-divider {
    position: relative;
    overflow: hidden;
    height: var(--wave-height, 70px);
    color: var(--wave-fill, var(--bg-alt));
    margin-bottom: -1px;   /* kill the sub-pixel seam against the next section */
}
.hzo-wave-divider svg {
    position: absolute;
    left: 0;
    bottom: 0;
    width: 200%;
    height: 100%;
    display: block;
    animation: hzo-wave-drift var(--wave-duration, 12s) ease-in-out infinite alternate;
}
.hzo-wave-divider--flip { transform: scaleY(-1); margin-bottom: 0; margin-top: -1px; }
.hzo-wave-divider--static svg { animation: none; }
@media (min-width: 768px) {
    .hzo-wave-divider { height: var(--wave-height-lg, 100px); }
}
@keyframes hzo-wave-drift {
    from { transform: translateX(0); }
    to   { transform: translateX(-25%); }
}
@media (prefers-reduced-motion: reduce) {
    .hzo-wave-divider svg { animation: none; }
}

.hzo-mosaic-divider {
    /* Private per-instance palette: overridable via --mosaic-c1..c4, defaults
       derive from the brand so the band auto-themes on every client. */
    --_mc1: var(--mosaic-c1, color-mix(in srgb, var(--brand) 25%, white));
    --_mc2: var(--mosaic-c2, color-mix(in srgb, var(--brand) 45%, white));
    --_mc3: var(--mosaic-c3, color-mix(in srgb, var(--brand) 65%, white));
    --_mc4: var(--mosaic-c4, color-mix(in srgb, var(--brand) 35%, white));
    background: var(--mosaic-bg, var(--brand-pale));
    padding: var(--space-md) 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}
.hzo-mosaic-divider__row {
    height: 26px;
    background-image: repeating-linear-gradient(to right,
        var(--_mc1) 0,    var(--_mc1) 26px,  transparent 26px,  transparent 30px,
        var(--_mc2) 30px, var(--_mc2) 56px,  transparent 56px,  transparent 60px,
        var(--_mc3) 60px, var(--_mc3) 86px,  transparent 86px,  transparent 90px,
        var(--_mc4) 90px, var(--_mc4) 116px, transparent 116px, transparent 120px);
}
.hzo-mosaic-divider__row--offset { background-position: 60px 0; }


/* --- section-divider/section-divider.css --- */
/* ─── Section divider (loader manifest) ───────────────────────────────
   The actual rules live in section-dividers.css (plural — the filename
   predates widgetization and is still @imported by the legacy
   assets/css/primitives.css fallback at its line 70, so it keeps its
   name rather than breaking that path). The loader enqueues ONLY
   <name>.css, so this manifest @imports the sheet to give it a load
   path in components mode (the wave-4 video.css lesson: before this
   file existed, the wave/mosaic divider styles had NO load path on a
   normal checkout — primitives.css is only enqueued in fallback mode).
   In the rare double-fallback case both paths import the same sheet;
   identical rules twice are a harmless no-op.

   ⚠ REACH FOR _base/section-edges.css FIRST (Wave S). This component's paths
   are FROZEN shapes: you pick one of four and you are done — no angle, no
   size, no way to make the boundary belong to the two surfaces it sits
   between. _base/section-edges.css is the parameterized alternative
   (--edge-color / --edge-size / --edge-angle / --edge-radius, six shapes,
   top or bottom) and it always loads. This sheet stays supported and
   unchanged for every page already using it; it is simply no longer the
   default answer to "how should this boundary look". */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */


/* --- section-intro/section-intro.css --- */
/* ─── Section Intro Header (.hzo-section-intro) ─────────────────────────────
   W-090 — the standardized opener for every marketing section: eyebrow,
   semantically-correct heading (level decoupled from visual size), a
   measure-capped paragraph, and an optional actions row (0–2 buttons, an APG
   tablist, or a filter-chip group). Composes existing primitives (button/tag
   tokens, .hzo-reveal, divider tokens) and owns only the layout + a11y
   scaffolding.

   GATING: always-load (the shortcode un-gates). The common eyebrow+heading+text
   case is pure CSS — zero runtime effect (section-intro.js only does work when a
   filter group or a tablist is present, plus a delegated tel: mirror).

   Tokens only: --space-* (rhythm), --border / --border-* (divider + chip border),
   --brand* / --accent-text (chip + eyebrow ink), --radius-* (chip pill),
   --fs-h1..h3 (visual size ladder), --dur-* / --ease-* (chip/tab transitions),
   --font-heading / --font-body. No hardcoded hex, no bare z-index (the intro is
   flow content — it claims none), px only for the hairline border + icon sizing. */

.hzo-section-intro {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-md);
    margin: 0;
}

/* ── Uniform bottom spacing (opener → the content it introduces) ───────────── */
.hzo-section-intro--space-sm { margin-block-end: var(--space-lg); }
.hzo-section-intro--space-md { margin-block-end: var(--space-xl); }
.hzo-section-intro--space-lg { margin-block-end: var(--space-2xl); }

/* ── Hairline divider (shared --border token; padding matches the spacing) ── */
.hzo-section-intro--rule-top,
.hzo-section-intro--rule-both { border-block-start: 1px solid var(--border); padding-block-start: var(--space-lg); }
.hzo-section-intro--rule-bottom,
.hzo-section-intro--rule-both { border-block-end: 1px solid var(--border); padding-block-end: var(--space-lg); }

/* ── Alignment (left default; center for hero-style openers) ──────────────── */
.hzo-section-intro--align-center {
    align-items: center;
    text-align: center;
}
.hzo-section-intro--align-center .hzo-section-intro__text { margin-inline: auto; }

/* ── Eyebrow — ALWAYS a <p> (never a heading; can't pollute the outline) ──── */
.hzo-section-intro__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    margin: 0;
    font-family: var(--font-body);
    line-height: 1.3;
}
/* text: uppercase kicker on accent ink (mirrors the shared .hzo-eyebrow look). */
.hzo-section-intro__eyebrow--text,
.hzo-section-intro__eyebrow--icon {
    font-size: var(--eyebrow-size, 15px);
    font-weight: var(--fw-semibold, 600);
    text-transform: uppercase;
    letter-spacing: var(--eyebrow-tracking, 0.7px);
    color: var(--accent-text);
}
/* tag: a quiet bordered chip. pill: a filled brand-pale pill. Both are static
   (a <p>, not interactive) — sentence-cased kicker inside a token. */
.hzo-section-intro__eyebrow--tag,
.hzo-section-intro__eyebrow--pill {
    padding: 5px 12px;
    font-size: 13px;
    font-weight: var(--fw-semibold, 600);
    line-height: 1.2;
    letter-spacing: var(--tracking-wide, 0.05em);
    text-transform: uppercase;
    border-radius: var(--radius-pill, var(--radius-full));
}
.hzo-section-intro__eyebrow--tag {
    color: var(--brand-text);
    background: var(--bg-alt);
    border: 1px solid var(--border);
}
.hzo-section-intro__eyebrow--pill {
    color: var(--brand-text);
    background: var(--brand-pale);
    border: 1px solid transparent;
}
.hzo-section-intro__eyebrow-icon {
    width: 1.1em;
    height: 1.1em;
    flex-shrink: 0;
}

/* ── Heading — semantic <h2>/<h3>, VISUAL size from the root modifier. ──────
   text-wrap:balance for a tidy multi-line headline; the id (for anchors/TOC)
   lives on this element, not the wrapper. */
.hzo-section-intro__heading {
    margin: 0;
    font-family: var(--font-heading);
    line-height: 1.15;
    text-wrap: balance;
    color: var(--h2-color, var(--text-heading));
}
.hzo-section-intro--size-sm .hzo-section-intro__heading {
    font-size: calc(var(--fs-h3) * var(--hzo-type-scale, 1) * var(--hzo-heading-scale, 1));
    font-weight: var(--font-weight-heading, var(--h3-weight, 600));
}
.hzo-section-intro--size-md .hzo-section-intro__heading {
    font-size: calc(var(--fs-h2) * var(--hzo-type-scale, 1) * var(--hzo-heading-scale, 1));
    font-weight: var(--font-weight-heading, var(--h2-weight, 700));
}
.hzo-section-intro--size-lg .hzo-section-intro__heading {
    font-size: calc(var(--fs-h1) * var(--hzo-type-scale, 1) * var(--hzo-heading-scale, 1));
    font-weight: var(--font-weight-heading, var(--h1-weight, 700));
}

/* ── Supporting text — measure-capped for readability (~45–75ch). ─────────── */
.hzo-section-intro__text {
    margin: 0;
    max-inline-size: var(--hzo-intro-measure, 60ch);
    color: var(--text-body);
    font-size: var(--fs-body);
    line-height: var(--body-line-height, 1.6);
}

/* ── Actions row (buttons | tabs | filters) ───────────────────────────────── */
.hzo-section-intro__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
    margin: 0;
    padding: 0;
    margin-block-start: var(--space-xs);
}
.hzo-section-intro--align-center .hzo-section-intro__actions { justify-content: center; }
.hzo-section-intro__tabs-root { align-self: stretch; }
.hzo-section-intro--align-center .hzo-section-intro__tabs-root .hzo-section-intro__actions--tabs { justify-content: center; }

/* Tabs/filters can overflow a narrow container — scroll horizontally rather than
   wrap into a ragged block; the group keeps its role/aria intact. */
.hzo-section-intro__actions--tabs,
.hzo-section-intro__actions--filters {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}
.hzo-section-intro__actions--tabs::-webkit-scrollbar,
.hzo-section-intro__actions--filters::-webkit-scrollbar { display: none; }

/* ── Chip (shared by filter toggle-buttons AND tabs) ─────────────────────────
   A real <button> with text; selected state is carried by aria-pressed / .is-active
   (attribute, never :hover — G50). ≥44px comfort target (≥24 floor). */
.hzo-section-intro__chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    min-height: 44px;
    padding: 8px 16px;
    font-family: var(--font-body);
    font-size: 14px;
    font-weight: var(--fw-medium, 500);
    line-height: 1.2;
    white-space: nowrap;
    color: var(--text-body);
    background: var(--bg-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill, var(--radius-full));
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background var(--dur-1) var(--ease-standard),
                border-color var(--dur-1) var(--ease-standard),
                color var(--dur-1) var(--ease-standard);
}
.hzo-section-intro__chip:hover {
    background: var(--brand-pale);
    border-color: var(--brand);
    color: var(--brand-text);
}
.hzo-section-intro__chip:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}
.hzo-section-intro__chip:active { transform: translateY(1px); }

/* Selected filter chip (aria-pressed) + selected tab (aria-selected / .is-active)
   — state by attribute, not hover. */
.hzo-section-intro__chip.is-active,
.hzo-section-intro__chip[aria-pressed="true"],
.hzo-section-intro__chip[aria-selected="true"] {
    background: var(--brand-pale);
    border-color: var(--brand);
    color: var(--brand-text);
    font-weight: var(--fw-semibold, 600);
}

/* A filter group whose target selector matched nothing on the page — JS marks the
   whole group disabled rather than shipping a dead control (G11). */
.hzo-section-intro__actions--filters[aria-disabled="true"] .hzo-section-intro__chip {
    opacity: var(--opacity-disabled, 0.5);
    pointer-events: none;
}

/* ── Buttons row — composes the shared .hzo-btn primitive; no extra chrome. ── */
.hzo-section-intro__actions--buttons { gap: var(--space-md); }

/* ── Reduced motion — kill chip/tab transitions (state changes stay instant;
   PRESERVE-5). The reveal hook, when used, is owned/killed by main.js. ─────── */
@media (prefers-reduced-motion: reduce) {
    .hzo-section-intro__chip { transition: none; }
}

/* ── Forced colors — the selected chip must not signal via background alone
   (stripped in High Contrast): add a system-color border so aria-pressed /
   aria-selected keeps a visible correlate; focus resolves to a real outline. ── */
@media (forced-colors: active) {
    .hzo-section-intro__chip.is-active,
    .hzo-section-intro__chip[aria-pressed="true"],
    .hzo-section-intro__chip[aria-selected="true"] {
        border-color: Highlight;
        outline: 1px solid Highlight;
    }
    .hzo-section-intro__chip:focus-visible { outline: 2px solid ButtonText; }
}

/* ── Print — openers are LCP-visible, so force-reveal if the reveal hook left the
   header at opacity:0 (G13), and hide the non-functional actions row. ───────── */
@media print {
    .hzo-section-intro.hzo-reveal,
    .hzo-section-intro.hzo-reveal--init {
        opacity: 1 !important;
        transform: none !important;
    }
    .hzo-section-intro__actions--tabs,
    .hzo-section-intro__actions--filters { display: none; }
}


/* --- segmented-control/segmented-control.css --- */
/* segmented-control — @tokens: --segmented-control-gap, --segmented-control-pad, --segmented-control-option-pad-y, --segmented-control-option-pad-x

   The iOS-style segmented control: a mutually-exclusive PILL button group riding a
   track, where the selected segment lifts to a raised light pill. This is the
   wayfinding/view-switch control (List / Grid / Map) — distinct from `.hzo-tabs`,
   which are underline content-panel tabs over a panel. Batch E (navigation).

   Radio-backed control — same pattern as its siblings variation-swatch and
   rating-input: sr-only radios (still focusable & form-submittable), the focus ring
   rides the adjacent label, and checked/disabled are driven by `:checked +` /
   `:disabled +`. Each option is independent; only the CHECKED one lifts (`+`, the
   adjacent label only — no cumulative `~`).

   Intended markup (coordinator writes the demo):

     <span class="hzo-segmented-control" role="radiogroup" aria-label="View">
       <input type="radio" id="v-list" name="view" class="hzo-segmented-control__radio" checked>
       <label for="v-list" class="hzo-segmented-control__option">List</label>
       <input type="radio" id="v-grid" name="view" class="hzo-segmented-control__radio">
       <label for="v-grid" class="hzo-segmented-control__option">Grid</label>
       <input type="radio" id="v-map" name="view" class="hzo-segmented-control__radio">
       <label for="v-map" class="hzo-segmented-control__option">Map</label>
     </span>

   Token-only. The track's inner gap/padding and the option's snug pad have no
   semantic tokens in the scale (they're the sub-pixel dial that gives the raised-pill
   look), so they are component tokens with literal fallbacks — the same dial pattern
   variation-swatch/icon-button use. Everything else reads a semantic/L1 token. */
.hzo-segmented-control {
    display: inline-flex;
    align-items: center;
    gap: var(--segmented-control-gap, 2px);
    padding: var(--segmented-control-pad, 3px);
    background: var(--bg-alt);
    border-radius: var(--radius-pill);
}

/* Radio — visually hidden but still focusable & form-submittable (sr-only). Never
   display:none — that removes it from the tab order and the submitted form.
   (copied verbatim from variation-swatch__radio.) */
.hzo-segmented-control__radio {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    opacity: 0;
}

/* Option — the clickable <label> segment. Unselected reads as quiet body text. */
.hzo-segmented-control__option {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--segmented-control-option-pad-y, 6px) var(--segmented-control-option-pad-x, 16px);
    border-radius: var(--radius-control);
    color: var(--text-body);
    font-size: var(--fs-75);
    line-height: var(--lh-none);
    cursor: pointer;
    white-space: nowrap;
    transition: background var(--transition-base) ease,
                color var(--transition-base) ease,
                box-shadow var(--transition-base) ease;
}

/* Hover (unselected) — darken toward the heading colour as a cue. */
.hzo-segmented-control__option:hover {
    color: var(--text-heading);
}

/* Selected (checked) — the raised light pill: lifts off the track with a light
   fill + small shadow. The checked radio's own adjacent label is the only one lit
   (`+`, no cumulative `~`). */
.hzo-segmented-control__radio:checked + .hzo-segmented-control__option {
    background: var(--card-bg, #fff);
    color: var(--text-heading);
    box-shadow: var(--shadow-sm);
    font-weight: var(--fw-600);
}

/* A11y — required. The radio is hidden, so the focus ring rides its label option. */
.hzo-segmented-control__radio:focus-visible + .hzo-segmented-control__option {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}
.hzo-segmented-control__radio:disabled + .hzo-segmented-control__option,
.hzo-segmented-control.is-disabled {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-segmented-control__option { transition: none; }
}


/* --- service-schema/service-schema.css --- */
/* ─── Service + Offer Schema Block (.hzo-service-schema) ─────────────────
   The visible price-framing card for W-003 (service-schema.php). Structured
   content, not a composite widget: a heading, a measure-capped description,
   a price framing line, an optional priced offer list (native <details> when
   collapsed past offers-max), and an optional CTA. The Service JSON-LD is
   PHP-emitted and has no styling surface.

   Tokens only — no bare hex/px/ms, no z-index (this block never portals or
   stacks). schema-only render emits no DOM, so none of this applies there. */
.hzo-service-schema {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    max-width: var(--prose-max-width);
    padding: var(--space-lg);
    background: var(--card-bg, var(--surface));
    border: 1px solid var(--card-border, var(--border));
    border-radius: var(--radius-panel);
    box-shadow: var(--card-shadow);
}

.hzo-service-schema__heading {
    margin: 0;
    color: var(--text-heading);
}

.hzo-service-schema__desc {
    margin: 0;
    max-width: var(--prose-max-width);
    color: var(--text-body);
}

/* ── Price framing ── */
.hzo-service-schema__price {
    margin: 0;
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-sm);
}
.hzo-service-schema__price-value {
    color: var(--brand-text);
    font-family: var(--font-number, inherit);
    font-weight: var(--fw-bold, 700);
    /* Size inherits from the type-scale ladder (base.css); emphasis is carried
       by the number font + weight + brand color, not a hardcoded font-size. */
}
.hzo-service-schema__price-unit {
    color: var(--text-muted);
    font-weight: var(--fw-medium, 500);
}

/* ── Offer list + native <details> disclosure ── */
.hzo-service-schema__offers {
    margin: 0;
}
.hzo-service-schema__offers-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}
.hzo-service-schema__offer {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-md);
    padding-block: var(--space-2xs);
    border-bottom: 1px solid var(--card-border, var(--border));
    color: var(--text-body);
}
.hzo-service-schema__offer:last-child {
    border-bottom: 0;
}
.hzo-service-schema__offer-link {
    color: var(--brand-text);
    text-decoration: none;
}
.hzo-service-schema__offer-link:hover {
    text-decoration: underline;
}
.hzo-service-schema__offer-price {
    color: var(--text-heading);
    font-family: var(--font-number, inherit);
    font-weight: var(--fw-semibold, 600);
    white-space: nowrap;
    flex-shrink: 0;
}

/* <summary> toggle — native disclosure; ≥24px hit area, real focus ring.
   No box-shadow ring (forced-colors-safe): relies on outline (§12.2). */
.hzo-service-schema__offers-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    min-height: 24px;
    padding-block: var(--space-2xs);
    color: var(--brand-text);
    font-weight: var(--fw-semibold, 600);
    cursor: pointer;
    list-style: none;              /* hide default marker; use our own caret */
}
.hzo-service-schema__offers-toggle::-webkit-details-marker {
    display: none;
}
.hzo-service-schema__offers-toggle::after {
    content: "";
    width: 0.5em;
    height: 0.5em;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg);
    transition: transform var(--dur-1) var(--ease-standard);
}
.hzo-service-schema__offers[open] > .hzo-service-schema__offers-toggle::after {
    transform: rotate(-135deg);
}
.hzo-service-schema__offers[open] > .hzo-service-schema__offers-toggle {
    margin-bottom: var(--space-sm);
}

/* ── CTA (native <a>; inherits .hzo-btn) ── */
.hzo-service-schema__cta {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    min-height: 24px;              /* .hzo-btn already exceeds this; explicit floor */
}
.hzo-service-schema__cta-icon {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
}

/* ── Print: customers print estimates — the card, price, and FULL offer list
   must print. Force any collapsed <details> open, and force-reveal an opt-in
   .hzo-reveal card so it is never blank (G13). Drop the decorative caret. ── */
@media print {
    .hzo-service-schema {
        box-shadow: none;
        border-color: currentColor;
    }
    .hzo-service-schema.hzo-reveal {
        opacity: 1 !important;
        transform: none !important;
    }
    .hzo-service-schema__offers[open] > .hzo-service-schema__offers-toggle::after,
    .hzo-service-schema__offers-toggle::after {
        display: none;
    }
    .hzo-service-schema__offers-list {
        display: flex !important;   /* browsers auto-open <details> for print, but be explicit */
    }
}

/* ── Forced-colors: convey no state through color/shadow alone; keep a border. ── */
@media (forced-colors: active) {
    .hzo-service-schema {
        border: 1px solid CanvasText;
    }
    .hzo-service-schema__offers-toggle::after {
        border-color: CanvasText;
    }
}

/* ── Reduced-motion: kill the only animated affordance (the caret rotate).
   PRESERVE-5 — ambient motion stops; content stays visible. ── */
@media (prefers-reduced-motion: reduce) {
    .hzo-service-schema__offers-toggle::after {
        transition: none;
    }
}


/* --- sidebar-nav/sidebar-nav.css --- */
/* sidebar-nav — @tokens: --sidebar-nav-link-pad-y, --sidebar-nav-link-pad-x */

/* Reusable vertical in-page / app navigation (wayfinding). Distinct from
   the overlay mobile-menu and the doc-specific doc-toc: a plain nestable
   link list with an optional group label and a brand accent on the
   current item. Batch E (navigation/wayfinding). */
.hzo-sidebar-nav {
    display: block;
    width: 100%;
}

/* Groups stack with breathing room between them. */
.hzo-sidebar-nav__group + .hzo-sidebar-nav__group {
    margin-top: var(--space-sm);
}

/* Small uppercase section heading above a list. */
.hzo-sidebar-nav__label {
    margin: 0;
    padding: var(--space-2xs) var(--space-sm);
    font-size: var(--fs-50);
    font-weight: var(--fw-600);
    line-height: var(--lh-normal);
    letter-spacing: 0.04em;   /* structural micro-tracking for the uppercase label */
    text-transform: uppercase;
    color: var(--text-muted);
}

.hzo-sidebar-nav__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
}

/* Nested child list — indented under its parent link. */
.hzo-sidebar-nav__list--nested {
    padding-left: var(--space-sm);
    margin-top: var(--space-2xs);
}

.hzo-sidebar-nav__link {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    padding: var(--sidebar-nav-link-pad-y, 8px) var(--sidebar-nav-link-pad-x, 12px);
    border-radius: var(--radius-control);
    color: var(--text-body);
    text-decoration: none;
    font-size: var(--fs-100);
    line-height: var(--lh-normal);
    transition: background var(--transition-base) ease, color var(--transition-base) ease;
}

.hzo-sidebar-nav__link:hover {
    background: var(--bg-alt);
    color: var(--text-heading);
}

/* Current item — brand-pale surface + a left brand accent bar. Both the
   .is-active class and [aria-current="page"] resolve to the same state. */
.hzo-sidebar-nav__link.is-active,
.hzo-sidebar-nav__link[aria-current="page"] {
    background: var(--brand-pale);
    color: var(--brand-text);
    font-weight: var(--fw-600);
    box-shadow: inset 3px 0 0 var(--brand);   /* structural: 3px accent bar width */
}

.hzo-sidebar-nav__link:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-sidebar-nav__link {
        transition: none;
    }
}


/* --- skeleton/skeleton.css --- */
/* skeleton — @tokens: --skeleton-text-height, --skeleton-title-height, --skeleton-circle-size */
/* ─── Skeleton ────────────────────────────────────────────────────────
   Loading placeholder / skeleton screen. Purely presentational — markup
   is aria-hidden and a sibling sr-only "Loading…" carries the meaning.
   Default is a cheap, GPU-free opacity pulse (reduced-motion-safe); the
   optional --shimmer modifier adds a swept highlight. Shape modifiers
   (text / title / circle / media) size the placeholder to its content. */
@keyframes hzo-skeleton-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
}
@keyframes hzo-skeleton-shimmer {
    100% { transform: translateX(100%); }
}

.hzo-skeleton {
    display: block;
    position: relative;
    overflow: hidden;
    background: var(--bg-alt);
    border-radius: var(--radius-control);
    animation: hzo-skeleton-pulse 1.4s ease-in-out infinite;
}

/* ── Shape modifiers ─────────────────────────────────────────────── */
.hzo-skeleton--text {
    height: var(--skeleton-text-height, 0.8em);
    border-radius: var(--radius-control);
    margin-bottom: var(--space-xs);
}

.hzo-skeleton--title {
    height: var(--skeleton-title-height, 1.4em);
}

.hzo-skeleton--circle {
    width: var(--skeleton-circle-size, 48px);
    height: var(--skeleton-circle-size, 48px);
    border-radius: var(--radius-full);
}

.hzo-skeleton--media {
    width: 100%;
    aspect-ratio: 16 / 9;
    border-radius: var(--radius-panel);
}

/* ── Optional shimmer ────────────────────────────────────────────────
   A moving highlight swept across the placeholder. Disables the base
   pulse so the two effects don't compound. */
.hzo-skeleton--shimmer {
    animation: none;
}
.hzo-skeleton--shimmer::after {
    content: "";
    position: absolute;
    inset: 0;
    transform: translateX(-100%);
    background: linear-gradient(
        90deg,
        transparent,
        color-mix(in srgb, var(--card-bg, #fff) 60%, transparent),
        transparent
    );
    animation: hzo-skeleton-shimmer 1.4s ease-in-out infinite;
}

/* ── Reduced motion — static placeholder, no motion ─────────────────── */
@media (prefers-reduced-motion: reduce) {
    .hzo-skeleton,
    .hzo-skeleton--shimmer::after {
        animation: none;
    }
}


/* --- skip-link/skip-link.css --- */
/* ─── Skip-links (multi-target bypass block · .hzo-skip-links) ─────────────
   ADDITIVE to the single, always-loaded .hzo-skip-link (components/_base/
   skip-link.css, PRESERVE-6). That base sheet owns the reveal-on-focus PILL
   (clip-hidden → brand pill on :focus, --z-toast, reduced-motion kill). Per the
   §6 / G2 coupling rule the reveal MUST live in an always-loaded sheet — it does
   (_base), so a page that never enqueues THIS sheet still reveals every link.

   This sheet adds ONLY the group concerns the single link can't express:
     • the wrapper (position:fixed children, so it takes no layout space),
     • RTL-safe position modifiers (logical inset props override the base's
       physical `left`),
     • the `always` persistent-bar mode (kiosk / forced-colors),
     • print + forced-colors handling.
   Each child KEEPS the base .hzo-skip-link class, so the pill, z-index, focus
   ring, and reduced-motion behavior are inherited — never re-declared here. */

/* Group wrapper — the .hzo-skip-link children are position:fixed (base sheet),
   so the wrapper contributes no box; display:contents keeps it out of flow while
   the descendant position modifiers below still match. */
.hzo-skip-links {
    display: contents;
}

/* ── Position (logical, RTL-safe). Overrides the base's physical `left: 8px`;
      `top` is unchanged (block-start is top in both writing directions). ── */
.hzo-skip-links--start .hzo-skip-link {
    left: auto;
    inset-inline-start: var(--space-sm);
}
.hzo-skip-links--end .hzo-skip-link {
    left: auto;
    inset-inline-start: auto;
    inset-inline-end: var(--space-sm);
}
.hzo-skip-links--center .hzo-skip-link {
    left: auto;
    inset-inline-start: 50%;
    transform: translate(-50%, -150%);
}
/* Higher specificity than the base `.hzo-skip-link:focus` rule, so the centered
   pill reveals in place instead of snapping to the base's translateY(0). */
.hzo-skip-links--center .hzo-skip-link:focus {
    transform: translate(-50%, 0);
}

/* ── `always` mode — a persistent visible bar (kiosk / high-contrast setups).
      Children drop out of the fixed/clip reveal and flow inline in the bar. ── */
.hzo-skip-links--always {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    padding: var(--space-sm);
    background: var(--brand);
}
.hzo-skip-links--always .hzo-skip-link {
    position: static;
    inset: auto;
    transform: none;
}

/* ── Environment: navigation chrome is useless on paper; hide when printed.
      (Not reveal-hidden content — G13 force-reveal does not apply.) ── */
@media print {
    .hzo-skip-links,
    .hzo-skip-links .hzo-skip-link {
        display: none !important;
    }
}

/* Forced-colors: the revealed pill uses system colors and keeps a real outline
   (box-shadow rings are dropped in High-Contrast — §12.2). The base already
   supplies the outline; pin the fill to system colors so the bar mode survives. */
@media (forced-colors: active) {
    .hzo-skip-links--always {
        background: Canvas;
    }
    .hzo-skip-links .hzo-skip-link {
        outline: 1px solid CanvasText;
    }
}

/* Defensive reduced-motion kill (the base already zeroes the .hzo-skip-link
   transition; this keeps the centered-reveal transform instant if a future base
   edit ever animates transform under reduce). */
@media (prefers-reduced-motion: reduce) {
    .hzo-skip-links .hzo-skip-link {
        transition: none;
    }
}


/* --- social-icons/social-icons.css --- */
/* ─── Social icons (v1.1.3) ────────────────────────────────────────────────
   [hzo_social_icons] outputs inline SVGs without explicit width/height
   attributes. Without an explicit CSS constraint, SVGs in a flex container
   fall to fill-the-parent sizing — produced the 150px+ Facebook/Instagram
   icons surfaced 2026-05-22. Constrains both the default .hzo-social-icons
   class and the .hzo-footer-social variant the footer.html part passes. */
.hzo-social-icons,
.hzo-footer-social {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    flex-wrap: wrap;
    margin-top: var(--space-smd);
}
/* Social icon links carry the .hzo-social-icons__link class (added in
   [hzo_social_icons], v1.1.93) — again, the class exists so a per-client
   `a:not([class])` content-link rule excludes them. Styling stays on the
   original descendant selectors, which win on cascade order within this file.
   No !important. */
.hzo-social-icons a,
.hzo-footer-social a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 44px tap target (WCAG 2.5.5 comfort size) — G53 gate requires the family
       bar move off the old 36px, especially now the row carries more platforms.
       The glyph stays 20px; the extra size is padding around it. */
    width: 44px;
    height: 44px;
    color: var(--footer-social-link, var(--accent, currentColor));
    text-decoration: none;
    background-image: none;
    border-radius: 50%;
    /* Smooth ease-out settle rather than a snappy linear flip (v1.1.93). */
    transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1),
                background-color 0.3s cubic-bezier(0.22, 1, 0.36, 1),
                transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Hold the accent color after the (external) social link is visited — without
   this, `.hzo-site-footer a:visited` (rgba .85) repaints visited icons. Mirrors
   the .hzo-footer-link:visited pattern; placed after that rule so it wins the
   same-specificity tie on cascade order. No !important. */
.hzo-social-icons a:visited,
.hzo-footer-social a:visited { color: var(--footer-social-link, var(--accent, currentColor)); }
.hzo-social-icons a:hover,
.hzo-social-icons a:focus-visible,
.hzo-footer-social a:hover,
.hzo-footer-social a:focus-visible {
    color: var(--footer-social-link-hover, var(--text-on-accent, #fff));
    background-color: var(--footer-social-bg-hover, var(--accent-hover-on-dark, rgba(255,255,255,0.1)));
    transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
    .hzo-social-icons a,
    .hzo-footer-social a { transition: color 0.15s ease, background-color 0.15s ease; }
    .hzo-social-icons a:hover,
    .hzo-footer-social a:hover { transform: none; }
}
.hzo-social-icons svg,
.hzo-footer-social svg {
    width: 20px;
    height: 20px;
    display: block;
}

/* Footer social row: a touch tighter (symmetric row + column gap) and a smaller
   hover disc. The 44px tap target (WCAG comfort) is PRESERVED — only the visible
   hover circle shrinks, via a centred ::before behind the glyph, so the icons read
   cleaner without losing reach. Scoped to .hzo-footer-social; the general
   .hzo-social-icons row is unchanged. */
.hzo-footer-social { gap: var(--space-sm); }
.hzo-footer-social a { position: relative; z-index: 0; }
.hzo-footer-social a,
.hzo-footer-social a:hover,
.hzo-footer-social a:focus-visible { background-color: transparent; }
.hzo-footer-social a::before {
    content: "";
    position: absolute;
    inset: 5px;                 /* 44px box → 34px visible disc, centred */
    border-radius: 50%;
    /* Always-on "glass" disc so each icon reads as a defined chip on the dark
       footer. Translucent (not solid white) to stay subtle; hover fills to the
       accent disc below. Client-tunable via --footer-social-bg. */
    background-color: var(--footer-social-bg, rgba(255,255,255,0.12));
    z-index: -1;
    transition: background-color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.hzo-footer-social a:hover::before,
.hzo-footer-social a:focus-visible::before {
    background-color: var(--footer-social-bg-hover, var(--accent-hover-on-dark, rgba(255,255,255,0.24)));
}
/* A touch more breathing room for the glyph inside the disc (the disc size stays
   the same; the icon just gets a hair more padding). */
.hzo-footer-social svg { width: 18px; height: 18px; }
@media (prefers-reduced-motion: reduce) {
    .hzo-footer-social a::before { transition: background-color 0.15s ease; }
}

/* ═══════════════════════════════════════════════════════════════════════
   WAVE B — style-NEUTRAL, opt-in capability primitives (Group 4)
   Every rule below is class-scoped .hzo-*, token-driven, WCAG-AA, and gates
   all motion behind prefers-reduced-motion. New classes only — nothing here
   changes how any existing markup renders. Component-local vars carry token
   fallbacks so no main.css :root edit is needed.
   ═══════════════════════════════════════════════════════════════════════ */


/* --- spinner/spinner.css --- */
/* ─── Spinner ─────────────────────────────────────────────────────────
   Standalone async indicator (form submit, cart, lazy content). Inherits
   currentColor so it adapts to any text context — drop it in a dark section
   and the ring goes light automatically. Sizes via --sm / --lg. Shares the
   hzo-spin keyframe with the button loading state above. */
@keyframes hzo-spin { to { transform: rotate(360deg); } }
.hzo-spinner {
    display: inline-block;
    width: 20px;
    height: 20px;
    border: var(--border-2) solid color-mix(in srgb, currentColor 25%, transparent);
    border-top-color: currentColor;
    border-radius: 50%;
    animation: hzo-spin 0.6s linear infinite;
    vertical-align: -0.2em;
}
.hzo-spinner--sm { width: 14px; height: 14px; border-width: 1.5px; }
.hzo-spinner--lg { width: 32px; height: 32px; border-width: var(--border-3); }
@media (prefers-reduced-motion: reduce) {
    .hzo-spinner { animation-duration: 1.5s; }
}


/* --- stat-block/stat.css --- */
/* ─── Stat ────────────────────────────────────────────────────────────
   Positions a .hzo-stat-number + .hzo-stat-label pair within a grid cell.
   (The number/label typography itself lives in stats/stats.css.)

   ALIGNMENT IS NOW A TOKEN, not a hardcoded value. --stat-align defaults to
   `center`, so every existing stat row renders exactly as before; a client
   that wants left-aligned metrics site-wide sets --stat-align: left once,
   instead of overriding this rule per page.

   ── Why the default was not flipped ─────────────────────────────────────
   A centred number over a small-caps label is the generic metric row — the
   treatment that makes a stats band read as filler. The premium treatment is
   left-aligned and given an edge (a rule or a panel) so each figure reads as
   a claim rather than as decoration. That is a real craft improvement, and it
   is deliberately NOT made the engine default here: flipping `center` to
   `left` silently restyles every stats row on every site in the fleet, and no
   live render was available to verify that against. The vocabulary ships; the
   flip is one token and belongs to whoever is looking at the page.

   ── The modifiers ───────────────────────────────────────────────────────
     .hzo-stat--left    left-align this stat
     .hzo-stat--rule    left-align + a brand rule down the leading edge. The
                        cheapest way to make a number look load-bearing.
     .hzo-stat--boxed   left-align + a real surface panel (bg, border, radius,
                        padding). Use when the stats row IS the section rather
                        than a strip inside one.
   --rule and --boxed both imply left; a centred rule reads as a mistake.

   ── The specificity note, because it will bite otherwise ────────────────
   stats/stats.css declares `.hzo-stats .hzo-stat { text-align: … }` (0,2,0)
   and loads AFTER this sheet (component enqueue order is alphabetical:
   stat-block, then stats). A plain `.hzo-stat--left` (0,1,0) — or even
   `.hzo-stat.hzo-stat--left` (0,2,0), which ties and loses on source order —
   would be a silent no-op inside the [hzo_stats] widget while working fine in
   hand-authored markup. Hence the doubled selector on every modifier: the
   compound form for standalone markup, the descendant form for the widget.
   ────────────────────────────────────────────────────────────────────────── */
.hzo-stat { text-align: var(--stat-align, center); }

.hzo-stat.hzo-stat--left,
.hzo-stats .hzo-stat.hzo-stat--left,
.hzo-stat.hzo-stat--rule,
.hzo-stats .hzo-stat.hzo-stat--rule,
.hzo-stat.hzo-stat--boxed,
.hzo-stats .hzo-stat.hzo-stat--boxed { text-align: left; }

.hzo-stat.hzo-stat--rule,
.hzo-stats .hzo-stat.hzo-stat--rule {
    border-inline-start: var(--stat-rule-width, 3px) solid var(--stat-rule-color, var(--accent));
    padding-inline-start: var(--stat-rule-gap, var(--space-md));
}

.hzo-stat.hzo-stat--boxed,
.hzo-stats .hzo-stat.hzo-stat--boxed {
    background: var(--stat-box-bg, var(--surface));
    border: var(--border-1) solid var(--stat-box-border, var(--border));
    border-radius: var(--stat-box-radius, var(--radius-panel));
    padding: var(--stat-box-padding, var(--space-lg));
}

/* On a dark band the boxed panel's light surface would punch a hole in the
   band. Fall back to a translucent white veil so the panel still reads as a
   panel without breaking the surface. */
.hzo-section--dark .hzo-stat.hzo-stat--boxed,
.hzo-section--dark .hzo-stats .hzo-stat.hzo-stat--boxed {
    background: rgba(255, 255, 255, 0.08);
    border-color: rgba(255, 255, 255, 0.22);
}


/* --- stat-block/stat-block.css --- */
/* ─── Stat block (headline metric row) ──────────────────────────────────
   Aggregator sheet — the component loader enqueues ONLY <name>.css, so the
   component's own stat.css is @imported here to ride the same request (this
   is its only load path in components mode — same fix as video.css in Wave
   4; in fallback mode primitives.css @imports stat.css directly):
     stat.css   .hzo-stat — centers a number/label pair within a grid cell

   Everything else is composition, no styling of its own: the metric grid is
   the _base responsive .hzo-grid--2/--3/--4, the .hzo-stat-number /
   .hzo-stat-label typography lives in stats/stats.css (always-loaded), the
   count-up is the cross-cutting data-hzo-countup contract in stats/stats.js,
   and .hzo-reveal is the _base scroll-reveal hook. The .hzo-stat-block root
   class emitted by the shortcode is an unstyled, stable hook.

   Static content — no transitions of its own to kill under reduced motion
   (the count-up/reveal reduced-motion gating lives with their owners).
   Shortcode: stat-block.php [hzo_stat_block]. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */


/* --- stats/stats.css --- */
/* ─── Stats ─────────────────────────────────────────────────────────── */
.hzo-stat-number {
    font-family: var(--font-number);
    font-size: clamp(2.25rem, 4vw, 3rem);
    font-weight: var(--fw-700);
    line-height: var(--lh-none);
    color: var(--brand-text);
    letter-spacing: -0.01em;
    margin: 0 0 var(--space-sm);
}
.hzo-stat-label {
    font-family: var(--font-body);
    font-size: var(--fs-100);
    font-weight: var(--fw-500);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    line-height: 1.4;
}

/* ─── [hzo_stats] widget root (Phase-2 widget system) ──────────────────────
   The shortcode composes the always-loaded base grid (.hzo-grid --2/--3/--4);
   only the stat-item centering is needed here, SCOPED under .hzo-stats so the
   hand-authored pattern markup (stat-block, page variants — .hzo-stat without
   an .hzo-stats ancestor) is untouched. (Unscoped `.hzo-stat` centering is
   owned by components/stat-block/stat.css.)
   Wave S: reads --stat-align (default center — computed output unchanged) so a
   site-wide alignment flip is one token rather than two overrides. The
   .hzo-stat--left/--rule/--boxed modifiers carry a `.hzo-stats .hzo-stat.…`
   form specifically to beat this selector, which loads after stat.css. */
.hzo-stats .hzo-stat { text-align: var(--stat-align, center); }

/* Reduced motion: the count-up itself is JS-driven text (stats.js never
   animates under prefers-reduced-motion); kill any transition/animation on
   the widget's own elements for belt-and-suspenders. Scoped to this
   component's classes only — the .hzo-reveal utility handles its own
   reduced-motion state in _base/scroll-reveal.css. */
@media (prefers-reduced-motion: reduce) {
    .hzo-stats,
    .hzo-stat-number,
    .hzo-stat-label {
        transition: none !important;
        animation: none !important;
    }
}


/* --- steps/steps.css --- */
/* ─── Steps connectors (.hzo-steps--connected) ─────────────────────────
   Opt-in modifier for a SINGLE-ROW steps grid (compose onto
   .hzo-grid.hzo-grid--2/--3/--4 of .hzo-stat items — see blocks/steps.html).
   Draws a connector between consecutive step centers: horizontal from 768px
   (each item after the first draws back to the previous item's center;
   nothing ever paints left of the first or right of the last item), vertical
   through the stack gap below 768. The step number masks the line with
   --connector-mask (default --bg-page; set to --bg-alt on tinted sections).
   Tokens: --connector-color (default --border), --connector-style (solid),
   --connector-width (2px), --connector-offset (line height from item top).
   Static decoration — no motion to gate. Single-row use only: in a wrapped
   grid the line would connect across rows. */
/* ── ONE COMPOSITION (Wave S) ────────────────────────────────────────────
   This component ships exactly ONE composition: an evenly-spaced single-row grid of numbered items with an optional connector.

   That is a feature. One composition is why it is responsive, accessible and
   identical on two hundred sites, and why a client build can place it without
   re-deciding anything. It is also a hard limit: the component cannot express
   a SECOND composition, and making it try — nesting it, stacking modifiers on
   it, overriding its grid from a client sheet — is how a page ends up fighting
   its own components and still looking generic.

   FOR AN AMPLIFY OR ELEVATE SECTION, DO NOT BEND THIS COMPONENT. Take one of:
     • RAW mode — author the section's markup directly, or
     • hand-author to the class contract on the spatial utilities:
       .hzo-grid + .hzo-span-* / .hzo-rowspan-* / --hzo-grid-template,
       .hzo-bleed, .hzo-pull-* / .hzo-z-*, .hzo-section--edge-*, .hzo-measure-*.
   Both are the STANDARD route for a high-intensity section, not an escape
   hatch, and the composition plan is what says which sections those are.

   PLUMBING AND PROOF COMPONENTS ARE UNAFFECTED. Every JS widget and every
   proof/data component (reviews, credentials, ratings, schema) stays
   mandatory — never hand-roll those. This note is about LAYOUT only.
   ────────────────────────────────────────────────────────────────────────── */

.hzo-steps--connected > * { position: relative; }
@media (min-width: 768px) {
    .hzo-steps--connected > * + *::before {
        content: "";
        position: absolute;
        top: var(--connector-offset, 24px);
        right: 50%;
        width: calc(100% + var(--space-lg));   /* item width + the .hzo-grid gap = previous item's center */
        border-top: var(--connector-width, 2px) var(--connector-style, solid) var(--connector-color, var(--border));
        pointer-events: none;
    }
    .hzo-steps--connected .hzo-stat-number {
        position: relative;
        z-index: 1;
        display: inline-block;
        padding-inline: var(--space-md);
        background: var(--connector-mask, var(--bg-page));
    }
}
@media (max-width: 767.98px) {
    .hzo-steps--connected > * + *::before {
        content: "";
        position: absolute;
        bottom: 100%;                          /* fills the stack gap between items */
        left: 50%;
        height: var(--space-lg);
        border-left: var(--connector-width, 2px) var(--connector-style, solid) var(--connector-color, var(--border));
        pointer-events: none;
    }
}


/* --- sticky-cta/sticky-bar.css --- */
/* ─── Sticky bar — mobile bottom CTA ───────────────────────────────────
   Fixed bottom action bar for phones (desktop keeps its header CTA, so this is
   display:none above the mobile breakpoint). sticky-cta.js ([data-hzo-sticky-bar]
   + data-hzo-sticky-after="<selector>") reveals it once the referenced element
   scrolls out of view. Rides var(--z-sticky-bar); pads for the iOS home
   indicator via env(safe-area-inset-bottom). Reduced motion → appears without
   the slide. Markup: blocks/sticky-cta.html. */
.hzo-sticky-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: var(--z-sticky-bar);
    display: none;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    padding-bottom: calc(var(--space-sm) + env(safe-area-inset-bottom, 0px));
    background: var(--sticky-bar-bg, var(--bg-page));
    border-top: var(--border-1) solid var(--border);
    box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.08);
    transform: translateY(110%);
    visibility: hidden;                 /* G54: while slid away it's not focusable/clickable — */
    transition: transform var(--duration-base) var(--ease-out), visibility 0s linear var(--duration-base);
}
.hzo-sticky-bar > * { flex: 1 1 auto; }
.hzo-sticky-bar.is-visible { transform: translateY(0); visibility: visible; transition-delay: 0s; }
@media (max-width: 767.98px) {
    .hzo-sticky-bar { display: flex; }
}
@media (prefers-reduced-motion: reduce) {
    .hzo-sticky-bar { transition: none; }
}


/* --- sticky-cta/sticky-bar-product.css --- */
/* ─── Sticky bar — product modifier (.hzo-sticky-bar--product) ─────────
   Product-page layout for the existing mobile sticky bar: 40px thumb +
   title/price stack + ATC button. Reveal wiring is unchanged — point
   data-hzo-sticky-after at the on-page product form so the bar appears
   once the real add-to-cart scrolls away:
     <div class="hzo-sticky-bar hzo-sticky-bar--product" data-hzo-sticky-bar
          data-hzo-sticky-after="form.cart" role="region" aria-label="Add to cart">
       <img class="hzo-sticky-bar__thumb" src="…" alt="">
       <span class="hzo-sticky-bar__info">
         <span class="hzo-sticky-bar__title">Product name</span>
         <span class="hzo-sticky-bar__price">$49.00</span>
       </span>
       <button type="button" class="hzo-btn hzo-btn--primary" data-hzo-add-to-cart="123">Add to cart</button>
     </div>
   data-hzo-add-to-cart MUST carry the product id (as above). A bare
   data-hzo-add-to-cart with no value has no product to post: on a WC site
   commerce.js resets it silently, and on a non-WC site the button would sit
   in .is-loading forever (buttons.js only self-resolves demo buttons). Use the
   real id, or data-hzo-add-to-cart-demo for a non-functional showcase.
   No new JS — reveal + add-to-cart states both reuse shipped modules. */
.hzo-sticky-bar--product > * { flex: 0 0 auto; }   /* cancel the base equal-stretch */
.hzo-sticky-bar--product .hzo-sticky-bar__info {
    flex: 1 1 auto;
    min-width: 0;                    /* lets the title ellipsis instead of overflowing */
    display: flex;
    flex-direction: column;
}
.hzo-sticky-bar__thumb {
    width: 40px;
    height: 40px;
    border-radius: var(--radius-control);
    object-fit: cover;
    display: block;
}
.hzo-sticky-bar__title {
    font-size: var(--fs-100);
    font-weight: var(--fw-600);
    color: var(--on-surface, var(--text-heading));
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.hzo-sticky-bar__price {
    font-size: var(--fs-75);
    color: var(--on-surface-muted, var(--text-muted));
    line-height: 1.3;
}


/* --- sticky-cta/sticky-cta.css --- */
/* ─── Sticky CTA widget (mobile bottom action bar) ─────────────────────
   Aggregator sheet — the component loader enqueues ONLY <name>.css, so the
   two carved-out sheets are @imported here to ride the same request (this
   is their only load path in component mode; the legacy whole-file
   primitives.css fallback imports them directly and never loads alongside
   this file):
     sticky-bar.css          .hzo-sticky-bar — fixed bottom bar + reveal slide
     sticky-bar-product.css  .hzo-sticky-bar--product — product-page modifier
   Behavior: sticky-cta.js. Shortcode: sticky-cta.php [hzo_sticky_cta].
   @imports must precede all rules — keep them first. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */

/* Dismiss control — OPT-IN (emitted only by [hzo_sticky_cta dismiss="true"]
   or authored by hand); attribute-free legacy markup never renders it, so
   the default path is untouched. flex 0 0 auto opts the button out of the
   bar's equal-stretch children rule; 44px square keeps the WCAG 2.5.8
   target size (same floor as back-to-top). */
.hzo-sticky-bar__close {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-input);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}
.hzo-sticky-bar__close:hover { color: var(--text-heading); }
.hzo-sticky-bar__close svg { width: 18px; height: 18px; }

/* Reduced-motion: kill every transition this component animates (belt &
   braces on top of sticky-bar.css's own block — per the convention, the
   aggregator ends with a transition-kill). */
@media (prefers-reduced-motion: reduce) {
    .hzo-sticky-bar { transition: none; }
}


/* --- stock-status/stock-status.css --- */
/* stock-status — @tokens: --stock-status-dot-size

   An inline WooCommerce availability indicator: a small status dot + a text
   label. Non-interactive display atom — the MEANING is carried by the label
   text, never colour alone (the dot only reinforces it). Per the badge.css
   stock mapping, unavailability is a STATE not an error — red (--error) is
   reserved for genuine errors, so out-of-stock uses a neutral muted dot.

   Token-only: the dot diameter has no semantic token in the scale, so it's a
   component token with a literal fallback — the same dial pattern chip.css uses.
   Everything else reads a semantic/L1 token. */
.hzo-stock-status {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    color: var(--text-body);
    font-size: var(--fs-50);
    line-height: var(--lh-none);
}

/* The status swatch — a small circle. Inherits currentColor by default; state
   modifiers below recolour it. */
.hzo-stock-status__dot {
    flex-shrink: 0;
    width: var(--stock-status-dot-size, 8px);
    height: var(--stock-status-dot-size, 8px);
    border-radius: var(--radius-full);
    background: currentColor;
}

/* The label — the load-bearing meaning. */
.hzo-stock-status__label {
    color: inherit;
}

/* State modifiers set the DOT colour only (per badge.css's stock mapping). */
.hzo-stock-status--in-stock     .hzo-stock-status__dot { background: var(--success); }
.hzo-stock-status--low-stock    .hzo-stock-status__dot { background: var(--warning); }
.hzo-stock-status--out-of-stock .hzo-stock-status__dot { background: var(--text-muted); }
.hzo-stock-status--backorder    .hzo-stock-status__dot { background: var(--info); }


/* --- story-band/media-band.css --- */
/* ─── Media band — inset story panel with scrimmed copy ────────────────
   A rounded photo panel inset from the container gutter with copy sitting on
   its scrimmed side (reuses the directional-scrim approach; --scrim-color
   default --brand-deep). --reverse flips copy to the other side. The photo
   is set via the inline --band-image var. Below 768px the band STACKS — image
   on top, copy on a SOLID var(--bg-dark-section) panel underneath: on small
   screens we never scrim over text, which is the AA guarantee. Zero JS.
   Markup: blocks/story-band.html. */
.hzo-media-band {
    position: relative;
    border-radius: var(--band-radius, var(--radius-panel));
    overflow: hidden;
}
.hzo-media-band__media {
    aspect-ratio: var(--band-aspect, 16 / 9);   /* mobile: image is a top block */
    background-image: var(--band-image);
    background-size: cover;
    background-position: var(--band-image-pos, center);
}
.hzo-media-band__body {
    background: var(--band-panel-bg, var(--bg-dark-section));   /* solid on mobile → AA */
    color: #fff;
    padding: var(--space-xl) var(--space-lg);
}
/* Force headline/eyebrow/body colors light on both the mobile solid panel and
   the desktop scrim (headings otherwise default to the dark --text-heading). */
.hzo-media-band__body :is(h1, h2, h3, h4, .hzo-visual-h1, .hzo-visual-h2, .hzo-visual-h3, .hzo-visual-h4) { color: #fff; }
.hzo-media-band__body p { color: rgba(255, 255, 255, 0.9); }
.hzo-media-band__body .hzo-eyebrow { color: var(--accent); }
/* h1-h6 eyebrows carry !important base colors (the SEO heading pattern) —
   match that strength here or they render the on-light --accent-text over
   the dark panel/scrim (same fix .hzo-section--dark already carries). */
.hzo-media-band__body h1.hzo-eyebrow,
.hzo-media-band__body h2.hzo-eyebrow,
.hzo-media-band__body h3.hzo-eyebrow,
.hzo-media-band__body h4.hzo-eyebrow { color: var(--accent) !important; }
@media (min-width: 768px) {
    .hzo-media-band {
        display: grid;
        align-items: center;
        min-height: var(--band-min-height, 460px);
    }
    .hzo-media-band__media { position: absolute; inset: 0; aspect-ratio: auto; }
    /* PF-063 — --band-copy-width now means the TEXT MEASURE, which is what its
       name says and what every consumer assumed it meant.

       It used to be applied as a bare max-width on this element, which also
       carries 64px of padding on all four sides. So the documented 44ch
       default produced 44ch of PADDED BOX and about 28 characters of actual
       measure: a newspaper column inside a 1222px-wide panel. Headings broke
       to four lines, body copy to three-and-four-word lines, and the band grew
       to 793px tall against its 460px min-height purely because the copy could
       not fit across. Measured live at 1440px before this change:
         --band-copy-width 44ch -> body width 350px, padding 64px
                                -> real measure 222px ~= 28 characters
       The token was reasoned from its name rather than from a render, and the
       DESIGN.md walk calling 44ch "a comfortable measure" was right about 44ch
       and wrong about what the token delivered.

       Fix: the padding is added back into the max-width, so the value IS the
       measure. Padding is tokenized as --band-panel-padding at its historical
       --space-2xl so the two can never drift apart.

       DELIBERATE BEHAVIOR CHANGE, fleet-wide: every existing story band's
       desktop copy panel widens by 2 x --band-panel-padding (128px at the
       default) and the copy inside finally gets the measure it was configured
       for. Clients who compensated by inflating copy-width (e.g. 62 to buy
       ~46 characters) now get MORE than they asked for and should reset the
       value to the measure they actually want. */
    .hzo-media-band__body {
        position: relative;
        z-index: 1;
        background: transparent;
        justify-self: start;
        max-width: calc(var(--band-copy-width, 44ch) + var(--band-panel-padding, var(--space-2xl)) * 2);
        padding: var(--band-panel-padding, var(--space-2xl));
    }
    /* Directional scrim anchored under the copy side, fading toward the photo.
       z-index: 1 — the photo (__media, z auto) is later in tree order, so at
       equal stacking the image would paint OVER the scrim, leaving white copy
       on the raw photo. The body is z-index:1 AND later in tree order, so it
       still paints above the scrim. */
    .hzo-media-band::before {
        content: "";
        position: absolute;
        inset: 0;
        z-index: 1;
        background: linear-gradient(to right,
            color-mix(in srgb, var(--scrim-color, var(--brand-deep)) 82%, transparent) 0%,
            color-mix(in srgb, var(--scrim-color, var(--brand-deep)) 58%, transparent) 42%,
            transparent 72%);
    }
    .hzo-media-band--reverse .hzo-media-band__body { justify-self: end; }
    .hzo-media-band--reverse::before {
        background: linear-gradient(to left,
            color-mix(in srgb, var(--scrim-color, var(--brand-deep)) 82%, transparent) 0%,
            color-mix(in srgb, var(--scrim-color, var(--brand-deep)) 58%, transparent) 42%,
            transparent 72%);
    }
}


/* --- story-band/story-band.css --- */
/* ─── Story band (aggregator) ──────────────────────────────────────────
   The component loader enqueues ONLY <name>.css, so the actual styles — which
   live in media-band.css (root class .hzo-media-band; the historical filename
   is kept so the legacy primitives.css fallback @import keeps resolving) —
   are @imported here to ride the same request. This is the component's load
   path in component-loader mode. Shortcode: story-band.php [hzo_story_band].
   Zero JS.

   No trailing reduced-motion transition-kill block: media-band.css declares no
   transitions or animations of its own (the .hzo-reveal hook's motion is owned
   by _base/scroll-reveal.css, which carries its own reduced-motion guard). */
/* ── ONE COMPOSITION (Wave S) ────────────────────────────────────────────
   This component ships exactly ONE composition: a rounded photo panel with a scrimmed copy panel pinned to one side.

   That is a feature. One composition is why it is responsive, accessible and
   identical on two hundred sites, and why a client build can place it without
   re-deciding anything. It is also a hard limit: the component cannot express
   a SECOND composition, and making it try — nesting it, stacking modifiers on
   it, overriding its grid from a client sheet — is how a page ends up fighting
   its own components and still looking generic.

   FOR AN AMPLIFY OR ELEVATE SECTION, DO NOT BEND THIS COMPONENT. Take one of:
     • RAW mode — author the section's markup directly, or
     • hand-author to the class contract on the spatial utilities:
       .hzo-grid + .hzo-span-* / .hzo-rowspan-* / --hzo-grid-template,
       .hzo-bleed, .hzo-pull-* / .hzo-z-*, .hzo-section--edge-*, .hzo-measure-*.
   Both are the STANDARD route for a high-intensity section, not an escape
   hatch, and the composition plan is what says which sections those are.

   PLUMBING AND PROOF COMPONENTS ARE UNAFFECTED. Every JS widget and every
   proof/data component (reviews, credentials, ratings, schema) stays
   mandatory — never hand-roll those. This note is about LAYOUT only.
   ────────────────────────────────────────────────────────────────────────── */

/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */


/* --- swatches/swatches.css --- */
/* ═════════════════════════════════════════════════════════════════════════
   VARIATION SWATCHES (W-131) — styles, scoped to .hzo-swatches*

   Progressive enhancement over WooCommerce's variations_form. swatches.js adds
   `.hzo-swatches-active` to the form (hides the native <select>s, kept as the
   source of truth + no-JS fallback) and injects one `.hzo-swatches` radiogroup
   per attribute row.

   Tokens only — no bare hex / px where a token exists, no z-index above
   --z-toast, focus ring via `outline` (not a hand-rolled box-shadow width, per
   interaction-standards §9.2) so it also survives forced-colors (§12.2).
   ═════════════════════════════════════════════════════════════════════════ */

/* When JS is active, the swatches replace the raw dropdown. Without JS the class
   is never added, so the native <select> stays fully visible + operable (G26 /
   §12.3 no-JS recovery). */
.hzo-swatches-active .variations td.value select,
.hzo-swatches-active .variations select,
.hzo-swatches-active .variations td.value .select2-container,
.hzo-swatches-active .variations .select2-container {
    display: none;
}

.hzo-swatches {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
    margin: var(--space-sm) 0;
}

/* ── The swatch (44×44 minimum target, WCAG 2.5.8 §11.2) ─────────────────── */
.hzo-swatches__swatch {
    --hzo-swatch-size: 44px;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--hzo-swatch-size);
    min-height: var(--hzo-swatch-size);
    margin: 0;
    padding: 0;
    border: var(--border-2) solid var(--border);
    border-radius: var(--radius-control);
    background: var(--surface);
    color: var(--text-body);
    font: inherit;
    line-height: 1;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
    transition:
        border-color var(--dur-2) var(--ease-standard),
        box-shadow var(--dur-2) var(--ease-standard),
        transform var(--dur-1) var(--ease-standard);
}

.hzo-swatches__swatch:hover {
    border-color: var(--text-muted);
}

/* One focus-ring recipe: outline (not a box-shadow width) — §9.2 + forced-colors. */
.hzo-swatches__swatch:focus-visible {
    outline: var(--border-2) solid var(--color-focus);
    outline-offset: var(--space-2xs);
}

/* Selected — branded border + inner ring (box-shadow here is state, not a focus
   ring). A forced-colors fallback below re-asserts it with an outline. */
.hzo-swatches__swatch.is-selected {
    border-color: var(--brand);
    box-shadow: inset 0 0 0 var(--border-2) var(--brand);
    transform: translateY(-1px);
}

/* ── Color chip ──────────────────────────────────────────────────────────── */
.hzo-swatches__swatch--color {
    padding: 3px;
}
.hzo-swatches__chip {
    display: block;
    width: 100%;
    height: 100%;
    min-width: 34px;
    min-height: 34px;
    border-radius: calc(var(--radius-control) - 2px);
    background-color: var(--hzo-swatch-color, var(--surface-2));
    background-size: cover;
    background-position: center;
    /* keep a hairline so white/very-light chips stay visible on white surface */
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.hzo-swatches__chip--image {
    background-color: var(--surface-2);
}

/* ── Size / text pill ────────────────────────────────────────────────────── */
.hzo-swatches__swatch--pill {
    padding: 0 var(--space-md);
}
.hzo-swatches__text {
    display: inline-block;
    font-weight: 600;
    white-space: nowrap;
}

/* ── Out-of-stock / incompatible — struck + dimmed, still focusable ───────── */
.hzo-swatches__swatch.is-unavailable {
    cursor: not-allowed;
    opacity: 0.5;
}
.hzo-swatches__swatch.is-unavailable::before {
    content: "";
    position: absolute;
    left: 8%;
    right: 8%;
    top: 50%;
    /* border-based diagonal (survives forced-colors better than a gradient) */
    border-top: var(--border-2) solid var(--text-muted);
    transform: rotate(-45deg);
    pointer-events: none;
}
.hzo-swatches__swatch--pill.is-unavailable .hzo-swatches__text {
    text-decoration: line-through;
}
.hzo-swatches__swatch--pill.is-unavailable::before {
    display: none;   /* the line-through already reads OOS for text pills */
}

/* ── Supplemental hover/focus tip (chip swatches only) ────────────────────── */
.hzo-swatches__tip {
    position: absolute;
    bottom: calc(100% + var(--space-2xs));
    left: 50%;
    z-index: var(--z-popover);   /* below --z-toast; never covers cart feedback */
    padding: 2px var(--space-sm);
    border-radius: var(--radius-control);
    background: var(--text-heading);
    color: var(--surface);
    font-size: 0.75rem;
    font-weight: 500;
    white-space: nowrap;
    opacity: 0;
    transform: translateX(-50%) translateY(2px);
    pointer-events: none;
    transition:
        opacity var(--dur-1) var(--ease-standard),
        transform var(--dur-1) var(--ease-standard);
}
.hzo-swatches__swatch:hover .hzo-swatches__tip,
.hzo-swatches__swatch:focus-visible .hzo-swatches__tip {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

/* ── Selected-term label beside the group ────────────────────────────────── */
.hzo-swatches__selected {
    margin-left: var(--space-sm);
    color: var(--text-heading);
    font-weight: 600;
}
.hzo-swatches__selected:empty {
    display: none;
}

/* ── Reduced motion (PRESERVE-5: ambient stops; no status motion here) ────── */
@media (prefers-reduced-motion: reduce) {
    .hzo-swatches__swatch,
    .hzo-swatches__tip {
        transition: none;
    }
    .hzo-swatches__swatch.is-selected {
        transform: none;
    }
}

/* ── Forced-colors / Windows High Contrast (§12.2) ───────────────────────── */
@media (forced-colors: active) {
    .hzo-swatches__swatch.is-selected {
        outline: var(--border-2) solid Highlight;
        outline-offset: var(--space-2xs);
    }
    .hzo-swatches__swatch.is-unavailable {
        opacity: 1;   /* opacity is unreliable in forced-colors — rely on the strike */
    }
    .hzo-swatches__swatch.is-unavailable::before {
        border-top-color: GrayText;
    }
    .hzo-swatches__chip {
        box-shadow: inset 0 0 0 1px CanvasText;
    }
}

/* ── Print (§12.1: keep swatches visible; drop chrome) ───────────────────── */
@media print {
    .hzo-swatches__tip {
        display: none;
    }
    .hzo-swatches__swatch {
        border-color: #000;
    }
}


/* --- table/table-ruled.css --- */
/* ─── Table widget skin (shortcode-emitted modifiers) ─────────────────
   Additive classes only the [hzo_table] shortcode emits (or an author opts
   into by hand). Bare .hzo-table markup never carries them, so the frozen
   primitive contract in table.css is untouched (back-compat rule).

   .hzo-table--ruled    the widget's default desktop skin: the design-preview
                        table look (cell padding + row rules), tokenized here
                        instead of the hand-authored contract's inline styles.
   .hzo-table--striped  zebra rows (even rows tinted); composes with --ruled.
   th[scope="row"]      row-header support ([hzo_table row-headers="true"]) —
                        base mobile stacking blockifies only <td>, never <th>
                        (the same gap comparison-table.css closes for
                        --compare), so --ruled promotes the row header to the
                        stacked card's title.

   @imported by table.css so the component loader — which enqueues only
   table.css — pulls this sheet on the same request. */

.hzo-table--ruled { width: 100%; border-collapse: collapse; }

@media (min-width: 768px) {
    /* Desktop skin, kept out of the mobile range so table.css's stacked-card
       rules (cell padding, per-cell rules, last-cell trim) win untouched
       below 768px. */
    .hzo-table--ruled th,
    .hzo-table--ruled td {
        padding: 12px 16px;
        text-align: left;
        vertical-align: middle;
        border-bottom: 1px solid var(--border);
    }
    .hzo-table--ruled thead th { border-bottom: 2px solid var(--border); }
    .hzo-table--ruled tbody tr:last-child th,
    .hzo-table--ruled tbody tr:last-child td { border-bottom: 0; }
    .hzo-table--ruled tbody th[scope="row"] {
        font-family: var(--font-body);
        font-weight: var(--fw-semibold, 600);
        color: var(--text-heading);
    }
}

/* Zebra rows: every other row (desktop) / stacked card (mobile, where the
   blockified <tr> is the card) tinted. No new token. */
.hzo-table--striped tbody tr:nth-child(even) { background: var(--bg-alt); }

@media (max-width: 767px) {
    /* Promote the row header to the stacked card's title — mirrors
       comparison-table.css's row-header rule so both table widgets' mobile
       cards read identically. */
    .hzo-table--ruled tbody th[scope="row"] {
        display: block;
        width: 100%;
        text-align: left;
        font-family: var(--font-heading);
        font-size: 1.0625rem;
        color: var(--text-heading);
        padding: 4px 0 10px;
        border-bottom: 1px solid var(--border);
        margin-bottom: 4px;
    }
}


/* --- table/table.css --- */
/* ─── Tables ───────────────────────────────────────────────────────────
   Phase-2 widget: [hzo_table] (table.php) generates this markup contract —
   headers + the per-cell data-labels the mobile stacking needs. The widget's
   opt-out skin classes (.hzo-table--ruled / .hzo-table--striped) live in the
   @imported variant sheet so the component loader (which enqueues only
   table.css) pulls them on the same request; everything below this import is
   the frozen hand-authored contract, unchanged. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */

/* G17: horizontal scroller so a wide table never overflows its container. */
.hzo-table-scroll { overflow-x: auto; overscroll-behavior-x: contain; }
.hzo-table { font-family: var(--font-body); }
.hzo-table thead th { font-family: var(--font-heading); color: var(--text-heading); }

/* Responsive table: on mobile, rows stack vertically and each <td> shows
   its column label via the data-label attribute. Requires each <td> to
   have data-label="Column Name" so the label appears next to the value.
   Desktop rendering is unchanged. */
@media (max-width: 767px) {
    .hzo-table thead { display: none; }
    .hzo-table,
    .hzo-table tbody,
    .hzo-table tr,
    .hzo-table td { display: block; width: 100%; box-sizing: border-box; }
    .hzo-table tr {
        border: var(--border-1) solid var(--border);
        border-radius: var(--radius-control);
        padding: var(--space-sm) var(--space-md);
        margin-bottom: var(--space-smd);
    }
    .hzo-table td {
        padding: 10px 0;
        border: 0;
        border-bottom: var(--border-1) solid var(--border);
        display: flex;
        justify-content: space-between;
        gap: var(--space-md);
        font-size: var(--fs-100);
    }
    .hzo-table tr td:last-child { border-bottom: 0; }
    .hzo-table td[data-label]::before {
        content: attr(data-label);
        font-weight: var(--fw-600, 600);
        color: var(--text-muted);
        flex-shrink: 0;
    }
}


/* --- tag/tag.css --- */
/* ─── Tags / chips ────────────────────────────────────────────────────
   Distinct from a badge: a tag is an INTERACTIVE category/filter chip
   (sentence case; can be a link or a removable token), where a badge is a
   STATIC status label (uppercase). <span> = static, <a> = filter link,
   <button> = toggle/removable. */
.hzo-tag {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px var(--space-smd);
    font-family: var(--font-body);
    font-size: var(--fs-75);
    font-weight: var(--fw-500, 500);
    line-height: 1.2;
    color: var(--text-body);
    background: var(--bg-alt);
    border: var(--border-1) solid var(--border);
    border-radius: var(--badge-radius, var(--radius-full));
}
/* Interactive tags (filter links / toggles) lift to a brand tint on hover.
   !important on color for the <a> form fights per-client a:visited; the
   matching :visited rule sits just below (co-located, same pattern as
   .hzo-footer-nap / .hzo-social-icons). */
a.hzo-tag,
button.hzo-tag {
    cursor: pointer;
    text-decoration: none;
    transition: background var(--transition-base), border-color var(--transition-base), color var(--transition-base);
}
a.hzo-tag { color: var(--text-body) !important; }
a.hzo-tag:visited { color: var(--text-body) !important; }
a.hzo-tag:hover,
a.hzo-tag:visited:hover,
button.hzo-tag:hover { background: var(--brand-pale); border-color: var(--brand); color: var(--brand-text) !important; }
a.hzo-tag:focus-visible,
button.hzo-tag:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
/* Selected / active filter chip. */
.hzo-tag.is-active,
.hzo-tag[aria-pressed="true"] {
    background: var(--brand-pale);
    border-color: var(--brand);
    color: var(--brand-text) !important;
}
/* Removable chip — trailing × button. Removal wiring (click ✕ → remove chip,
   move focus, fire hzo:tagremoved) lives in components/tag/tag.js; this styles
   the affordance and its states. */
.hzo-tag--removable { padding-right: 5px; }
.hzo-tag__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: 50%;
    cursor: pointer;
    transition: background var(--transition-base), color var(--transition-base);
}
.hzo-tag__remove:hover { background: var(--border); color: var(--text-heading); }
.hzo-tag__remove:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 1px; }
.hzo-tag__remove > svg { width: 12px; height: 12px; display: block; }
/* Container for a set of (often removable) chips. Removal + focus management is
   wired by components/tag/tag.js off the .hzo-tag__remove buttons. */
.hzo-tag-list { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.hzo-tag-list:focus { outline: none; }   /* -1 fallback focus target when the last chip is removed */


/* --- tag-group/tag-group.css --- */
/* tag-group — @tokens: (none — pure layout, all semantic)

   An inline-wrap layout container for tags / chips / badges. Pure layout
   primitive: it owns only spacing + wrapping, never the aesthetics of its
   children (each child — `.hzo-chip`, `.hzo-badge` — styles itself). No
   interactive states. Token-only: gap reads the semantic --space-* scale. */
.hzo-tag-group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
}

/* Tight — denser packing for label clouds / inline meta rows. */
.hzo-tag-group--tight {
    gap: var(--space-xs);
}


/* --- team-grid/bio-team-card.css --- */
/* ─── Bio / team card ─────────────────────────────────────────────────
   Centered card content (avatar on top). Composes .hzo-card + .hzo-avatar;
   name reuses .hzo-card__title, role reuses .hzo-meta. */
.hzo-bio { text-align: center; align-items: center; }
.hzo-bio .hzo-social-icons,
.hzo-bio .hzo-footer-social { justify-content: center; }


/* --- team-grid/team-grid.css --- */
/* ─── Team / bio grid ─────────────────────────────────────────────────
   Section pattern: a multi-up grid of team/bio cards — avatar, name,
   role, short bio, optional social links. Composes always-loaded
   pieces — .hzo-card (components/card), .hzo-avatar (components/avatar),
   .hzo-social-icons (components/social-icons), .hzo-grid
   (_base/responsive-content-grid), .hzo-eyebrow / .hzo-visual-h2 /
   .hzo-meta (base type) — and owns only the centered bio-card
   alignment, split into bio-team-card.css.

   The loader enqueues ONLY <name>.css, so this file @imports the split
   sheet to give it a load path on the same request (the wave-4
   video.css lesson: a variant sheet without an @import here is dead
   CSS in components mode; before this file existed, bio-team-card.css
   never loaded at all).

   .hzo-team-grid on the [hzo_team_grid] shortcode root is a structural
   hook (unstyled by design — the pieces above carry all the visuals). */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */


/* --- toast/toast.css --- */
/* ─── Toast / snackbar ─────────────────────────────────────────────────
   Transient status messages (form success, add-to-cart, errors). NOT an
   overlay: no focus trap, no background inert — it must NEVER steal focus.
   Announced via an aria-live region (polite default; assertive for errors).
   Created + managed by hzo.toast() in toast.js. Reuses the alert status colours;
   zero new tokens. The empty region is click-through; toasts re-enable pointer
   events so their dismiss button works. */
.hzo-toast-region {
    position: fixed;
    z-index: var(--z-toast);
    right: var(--space-lg);
    bottom: var(--space-lg);
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    width: min(92vw, 380px);
    pointer-events: none;
}
/* Assertive region sits just above the polite one so they don't overlap. */
.hzo-toast-region--assertive { bottom: auto; top: var(--space-lg); }
@media (max-width: 480px) {
    .hzo-toast-region {
        left: var(--space-md);
        right: var(--space-md);
        bottom: var(--space-md);
        width: auto;
    }
    .hzo-toast-region--assertive { top: var(--space-md); bottom: auto; }
}

.hzo-toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: var(--space-md);
    background: var(--surface, #fff);
    border-left: 4px solid var(--text-muted);
    border-radius: var(--radius-control);
    box-shadow: var(--shadow-lg);
    font-size: 15px;
    line-height: var(--lh-normal);
    color: var(--text-body);
    opacity: 0;
    transform: translateY(10px);
    transition: opacity var(--transition-base) ease, transform var(--transition-base) ease;
}
.hzo-toast.is-visible { opacity: 1; transform: none; }
.hzo-toast--success { border-left-color: var(--success); }
.hzo-toast--warning { border-left-color: var(--color-warning); }
.hzo-toast--error   { border-left-color: var(--error); }
.hzo-toast--info    { border-left-color: var(--color-info); }
.hzo-toast__message { flex: 1 1 auto; min-width: 0; }

/* Action slot (Undo / View cart) — a compact, focusable text action (audit G46). */
.hzo-toast__action {
    flex-shrink: 0;
    align-self: center;
    display: inline-flex;
    align-items: center;
    min-height: 44px;                /* WCAG 2.5.8 comfort tap target */
    padding: var(--space-xs) var(--space-sm);
    background: none;
    border: 0;
    border-radius: var(--radius-control);
    font: inherit;
    font-weight: 700;
    color: var(--accent-text);   /* AA text-on-light accent (never bare --accent as text) */
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}
.hzo-toast__action:hover { text-decoration: none; }
.hzo-toast__action:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 1px;
    text-decoration: none;
}

.hzo-toast__close {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    margin: -2px -2px 0 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    border-radius: 50%;
    cursor: pointer;
    transition: background var(--transition-base) ease, color var(--transition-base) ease;
}
.hzo-toast__close:hover { background: var(--bg-alt); color: var(--text-heading); }
.hzo-toast__close:focus-visible { outline: var(--border-2) solid var(--color-focus); outline-offset: 1px; }
.hzo-toast__close > svg { width: 14px; height: 14px; display: block; }

@media (prefers-reduced-motion: reduce) {
    .hzo-toast { transition: none; }
}


/* --- tooltip/clamp-tooltip.css --- */
/* ─── Clamp tooltip — truncation-detect hint ───────────────────────────
   .hzo-clamp is a token-driven line-clamp helper (--clamp-lines). Any element
   carrying [data-hzo-clamp-tooltip] is measured by tooltip.js: when its content
   overflows the clamp it gets .is-truncated, is made focusable, and reveals a
   floating .hzo-tooltip (reused styling) with the full text on hover AND focus
   (WCAG 1.4.13), rechecked on debounced resize. */
.hzo-clamp {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: var(--clamp-lines, 2);
    line-clamp: var(--clamp-lines, 2);
    overflow: hidden;
}
[data-hzo-clamp-tooltip].is-truncated { cursor: help; }
/* The floating hint reuses .hzo-tooltip visuals but is repositioned to fixed
   coordinates by the JS (so measuring the clamped element is never disturbed). */
.hzo-clamp-tooltip {
    position: fixed;
    top: 0;
    left: 0;
    bottom: auto;
    transform: none;
    max-width: 320px;
    z-index: var(--z-toast);
}

/* ═══════════════════════════════════════════════════════════════════════
   ROADMAP BATCH 1 — polish & structure. Every rule below is additive:
   opt-in classes/attributes only, token-driven, all motion gated behind
   prefers-reduced-motion. The only deliberate default changes are the
   text-wrap pass (main.css + .hzo-display below) and the print stylesheet
   at the very end of this file.
   ═══════════════════════════════════════════════════════════════════════ */


/* --- tooltip/tooltip.css --- */
/* ─── Tooltip ──────────────────────────────────────────────────────────
   Short text hint, shown on hover/focus (toggled by tooltip.js). Dark surface
   (--text-heading) + white text. Default above the trigger; --bottom flips
   below when the edge-flip positioner detects top clipping. Never interactive
   (pointer-events:none) — that's a popover. Reuses existing tokens.
   The clamp-tooltip variant sheet is @imported here so the component loader —
   which enqueues only tooltip.css — pulls it on the same request. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */
.hzo-tooltip-wrap { position: relative; display: inline-flex; }
.hzo-tooltip {
    position: absolute;
    bottom: calc(100% + var(--space-sm));
    left: 50%;
    transform: translateX(-50%);
    z-index: var(--z-popover);
    width: max-content;
    max-width: 240px;
    padding: 6px 10px;
    background: var(--text-heading);
    color: var(--bg-page, #fff);   /* inverse ink: page bg on a --text-heading chip — flips with the theme */
    border-radius: var(--radius-control);
    font-size: var(--fs-75);
    line-height: 1.4;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--transition-base) ease, visibility var(--transition-base);
}
.hzo-tooltip.is-open { opacity: 1; visibility: visible; }
.hzo-tooltip--bottom { bottom: auto; top: calc(100% + var(--space-sm)); }
@media (prefers-reduced-motion: reduce) { .hzo-tooltip { transition: none; } }


/* --- variation-swatch/variation-swatch.css --- */
/* variation-swatch — @tokens: --variation-swatch-size, --variation-swatch-pad-y, --variation-swatch-pad-x

   The WooCommerce variation selector styled as swatch chips. WC ships only native
   <select> dropdowns for variable-product attributes; this is the styled radio-group
   control that replaces one such dropdown. One `.hzo-variation-swatch` = one
   attribute (Colour, Size…), rendered as N × (sr-only radio + label option). The JS
   that mirrors the checked radio back into WC's hidden variation <select> is deferred
   (Track 2 wiring), so this ships CSS-only — no `.js`.

   Sibling to rating-input: same radio-backed control pattern (sr-only radios, focus
   ring rides the label, checked/disabled via `:checked +` / `:disabled +`). Unlike
   rating-input there is NO row-reverse and NO cumulative `~` fill — each option is
   an independent chip, emitted in normal ascending order, and only the CHECKED one
   lights up (`:checked +`, the adjacent label only).

   Intended markup (coordinator writes the demo):

     <span class="hzo-variation-swatch">
       <input type="radio" id="c-red" name="attr_colour" value="red" class="hzo-variation-swatch__radio">
       <label for="c-red" class="hzo-variation-swatch__option hzo-variation-swatch__option--color"
              style="--swatch-color:#c0392b">Red</label>
       …  (ascending / normal order)
     </span>

   Two option flavours share every state rule: the default TEXT pill (Size, label
   values) and the `--color` circle (colour attributes; the demo sets --swatch-color
   inline per option). The swatch box has no semantic dimension token in the scale, so
   it's a component token with a literal fallback — the icon-button.css dial pattern
   (`var(--icon-button-size, 40px)`). Everything else reads a semantic/L1 token. */
.hzo-variation-swatch {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs);
}

/* Radio — visually hidden but still focusable & form-submittable (sr-only). Never
   display:none — that removes it from the tab order and the submitted form.
   (copied verbatim from rating-input__radio.) */
.hzo-variation-swatch__radio {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    opacity: 0;
}

/* Option — the clickable <label>. Default = TEXT swatch (size/label pill). */
.hzo-variation-swatch__option {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--variation-swatch-size, 32px);
    min-height: var(--variation-swatch-size, 32px);
    padding: var(--variation-swatch-pad-y, 6px) var(--variation-swatch-pad-x, 12px);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-control);
    background: var(--bg-alt);
    color: var(--text-body);
    font-size: var(--fs-75);
    line-height: var(--lh-none);
    cursor: pointer;
    transition: border-color var(--transition-base) ease,
                box-shadow var(--transition-base) ease,
                color var(--transition-base) ease;
}

/* COLOUR swatch — a circle; the demo sets --swatch-color inline per option. Text is
   hidden (font-size 0) so the colour reads clean; all selected/hover/focus rules below
   apply unchanged. */
.hzo-variation-swatch__option--color {
    width: var(--variation-swatch-size, 32px);
    height: var(--variation-swatch-size, 32px);
    min-width: var(--variation-swatch-size, 32px);
    min-height: var(--variation-swatch-size, 32px);
    padding: 0;
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-full);
    background: var(--swatch-color, var(--bg-alt));
    overflow: hidden;
    font-size: 0;
}

/* Hover — brand border cue. */
.hzo-variation-swatch__option:hover {
    border-color: var(--brand);
}

/* Selected (checked) — a firm brand ring that reads even over a coloured swatch. The
   checked radio's own adjacent label is the only one lit (`+`, no cumulative `~`). */
.hzo-variation-swatch__radio:checked + .hzo-variation-swatch__option {
    border-color: var(--brand);
    box-shadow: 0 0 0 2px var(--brand);
}

/* A11y — required. The radio is hidden, so the focus ring rides its label option. */
.hzo-variation-swatch__radio:focus-visible + .hzo-variation-swatch__option {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}
.hzo-variation-swatch__radio:disabled + .hzo-variation-swatch__option,
.hzo-variation-swatch.is-disabled {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-variation-swatch__option { transition: none; }
}


/* --- video/lite-video.css --- */
/* ─── Lite video facade (.hzo-lite-video) ──────────────────────────────
   Click-to-load video: a plain <button> holding the poster image + a play
   badge — ZERO third-party network cost until the visitor activates it.
   video.js (lite-facade subsystem) swaps in the real iframe on activation
   (click; Enter/Space come free with <button>), reading:
     data-hzo-video-src    REQUIRED — the youtube-nocookie / Vimeo EMBED URL
     data-hzo-video-title  injected iframe title (default "Video player")
     data-hzo-video-allow  iframe allow list (default covers YT/Vimeo)
   The button MUST carry an aria-label naming the video ("Play video: …").
   Focus moves to the injected iframe and a bubbling hzo:videoopen fires.
   Play badge is 64px — comfortably past the 44px target floor. Focus ring
   draws INSET because the .hzo-embed wrapper clips overflow. Markup:
   blocks/video.html / blocks/video-testimonial.html. */
.hzo-lite-video {
    position: relative;
    display: block;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
}
.hzo-lite-video > img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--duration-slow, 400ms) var(--ease-standard, ease);
}
.hzo-lite-video__play {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 64px;
    height: 64px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-full);
    background: var(--lite-video-play-bg, var(--brand));
    color: var(--lite-video-play-icon, var(--text-on-brand));
    box-shadow: var(--shadow-lg);
    transition: transform var(--duration-base) var(--ease-spring),
                background var(--duration-base) var(--ease-standard);
}
.hzo-lite-video__play svg {
    width: 24px;
    height: 24px;
    display: block;
    margin-left: 3px;   /* optical centering for the triangle */
}
.hzo-lite-video:hover > img,
.hzo-lite-video:focus-visible > img { transform: scale(1.03); }
.hzo-lite-video:hover .hzo-lite-video__play,
.hzo-lite-video:focus-visible .hzo-lite-video__play {
    transform: translate(-50%, -50%) scale(1.08);
    background: var(--lite-video-play-bg-hover, var(--brand-hover-on-light));
}
.hzo-lite-video:focus-visible { outline: 3px solid var(--color-focus); outline-offset: -3px; }
@media (prefers-reduced-motion: reduce) {
    .hzo-lite-video > img,
    .hzo-lite-video__play { transition: none; }
    .hzo-lite-video:hover > img,
    .hzo-lite-video:focus-visible > img { transform: none; }
    .hzo-lite-video:hover .hzo-lite-video__play,
    .hzo-lite-video:focus-visible .hzo-lite-video__play { transform: translate(-50%, -50%); }
}

/* G45 — spinner shown in the .hzo-embed frame while a clicked embed's iframe
   loads over the network (the poster is already gone). video.js toggles
   .hzo-lite-video-loading on the .hzo-embed parent and clears it on the iframe
   load event. Self-contained keyframe so it never depends on the gated
   spinner.css. The ::after paints above the (blank, still-loading) iframe. */
@keyframes hzo-lite-video-spin { to { transform: rotate(360deg); } }
.hzo-lite-video-loading::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 40px;
    height: 40px;
    margin: -20px 0 0 -20px;
    border: 3px solid rgba(255, 255, 255, 0.35);
    border-top-color: #fff;
    border-radius: 50%;
    animation: hzo-lite-video-spin 0.6s linear infinite;
    pointer-events: none;
    z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
    .hzo-lite-video-loading::after { animation-duration: 1.4s; }
}


/* --- video/video-background.css --- */
/* ─── Video background band (.hzo-section--video) ──────────────────────
   The --image band with a looping ambient <video> instead of a photo.
   Structure (full example in blocks/video.html):
     <section class="hzo-section hzo-section--video">
       <video autoplay muted loop playsinline preload="metadata"
              poster="frame.jpg" aria-hidden="true">
         <source src="ambient.mp4" type="video/mp4"></video>
       <button type="button" class="hzo-video-toggle" data-hzo-video-toggle
               aria-pressed="false">Pause</button>
       <div class="hzo-container"> … </div>
     </section>
   The scrim reuses the --image knobs (--hzo-section-scrim strength,
   --scrim-color tint). Stacking: the video is a positioned child painted
   first; the scrim ::before carries z-index:1 (an unpositioned ::before
   would paint UNDER a later positioned sibling — same trap the media-band
   documents); every other child rides z-index:2 above both. The pause
   control is REQUIRED (WCAG 2.2.2 — >5s of auto-motion needs a visible
   pause); video.js (bg-band subsystem) wires it (aria-pressed + visible
   label sync), NEVER autoplays under prefers-reduced-motion (paused
   immediately at the poster/first frame; the control still works), and
   pauses the file while the band is offscreen. */
.hzo-section--video {
    position: relative;
    overflow: hidden;
}
.hzo-section--video > video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.hzo-section--video::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background: color-mix(in srgb, var(--scrim-color, var(--brand-deep)) var(--hzo-section-scrim, 60%), transparent);
}
.hzo-section--video > :where(:not(video):not(.hzo-video-toggle)) {
    position: relative;
    z-index: 2;   /* lift content above the scrim (:where = zero specificity) */
}
/* Light text over the scrim — same guarantees as --image, expressed through
   the on-dark token (fallback white lives INSIDE var() per §1.22). */
.hzo-section--video h1, .hzo-section--video h2, .hzo-section--video h3,
.hzo-section--video h4, .hzo-section--video h5, .hzo-section--video h6,
.hzo-section--video .hzo-eyebrow, .hzo-section--video .hzo-display { color: var(--heading-color-on-dark, #fff) !important; }
.hzo-section--video p, .hzo-section--video .hzo-subtitle { color: color-mix(in srgb, var(--heading-color-on-dark, #fff) 90%, transparent); }
.hzo-section--video a:not(.hzo-btn) { color: var(--heading-color-on-dark, #fff); }
/* The visible pause/play control — a quiet surface pill, bottom-right. */
.hzo-video-toggle {
    position: absolute;
    right: var(--space-lg);
    bottom: var(--space-lg);
    z-index: 2;
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 10px 18px;
    border: var(--border-1) solid var(--surface-border, transparent);
    border-radius: var(--radius-full);
    background: var(--surface, #fff);
    color: var(--on-surface, var(--text-heading));
    font-family: var(--font-body);
    font-size: var(--fs-100);
    font-weight: var(--fw-600, 600);
    box-shadow: var(--shadow-md);
    cursor: pointer;
    transition: background var(--duration-base) var(--ease-standard);
}
.hzo-video-toggle:hover { background: var(--bg-alt); }
.hzo-video-toggle:focus-visible { outline: 3px solid var(--color-focus); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
    .hzo-video-toggle { transition: none; }
}


/* --- video/video.css --- */
/* ─── Video widget (lite facade + background band) ─────────────────────
   Aggregator sheet — the component loader enqueues ONLY <name>.css, so the
   two variant sheets are @imported here to ride the same request (this is
   their only load path in gated mode):
     lite-video.css       .hzo-lite-video — click-to-load facade
     video-background.css .hzo-section--video band + .hzo-video-toggle
   Behavior: video.js (lite + background subsystems only — mode="player" is
   deliberately JS-free and uses native <video controls>).
   Shortcode: video.php [hzo_video].
   @imports must precede all rules — keep them first. */
/* DELIVERY (v1.11.10): the sibling sheet(s) this file used to @import are now
   enqueued as real handles, in the same order, by
   hzostudio_component_sidecar_sheets() in includes/component-loader.php — so any
   "@imported here" wording above describes the old path. DO NOT re-add one: the
   CSS bundler could not resolve an @import carrying the ?ver= that
   scripts/zip_builder.py stamps on at build time, and 37 sheets shipped dark
   fleet-wide on v1.11.8/9 as a result. check_css_delivery.py fails the build. */

/* ─── Self-hosted player frame (mode="player", PF-032) ──────────────────
   .hzo-embed sets its child <video> to object-fit: cover, which is correct for
   ambient/decorative media and wrong for a player: cropping a commercial cuts
   off content the client paid to have in frame. --player letterboxes instead
   and paints the letterbox bars a real black rather than the frame's --bg-alt,
   so the pillarbox reads as part of the video rather than as a layout gap.
   Opt back into cropping with fit="cover" on the shortcode. */
.hzo-embed--player { background: #000; }
.hzo-embed--player > video { object-fit: contain; }

/* Reduced-motion: kill every transition this component animates (belt &
   braces on top of the variant sheets' own blocks — per the convention,
   the aggregator ends with a transition-kill). */
@media (prefers-reduced-motion: reduce) {
    .hzo-lite-video > img,
    .hzo-lite-video__play,
    .hzo-video-toggle { transition: none; }
}


/* --- wishlist/wishlist.css --- */
/* wishlist — @tokens: --wishlist-size, --wishlist-padding-x, --wishlist-padding-y

   A styled save/favorite toggle: a bare <button> heart that flips between an
   empty outline (unsaved) and a filled accent heart (saved). Interactive
   sibling family to the commerce controls — it owns its OWN root + tokens, it
   does NOT borrow icon-button's or chip's dials. STYLED CONTROL ONLY: the
   toggle is cosmetic (JS flips .is-active + aria-pressed and emits an event);
   persistence / storage / server sync are intentionally out of scope.

   Token-only: the box dimension and the labeled-variant padding pair have no
   semantic token in the scale, so they are component tokens with a literal
   fallback — the same dial pattern icon-button.css / chip.css use. Everything
   else reads a semantic / L1 token. */
.hzo-wishlist {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--wishlist-size, 40px);
    height: var(--wishlist-size, 40px);
    padding: 0;
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-full);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    transition: color var(--transition-base) ease, border-color var(--transition-base) ease, background var(--transition-base) ease;
}

/* Icon wrapper — centers the heart glyph; the svg is drawn as an outline
   (fill:none; stroke:currentColor set in the markup) so it inherits the
   button's color. */
.hzo-wishlist__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.hzo-wishlist__icon > svg {
    display: block;
    width: 60%;
    height: 60%;
}

/* Hover — border firms up to brand, ink darkens to heading (still empty). */
.hzo-wishlist:hover {
    border-color: var(--brand);
    color: var(--text-heading);
}

/* Saved — the one state vocab (.is-active); markup carries aria-pressed="true".
   Accent ink + border, and the heart fills to a solid accent silhouette. */
.hzo-wishlist.is-active {
    color: var(--accent);
    border-color: var(--accent);
}
.hzo-wishlist.is-active .hzo-wishlist__icon > svg {
    fill: currentColor;
}

/* Labeled variant — becomes a pill with trailing text (e.g. "Save"). Width
   relaxes to the content, component-token padding + a small gap. Minimal. */
.hzo-wishlist--labeled {
    width: auto;
    gap: var(--space-2xs);
    padding: var(--wishlist-padding-y, 8px) var(--wishlist-padding-x, 12px);
    border-radius: var(--radius-pill);
}
.hzo-wishlist__label {
    font-size: var(--fs-75);
    line-height: var(--lh-none);
}

/* A11y — required. Focus ring on the button; disabled state. Reduced-motion is
   also handled globally by components/_base/a11y.css. */
.hzo-wishlist:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}
.hzo-wishlist[disabled],
.hzo-wishlist[aria-disabled="true"] {
    opacity: var(--opacity-disabled);
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .hzo-wishlist { transition: none; }
}


/* --- zip-checker/zip-checker.css --- */
/* ─── Service-Area ZIP Checker (.hzo-zip-checker) ──────────────────────────────
   W-042 — an interactive "do you come to my town?" gate. Type a ZIP/town → an
   instant, fully client-side answer + per-region messaging, a nearest-branch
   card, a miss CTA. On a hit it reveals success content and passes the ZIP to
   the lead form. The town-list + areaServed schema are server-rendered (this
   sheet only styles the interactive shell).

   GATING: ALWAYS-LOAD (zip-checker.php un-gates itself) — it renders from a
   sticky/header CTA, a footer strip, or an ACF field the scan can't see.

   Tokens only: --brand/--accent families, --space-*, --radius-*, --shadow-*,
   --border-*, --dur-*/--ease-*, --z-* ladder (the suggest listbox rides
   --z-popover=60, below --z-toast=70). No hardcoded hex; px only for icon
   sizing / hairline borders, matching the input + button primitives. */

.hzo-zip-checker {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    margin: 0;
    max-width: 34rem;
    color: var(--text-body);
}

/* ── Field: label + controls + hint ──────────────────────────────────────── */
.hzo-zip-checker__field {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}

.hzo-zip-checker__label {
    font-size: var(--fs-100);
    font-weight: var(--fw-600);
    color: var(--text-heading);
}

.hzo-zip-checker__controls {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: var(--space-sm);
}

/* Input owns the flexible width; the combobox listbox anchors to this wrap. */
.hzo-zip-checker__inputwrap {
    position: relative;
    flex: 1 1 12rem;
    min-width: 0;
    display: block;
}

.hzo-zip-checker__input {
    width: 100%;
    box-sizing: border-box;
    padding: var(--space-sm) var(--space-md);
    font-size: var(--fs-200);
    color: var(--text-body);
    background: var(--input-bg);
    border: var(--border-1) solid var(--input-border);
    border-radius: var(--radius-input);
    transition: border-color var(--dur-1) var(--ease-standard),
                box-shadow var(--dur-1) var(--ease-standard);
}
.hzo-zip-checker__input::placeholder { color: var(--color-placeholder); }

.hzo-zip-checker__input:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: 2px;
    border-color: var(--brand);
}

/* Input-validation error (malformed / non-numeric when match=zip). The MISS
   answer is NOT an error — it never sets this state. */
.hzo-zip-checker__input.is-error,
.hzo-zip-checker__input[aria-invalid="true"] {
    border-color: var(--error);
}
.hzo-zip-checker__input.is-error:focus-visible,
.hzo-zip-checker__input[aria-invalid="true"]:focus-visible {
    outline-color: var(--error);
}

/* ── Buttons (submit + geolocate) — lean on the shared control tokens ────── */
.hzo-zip-checker__submit,
.hzo-zip-checker__geo {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    /* ≥44×44 comfort target */
    min-height: 44px;
    padding: var(--space-sm) var(--space-md);
    font-size: var(--fs-100);
    font-weight: var(--btn-weight);
    line-height: var(--lh-none);
    border-radius: var(--radius-input);
    border: var(--border-1) solid transparent;
    cursor: pointer;
    transition: background-color var(--dur-1) var(--ease-standard),
                border-color var(--dur-1) var(--ease-standard),
                color var(--dur-1) var(--ease-standard);
}

.hzo-zip-checker__submit {
    background: var(--btn-primary-bg);
    color: var(--btn-primary-text);
}
.hzo-zip-checker__submit:hover { background: var(--accent-hover-on-light); }
.hzo-zip-checker__submit:active { transform: translateY(1px); }

.hzo-zip-checker__geo {
    background: transparent;
    color: var(--brand-text);
    border-color: var(--border);
}
.hzo-zip-checker__geo:hover {
    background: var(--brand-pale);
    border-color: var(--brand);
}
.hzo-zip-checker__geo:active { transform: translateY(1px); }

.hzo-zip-checker__submit:focus-visible,
.hzo-zip-checker__geo:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: 2px;
}

.hzo-zip-checker__geo-icon { width: 18px; height: 18px; flex-shrink: 0; }

/* Loading (geolocate path ONLY — the check itself is synchronous, no spinner).
   The dot SLOWS under reduced-motion, never freezes (PRESERVE-5 / G13/G60). */
.hzo-zip-checker__geo.is-loading {
    color: var(--text-muted);
    cursor: progress;
}
.hzo-zip-checker__geo.is-loading .hzo-zip-checker__geo-icon {
    animation: hzo-zip-geo-pulse 1s var(--ease-in-out) infinite;
}
@keyframes hzo-zip-geo-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.4; }
}

/* ── Inline hint / input-validation message (aria-describedby target) ────── */
.hzo-zip-checker__hint {
    margin: 0;
    font-size: var(--fs-75);
    color: var(--text-muted);
}
.hzo-zip-checker__hint.is-error { color: var(--error-text); }

/* ── Result region (role=status). Hit = affirmative, miss = neutral info. ── */
.hzo-zip-checker__result {
    margin: 0;
    font-size: var(--fs-200);
    line-height: var(--lh-snug);
    min-height: 0;
}
.hzo-zip-checker__result:empty { display: none; }

.hzo-zip-checker__result-msg { display: inline; }

/* Non-color glyph carries the hit/miss meaning too (forced-colors safe). */
.hzo-zip-checker__result::before {
    speak: none;
}
.hzo-zip-checker__result.is-hit {
    color: var(--success-text);
    font-weight: var(--fw-600);
}
.hzo-zip-checker__result.is-hit::before {
    content: "\2713\00A0"; /* ✓ */
}
.hzo-zip-checker__result.is-miss {
    color: var(--text-body);
}
.hzo-zip-checker__result.is-miss::before {
    content: "\2139\00A0"; /* ℹ (info, not error) */
    color: var(--info);
}

/* The inline CTA rendered inside the result region (hit / miss action). */
.hzo-zip-checker__cta {
    display: inline-flex;
    align-items: center;
    margin-inline-start: var(--space-sm);
    padding: var(--space-xs) var(--space-smd);
    font-size: var(--fs-75);
    font-weight: var(--fw-600);
    color: var(--btn-primary-text);
    background: var(--btn-primary-bg);
    border-radius: var(--radius-input);
    text-decoration: none;
    cursor: pointer;
    border: var(--border-1) solid transparent;
    transition: background-color var(--dur-1) var(--ease-standard);
}
.hzo-zip-checker__cta:hover { background: var(--accent-hover-on-light); }
.hzo-zip-checker__cta:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: 2px;
}

/* ── Reveal block (coupon / branch / booking) ─────────────────────────────── */
.hzo-zip-checker__reveal {
    padding: var(--space-md);
    background: var(--surface-2);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-sm);
    animation: hzo-zip-reveal-in var(--dur-3) var(--ease-out);
}
.hzo-zip-checker__reveal[hidden] { display: none; }
.hzo-zip-checker__reveal:focus-visible {
    outline: var(--border-2) solid var(--focus-ring);
    outline-offset: 2px;
}
.hzo-zip-checker__reveal-title {
    margin: 0 0 var(--space-xs);
    font-size: var(--fs-300);
    font-weight: var(--fw-700);
    color: var(--text-heading);
}
.hzo-zip-checker__reveal-line { margin: 0 0 var(--space-xs); }
.hzo-zip-checker__reveal-line:last-child { margin-bottom: 0; }
.hzo-zip-checker__coupon-code {
    display: inline-block;
    padding: var(--space-xs) var(--space-smd);
    font-family: var(--font-number);
    font-size: var(--fs-400);
    letter-spacing: var(--tracking-wide);
    color: var(--brand-text);
    background: var(--brand-pale);
    border: var(--border-1) dashed var(--brand);
    border-radius: var(--radius-input);
}
.hzo-zip-checker__reveal-link {
    color: var(--brand-text);
    font-weight: var(--fw-600);
}

@keyframes hzo-zip-reveal-in {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
}

/* ── Suggest combobox listbox (APG) — rides --z-popover (below --z-toast) ── */
.hzo-zip-checker__listbox {
    position: absolute;
    z-index: var(--z-popover);
    top: calc(100% + var(--space-xs));
    left: 0;
    right: 0;
    max-height: 15rem;
    margin: 0;
    padding: var(--space-xs);
    list-style: none;
    overflow-y: auto;              /* internal scroll — never hijacks page scroll */
    overscroll-behavior: contain;
    background: var(--surface);
    border: var(--border-1) solid var(--border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-lg);
}
.hzo-zip-checker__listbox[hidden] { display: none; }

.hzo-zip-checker__option {
    display: block;
    padding: var(--space-sm) var(--space-smd);
    min-height: 44px;             /* finger-height option rows */
    box-sizing: border-box;
    border-radius: var(--radius-input);
    cursor: pointer;
    color: var(--text-body);
}
.hzo-zip-checker__option:hover,
.hzo-zip-checker__option.is-active,
.hzo-zip-checker__option[aria-selected="true"] {
    background: var(--brand-pale);
    color: var(--brand-text);
}
.hzo-zip-checker__option--empty {
    color: var(--text-muted);
    cursor: default;
}

/* ── Server-rendered town list (crawlable; visible with or without JS) ────── */
.hzo-zip-checker__towns {
    margin-top: var(--space-xs);
    font-size: var(--fs-100);
}
.hzo-zip-checker__towns-summary {
    margin: 0 0 var(--space-xs);
    font-weight: var(--fw-600);
    color: var(--text-heading);
    cursor: pointer;
}
details.hzo-zip-checker__towns > summary { list-style: revert; }
.hzo-zip-checker__town-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs) var(--space-md);
    margin: 0;
    padding: 0;
    list-style: none;
    color: var(--text-muted);
}
.hzo-zip-checker__town { white-space: nowrap; }

/* ── Reduced motion (PRESERVE-5): stop reveal entrance + geo pulse with
   content visible; the answer text stays put. ─────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
    .hzo-zip-checker__reveal { animation: none; }
    .hzo-zip-checker__geo.is-loading .hzo-zip-checker__geo-icon { animation-duration: 2.4s; }
    .hzo-zip-checker__input,
    .hzo-zip-checker__submit,
    .hzo-zip-checker__geo,
    .hzo-zip-checker__cta { transition: none; }
    .hzo-zip-checker__submit:active,
    .hzo-zip-checker__geo:active { transform: none; }
}

/* ── Forced colors: hit/miss carry a non-color glyph + text; keep controls and
   the selected option legible under High Contrast. ───────────────────────── */
@media (forced-colors: active) {
    .hzo-zip-checker__input,
    .hzo-zip-checker__submit,
    .hzo-zip-checker__geo,
    .hzo-zip-checker__reveal,
    .hzo-zip-checker__listbox {
        border: 1px solid CanvasText;
    }
    .hzo-zip-checker__option.is-active,
    .hzo-zip-checker__option[aria-selected="true"] {
        background: Highlight;
        color: HighlightText;
    }
}

/* ── Print: server-rendered town-list + schema always print; hide the
   interactive geolocate/combobox chrome; the input prints as a plain field. ── */
@media print {
    .hzo-zip-checker__geo,
    .hzo-zip-checker__listbox { display: none !important; }
    .hzo-zip-checker__reveal[hidden] { display: none; }
}

