  :root{
    --ink:#1C2B22;
    --ink-soft:#33473A;
    --paper:#F2EEE3;
    --paper-dim:#E8E1CE;
    --brass:#A07C2E;
    --brass-light:#C9A85C;
    --oxblood:#7A2E2E;
    --line: rgba(28,43,34,0.18);
    --ink-tint-035: rgba(28,43,34,0.035);
    --ink-tint-045: rgba(28,43,34,0.045);
    --ink-tint-06: rgba(28,43,34,0.06);
    --ink-tint-07: rgba(28,43,34,0.07);
    --ink-tint-10: rgba(28,43,34,0.1);
    --brass-tint-12: rgba(160,124,46,0.12);
    --brass-tint-09: rgba(160,124,46,0.09);
    --brass-hover-20: rgba(201,168,92,0.2);
    --brass-light-50: rgba(201,168,92,0.5);
    --paper-90: rgba(242,238,227,0.9);
    --paper-55: rgba(242,238,227,0.55);
    --paper-45: rgba(242,238,227,0.45);
    --paper-40: rgba(242,238,227,0.4);
    --paper-08: rgba(242,238,227,0.08);
    --paper-18: rgba(242,238,227,0.18);
    --header-bg: var(--ink);
    --cta-bg: var(--brass);
    --radius: 2px;
    /* Fluid type + spacing scale — each size grows smoothly from ~390px to
       ~1200px of viewport, then locks at its max, so the page scales up on big
       screens instead of staying phone-sized. Applied to the shared high-impact
       primitives (body copy, section titles, contact buttons). clamp() does
       implicit calc, so no calc() wrapper. */
    --fs-body:  clamp(14px, 12.6px + 0.37vw, 17px);
    /* Section headings — the kit's --type-display-m (26→32), viewport-based so
       every section head on the page matches whether its section stays in the
       reading column or breaks out to 1024px. section_title_layouts.css sizes
       the same step in cqi, which is right for the lab and the dashboard picker
       but would give one page two heading sizes; the pin below reconciles them.
       Retuned from clamp(21px, 16.7px + 1.10vw, 30px) — 30px was a step that
       exists in neither scale. */
    --fs-h2:    clamp(26px, 2.6vw, 32px);
    --fs-btn:   clamp(13px, 11.8px + 0.31vw, 15px);
    --sp-section:   clamp(32px, 22.4px + 2.47vw, 52px);
    --sp-section-x: clamp(22px, 16.2px + 1.48vw, 34px);
    /* Font pairing (overridden inline per agent in agent_bio.html).
       --font-display = headings, --font-body = body text. */
    --font-display: 'Fraunces', serif;
    --font-body: 'IBM Plex Sans', sans-serif;
    /* Weight of the display face, per the chosen pairing (set inline in
       vendor_page.html). Applied to the big hero title so single-weight display
       faces (Bebas Neue, Anton, …) render at their intended weight. */
    --font-display-weight: 600;

    /* ====================================================================
       ZULLI TYPOGRAPHY KIT v1.0 — section 07 handoff tokens.

       Deliberately scoped HERE rather than in a shared :root, because three of
       these names already mean something different elsewhere in the codebase:
         --radius-lg  is 12px in base.css and ui_concept.css ("dialogs, feature
                      cards, swatches"); the kit's is 6px.
         --space-*    in ui_concept.css alias a separate numeric scale
                      (--space-8 etc.), not the kit's clamps.
         --font-*     on this page are the VENDOR's chosen pairing, set inline
                      in vendor_page.html — the kit's family aliases are
                      deliberately NOT included below, or they would clobber it.
       agent_bio.css is loaded only by vendor_page.html, so defining them here
       gives the public page the kit without touching the dashboard.

       The layout modules (service/pricing/faq/about/gallery/promo/link) read
       these through `var(--token, <kit value>)`. The fallbacks match, so
       installing this changes nothing on its own — what it buys is the two
       media-query blocks at the end of this file, which could not fire while
       the tokens were undefined. That is what was holding touch targets at
       40px instead of the kit's 44px floor.
       ==================================================================== */

    /* space scale — fluid */
    --space-3xs: 4px;
    --space-2xs: clamp(6px, 0.7vw, 8px);
    --space-xs:  clamp(8px, 1vw, 12px);
    --space-sm:  clamp(12px, 1.4vw, 16px);
    --space-md:  clamp(16px, 2vw, 24px);
    --space-lg:  clamp(22px, 3vw, 36px);
    --space-xl:  clamp(32px, 5vw, 56px);
    --space-2xl: clamp(44px, 7vw, 80px);
    --space-3xl: clamp(56px, 9vw, 120px);

    /* controls */
    --btn-h-sm:  clamp(32px, 3.4vw, 34px);
    --btn-h-md:  clamp(40px, 4.4vw, 44px);
    --btn-h-lg:  clamp(48px, 5.2vw, 54px);
    --btn-px-sm: clamp(10px, 1.2vw, 14px);
    --btn-px-md: clamp(14px, 1.8vw, 20px);
    --btn-px-lg: clamp(20px, 2.6vw, 30px);
    --btn-gap:   0.55em;

    /* radii — kit values, NOT base.css's 12px --radius-lg */
    --radius-sm: 4px;
    --radius-md: 5px;
    --radius-lg: 6px;

    /* icons */
    --icon-xs: 14px;
    --icon-sm: 16px;
    --icon-md: 18px;
    --icon-lg: clamp(20px, 2vw, 22px);
    --icon-xl: clamp(24px, 2.6vw, 28px);
    --icon-inline: 1.15em;
    --icon-stroke: 1.5;

    /* measure + tracking */
    --measure-body:    68ch;
    --measure-display: 56ch;
    --track-label:   0.08em;
    --track-eyebrow: 0.14em;
    --track-display-m: -0.016em;

    /* title-stack gaps */
    --stack-eb-gap:    clamp(8px, 1vw, 12px);
    --stack-sh-gap:    clamp(7px, 0.9vw, 10px);
    --stack-eb-gap-sm: clamp(6px, 0.8vw, 9px);
  }

  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;}
  /* .vhero-bg's full-bleed header photo (see its comment) sizes itself with
     width:100vw — 100vw measures the *whole* viewport, scrollbar included,
     while the page's actual visible width is narrower once a vertical
     scrollbar is showing (virtually always, on any page with real content).
     So the photo bled a few px past the true right edge, and the browser
     added a horizontal scrollbar just to let you reach that sliver — only
     ever noticeable once a header photo made .vhero-bg exist at all. The
     couple of pixels of photo that spill past the actual edge here are
     invisible anyway; just stop the page from scrolling to show them. */
  html{ overflow-x:hidden; }
  /* Clean, modern scrollbar in the vendor's brass palette — same treatment
     as the rest of the site (see base.css), just in the vendor's own accent
     instead of the site gray, so each vendor's page still reads as themed.
     WebKit/Blink only: the standard scrollbar-width/scrollbar-color props
     are intentionally omitted — setting them makes Chromium ignore these
     ::-webkit-scrollbar rules (and re-adds the native arrow buttons).
     Firefox falls back to its own scrollbars, which have no arrows anyway. */
  @supports selector(::-webkit-scrollbar){
    *{ scrollbar-width:auto; }
  }
  *::-webkit-scrollbar{ width:10px; height:10px; }
  *::-webkit-scrollbar-track{ background:transparent; }
  *::-webkit-scrollbar-thumb{
    background: var(--brass-light);
    border-radius:10px;
    border:2px solid transparent;
    background-clip:padding-box;
  }
  *::-webkit-scrollbar-thumb:hover{ background: var(--brass); background-clip:padding-box; }
  *::-webkit-scrollbar-button,
  *::-webkit-scrollbar-button:single-button,
  *::-webkit-scrollbar-button:start,
  *::-webkit-scrollbar-button:end,
  *::-webkit-scrollbar-button:horizontal,
  *::-webkit-scrollbar-button:vertical{
    display:none; width:0; height:0;
  }
  *::-webkit-scrollbar-corner{ background:transparent; }
  body{
    /* The page background now lives here so it runs edge-to-edge like a real
       website (the content column below only caps + centres the content). */
    background: var(--paper);
    font-family:var(--font-body);
    /* Fluid base size — cascades to all body copy not carrying its own size. */
    font-size: var(--fs-body);
    color: var(--ink);
    -webkit-font-smoothing:antialiased;
    /* A long unbroken word or URL wraps instead of being clipped by
       html{overflow-x:hidden}. */
    overflow-wrap:break-word;
    display:flex;
    justify-content:center;
  }

  @media (prefers-reduced-motion: reduce){
    *{animation-duration:0.001ms !important; animation-iteration-count:1 !important; transition-duration:0.001ms !important;}
  }

  /* Off-screen until it receives keyboard focus, so a Tab from the top of the
     page can jump straight past the header to the sections below without
     showing anything to a mouse/touch visitor. */
  .skip-link{
    position:absolute; left:8px; top:-48px; z-index:1000;
    background:var(--paper); color:var(--ink);
    padding:10px 16px; border-radius:var(--radius);
    box-shadow:0 2px 10px rgba(0,0,0,.25);
    font-size:14px; font-weight:600; text-decoration:none;
    transition:top .15s ease;
  }
  .skip-link:focus{ top:8px; }

  /* Content is a single centred column, but the page now reads like a real
     website rather than a floating link-in-bio card: the paper background lives
     on <body> and the photo backdrop (.page-bg) runs edge-to-edge, while this
     container only caps + centres the content — no card background, no shadow.
     --page-max drives the content width; the breakpoints below bump it.
     Standard tiers: base (mobile) → 768px (tablet) → 1024px (desktop). */
  .page{
    width:100%;
    --page-max:430px;
    max-width:var(--page-max);
    min-height:100vh;
    /* Sticky footer: a flex column so the footer is pushed to the bottom
       (margin-top:auto below). Without it, a short page — e.g. with the
       contact buttons off — still stretches to min-height:100vh, but the
       footer sits right under the last section, leaving a gap between the
       footer and the bottom of the viewport (the page looks pushed up). */
    display:flex;
    flex-direction:column;
    position:relative;
    background:transparent;
  }
  /* Absorbs leftover height so the footer always sits at the viewport bottom,
     no matter how little content is above it. */
  .page > .site-footer{ margin-top:auto; }
  /* The hero title already scales on its own (clamp(40px,5.5vw,48px)); the
     content grids are fr-based and reflow into the extra width. So these tiers
     mainly need to grow the column and give the body copy a small readability
     bump so it doesn't read as small-text-in-a-big-frame. */
  @media (min-width:768px){
    .page{ --page-max:600px; }
    .vhero-body{ font-size:15px; }
  }
  @media (min-width:1024px){
    .page{ --page-max:680px; }
  }

  /* Grid-based body modules (pricing packages, services, gallery) — plus the
     header's top nav bar (logo + hamburger/QR) — are allowed to break out of
     .page's reading-width column on desktop and stretch toward a wider cap of
     their own, so a 3-up row actually fits one row instead of wrapping, and
     the nav bar reads full-width instead of sitting in a narrow strip. The
     centered hero content (name, pills, CTA) and every other section stay at
     --page-max — unaffected, per the design. --wide-max lives on .page so
     every wide element (descendants of .page) shares the same value. The
     breakout is centred on .page itself (which is centred on the viewport via
     body's flex), not the viewport directly: equal-and-opposite margins sized
     off the gap between --page-max and --wide-max re-centre the wider box
     under the same axis. min() in --wide-max keeps it from overflowing the
     viewport on the smaller end of the desktop range, where 1024px of width
     wouldn't otherwise fit with room to breathe. */
  @media (min-width:1024px){
    .page{ --wide-max: min(1024px, calc(100vw - 48px)); }
    #section-packages, #section-offerings, #section-gallery, .vhero{
      width: var(--wide-max);
      margin-left: calc((var(--page-max) - var(--wide-max)) / 2);
      margin-right: calc((var(--page-max) - var(--wide-max)) / 2);
    }
  }

  .page > * { position:relative; z-index:1; }

  /* Custom background photo, optionally blurred. Sits behind the ledger
     texture and all real content (see the z-index:1 rule above) — the blur
     lives on a separately-positioned ::after so it can bleed past its own
     edges without blurring (or clipping) anything else on the page. */
  .page-bg{
    /* Fixed to the viewport so the photo stays put behind the page as the user
       scrolls, like a backdrop — full-bleed now (a real-website background),
       not capped to the content column. */
    position:fixed;
    top:0; left:0;
    width:100%;
    height:100vh; height:100dvh;
    overflow:hidden;
    pointer-events:none;
  }
  .page > .page-bg{ z-index:0; }
  .page-bg::after{
    content:"";
    position:absolute; inset:0;
    z-index:0;
    /* The photo renders clean by default — no automatic wash (an earlier
       version always tinted it toward --paper for guaranteed text contrast;
       see .vhero-bg::after's comment below for why that was dropped there
       too). --bg-overlay is the vendor's own opt-in legibility wash
       (background_overlay/background_overlay_intensity) — 'transparent'
       unless they turn it on. */
    background-image: linear-gradient(var(--bg-overlay, transparent), var(--bg-overlay, transparent)), var(--bg-image);
    background-size:cover;
    background-position:center;
  }
  .page-bg.is-blurred::after{
    inset:-24px;
    filter: blur(var(--bg-blur, 12px));
  }
  /* Layered look: paint the selected background pattern as a subtle texture
     OVER the photo (which would otherwise fully hide it). --hb-pattern /
     --hb-pattern-size inherit from the .hb-surface <body>, already resolved in
     the palette + --hb-boost. Scoped to the TILED patterns only — the
     directional washes (mesh/horizon/spotlight/rays/rings) set their sizing
     directly on <body>, not as custom props, so they wouldn't tile right here
     and read as too busy over a photo anyway. Sits above the photo ::after; the
     photo's blur never touches it, so the texture stays crisp. */
  body.hb-dots .page-bg::before,
  body.hb-grid .page-bg::before,
  body.hb-diagonal .page-bg::before,
  body.hb-crosshatch .page-bg::before{
    content:"";
    position:absolute; inset:0;
    z-index:1;
    pointer-events:none;
    opacity:.5;
    background-image: var(--hb-pattern, none);
    background-size: var(--hb-pattern-size, auto);
  }

  a{color:inherit; text-decoration:none;}
  button{font-family:inherit;}

  /* ---------- BROKERAGE LOGO ---------- */
  .brokerage-logo{
    background:transparent;
    display:flex;
    flex-direction:column;
    align-items:flex-start;
    justify-content:center;
    flex-shrink:0;
    padding-top:4px;
  }
  .brokerage-logo .logo-img{
    /* --logo-h is the agent's chosen height (set inline); width scales with it,
       capped at 3:1 so a very wide logo can't run into the QR button. */
    height: var(--logo-h, 40px);
    width:auto;
    max-width: calc(var(--logo-h, 40px) * 3);
    object-fit:contain;
  }

  @keyframes logoIn{
    0%{ transform: scale(0.8); opacity:0; }
    100%{ transform: scale(1); opacity:1; }
  }

  /* ---------- HEADER ---------- */
  header{
    padding:34px 22px 26px;
    background:
      radial-gradient(circle at 85% -10%, var(--brass-tint-12), transparent 55%),
      var(--header-bg);
    color: var(--paper);
    border-bottom: 4px solid var(--brass);
  }
  .qr-trigger{
    width:38px; height:38px;
    border-radius:50%;
    border:1.5px solid var(--brass-light);
    background: var(--paper-08);
    color: var(--paper);
    display:flex; align-items:center; justify-content:center;
    cursor:pointer;
    flex-shrink:0;
    animation: logoIn 0.5s ease both;
    animation-delay:0.2s;
    transition: background 0.2s ease;
  }
  .qr-trigger:hover{ background: var(--brass-hover-20); }
  .qr-trigger svg{ width:18px; height:18px; fill:currentColor; }

  .qr-modal-backdrop{
    position:fixed; inset:0;
    background: rgba(15,20,16,0.72);
    display:flex; align-items:center; justify-content:center;
    opacity:0;
    pointer-events:none;
    transition: opacity 0.25s ease;
    z-index:90;
  }
  .qr-modal-backdrop.open{ opacity:1; pointer-events:auto; }

  .qr-modal{
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:14px;
    background: var(--paper);
    border-radius:12px;
    padding:32px 28px;
    position:relative;
    transform: scale(0.92);
    transition: transform 0.25s cubic-bezier(.2,.8,.3,1);
    box-shadow: 0 20px 60px rgba(0,0,0,0.35);
    max-width:calc(100vw - 48px);
  }
  .qr-modal-backdrop.open .qr-modal{ transform: scale(1); }

  .qr-modal-close{
    position:absolute;
    top:10px; right:10px;
    width:32px; height:32px;
    border-radius:50%;
    border:none;
    background: var(--ink-tint-07);
    color: var(--ink);
    display:flex; align-items:center; justify-content:center;
    cursor:pointer;
  }
  .qr-modal-close svg{ width:14px; height:14px; fill:none; stroke:currentColor; stroke-width:1.8; }

  .qr-code{
    width:220px; height:220px;
    background: var(--paper);
    border-radius:8px;
    padding:10px;
    display:flex; align-items:center; justify-content:center;
    flex-shrink:0;
    border: 2px solid var(--brass);
    box-shadow: 0 3px 10px rgba(0,0,0,0.25);
    overflow:hidden;
  }
  .qr-code img, .qr-code canvas{ width:100%; height:100%; display:block; }
  .qr-label{
    font-family:'IBM Plex Mono', monospace;
    font-size:11px;
    letter-spacing:0.1em;
    text-transform:uppercase;
    color: var(--ink-soft);
    opacity:0.85;
    white-space:nowrap;
  }

  .social-row{
    display:flex;
    gap:10px;
  }
  /* Social icons sit on the light hero, so they follow the same brand-on-paper
     convention as the QR trigger: a brand outline + brand glyph rather than the
     old dark-hero fill (--paper-08 wash + --paper glyph, both of which vanished
     against the page background once the hero became a light surface). */
  .social-icon{
    width:42px; height:42px;
    border-radius:50%;
    border:1px solid var(--brass-light-50);
    background: transparent;
    display:flex; align-items:center; justify-content:center;
    color: var(--primary-text, var(--brass));
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
  }
  .social-icon:hover{ background: var(--brass-hover-20); border-color: var(--brass); }
  .social-icon svg{ width:19px; height:19px; fill: currentColor; }

  /* ---------- VENDOR HERO (design-template "card" layout) ----------
     The public page header rebuilt as the design-template card: a 4-color
     ribbon flush to the top edge, a top nav with the brand + a CSS-only
     hamburger that reveals a full-card menu overlay, then a centered hero
     (eyebrow, oversized name, meta pills, contact buttons, a filled
     Request-a-Quote CTA, socials, and an availability note). Keeps the themed
     band (--header-bg background, --paper text) so it stays consistent with
     the footer; the class beats the bare `header` rules above on specificity. */
  .vhero{
    position:relative;
    /* Lift the header's stacking context above the sibling page sections
       (which sit at z-index:1 via `.page > *`). Without this the full-page
       menu overlay — nested inside .vhero — can't rise above later sections
       and they paint through it. Stays below the QR modal (z-index:90). */
    z-index:30;
    overflow:hidden;
    padding:24px 24px 26px;
    /* No fill of its own — the hero shares the page background (palette color,
       or the custom photo when one is set) so it reads as part of the page. */
    background: transparent;
    color: var(--ink);
    border-bottom:none;
    text-align:center;
  }

  /* .vhero used to be .page's direct child, so its own z-index:30 (below)
     competed directly against sibling sections' z-index:1 (from `.page > *`
     above) and won — which is how its nested full-card menu overlay could
     rise above later sections. Now .vhero-stack is .page's direct child
     instead, and .vhero's z-index:30 only resolves WITHIN .vhero-stack's
     own stacking context; from .page's perspective .vhero-stack itself only
     carries the generic z-index:1 every `.page > *` gets, tying it with
     every later section — and on a tie the later DOM sibling paints on top,
     which is how the About/etc. section painted straight through the open
     menu overlay. Re-declare the same z-index:30 here, one level up, so
     .vhero-stack wins that fight the way .vhero alone used to. */
  .vhero-stack{ z-index:30; }

  /* Custom header photo, optionally blurred. Mirrors .page-bg but is scoped
     vertically to the hero's own height while breaking out to the full
     viewport width — a sibling of .vhero (not a child: see the comment in
     vendor_page.html), inside .vhero-stack (position:relative via the
     `.page > *` rule above, no overflow:hidden), so it isn't capped by
     .vhero's own reading-width/wide-breakout box or clipped by .vhero's
     overflow:hidden. left:50%+width:100vw+translateX(-50%) is the standard
     full-bleed-inside-a-centered-column technique: it centers on
     .vhero-stack's own center, which — since .vhero-stack has no side
     padding and .page is centered on the viewport — coincides with the
     viewport's center, at any --page-max/--wide-max tier. top/bottom:0
     match .vhero-stack's height, which auto-sizes to .vhero (the only
     normal-flow child) since this element is taken out of flow. The blur
     bleeds past its own edges via a negative-inset ::after so the blurred
     image reaches this element's own edges. */
  .vhero-bg{
    position:absolute; top:0; bottom:0;
    left:50%; width:100vw;
    transform:translateX(-50%);
    z-index:0;
    overflow:hidden;
    pointer-events:none;
  }
  /* The photo itself renders fully visible by default — no flat wash over it
     (an earlier version tinted the whole photo toward --paper for guaranteed
     text contrast, but that made most photos read as a flat tinted haze
     instead of a photo, which defeats the point of uploading one). The
     vendor can still opt into a wash by hand via header_background_overlay
     (--bg-overlay below, 'transparent' unless they turn it on) — the
     difference from the rejected always-on version is that it's off by
     default and vendor-controlled, not something every photo gets whether it
     needs it or not. The bottom edge fades out via a mask, not by painting a
     var(--paper) layer
     on top: a color hand-off only blends while whatever comes after the
     header is exactly flat --paper, and the moment it isn't — a page
     background photo (.page-bg), the header-pattern texture showing through
     on <body> — the flat band reads as a hard seam instead of a fade (the
     original bug report: "it breaks when we add page background"). Masking
     ramps the photo's own alpha to zero, so it dissolves into whatever is
     actually behind it, always — same fix as .hb-surface.glow-on in
     header_bg_layouts.css. */
  .vhero-bg::after{
    content:"";
    position:absolute; inset:0;
    z-index:0;
    background-image: linear-gradient(var(--bg-overlay, transparent), var(--bg-overlay, transparent)), var(--bg-image);
    background-size: cover;
    background-position:center;
    -webkit-mask-image: linear-gradient(to bottom, #000 65%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 65%, transparent 100%);
  }
  /* inset:-24px (below) makes this box (H+48) tall instead of .vhero-bg's H,
     so the visible bottom edge (clipped by .vhero-bg's own overflow:hidden)
     sits at mask-percentage (H+24)/(H+48) — ~93-96% for a normal hero, but
     noticeably short of the 100%-transparent mark for a short/compact one —
     never the fully-transparent tail the base rule's 65%→100% mask reaches
     at an unblurred (un-inset) hero. That gap used to read as a visible seam
     where the softened photo met the next section; reaching full
     transparency by 82% instead of 100% moves that point comfortably before
     every realistic visible-bottom percentage, so the whole visible tail has
     genuinely faded out, with margin, regardless of hero height. */
  .vhero-bg.is-blurred::after{
    inset:-24px;
    filter: blur(var(--bg-blur, 12px));
    -webkit-mask-image: linear-gradient(to bottom, #000 45%, transparent 82%);
    mask-image: linear-gradient(to bottom, #000 45%, transparent 82%);
  }
  /* Same tiled-pattern texture overlay as .page-bg, scoped to the hero photo.
     See the .page-bg::before note above for why washes are excluded. */
  body.hb-dots .vhero-bg::before,
  body.hb-grid .vhero-bg::before,
  body.hb-diagonal .vhero-bg::before,
  body.hb-crosshatch .vhero-bg::before{
    content:"";
    position:absolute; inset:0;
    z-index:1;
    pointer-events:none;
    opacity:.5;
    background-image: var(--hb-pattern, none);
    background-size: var(--hb-pattern-size, auto);
  }
  /* Hero text, forced light. Off by default: the eyebrow/name/bio/wordmark
     render in the theme's normal --he-ink, whether or not there's a header
     photo behind them — legibility is the vendor's own call, via
     header_background_overlay (a wash over a photo) or this toggle (a
     forced light text color), not a guess this CSS makes for them. Not
     gated on a header photo existing: a header background PATTERN
     (header_bg, painted on <body> — see .hb-surface) or even the theme's
     own plain paper can run dark too, and there was never a single "is
     what's behind the hero light or dark" choice that held up for every
     vendor's pick. Eyebrow/name/bio float with no backing of their own, so
     this is a flat color, not a text-shadow: legibility is left entirely to
     the overlay/this toggle instead of a shadow baked on unconditionally.

     Pills are untouched by the toggle: they already draw their own
     background chip (a tint of --he-ink), bumped opaque here instead of
     switched to white text — the same self-contained-contrast trick
     .he-cbtn already relies on, and more robust than a text color choice
     since the chip fixes contrast against whatever is behind it, toggle on
     or off. .he-contact/.he-quote are untouched for the same reason — both
     already paint a real fill behind their label. .he-socials is untouched
     too — its icons are its own glyphs, not colored text, so this toggle
     doesn't apply to it the same way. */
  .vhero.light-hero-text .he-eyebrow,
  .vhero.light-hero-text .he-name,
  .vhero.light-hero-text .he-bio,
  .vhero.light-hero-text .vnav-wordmark{
    color:#fff;
  }
  .vhero-bg + .vhero .he-pill{
    background: rgba(255,255,255,.92);
    color: #1a1a1a;
    box-shadow: 0 1px 4px rgba(0,0,0,.18);
  }
  /* Secondary hero text (bio, availability note) stops being muted once there
     is a photo behind it. A muted ink is a fade toward the PAGE colour, and
     that only reads as "quieter" while the thing behind it IS that flat
     colour — over a photograph the same fade just reads as losing the text,
     and it was the one part of the hero that consistently did. Size and weight
     still carry the hierarchy, so nothing is given up by painting it at full
     strength. Scoped to the sibling selector: a hero with no photo keeps its
     muted ink exactly as before. */
  .vhero-bg + .vhero{ --he-muted: var(--he-ink, var(--ink)); }

  /* Top nav — brand on the left, QR + hamburger on the right */
  .vnav{
    display:flex; align-items:center; justify-content:space-between;
    position:relative; z-index:50; margin-bottom:52px; padding-top:6px;
  }
  .vnav-brand{ padding-top:0; }
  .vnav-wordmark{
    font-family:var(--font-display); font-weight:700; font-size:clamp(17px, 4.2vw, 19px);
    color: var(--ink); line-height:1;
  }
  .vnav-actions{ display:flex; align-items:center; gap:12px; }
  /* QR button sits on the light hero now: brand outline + brand glyph. */
  .vhero .qr-trigger{
    border-color: var(--brass-light-50);
    background: transparent;
    color: var(--primary-text, var(--brass));
  }
  .vhero .qr-trigger:hover{ background: var(--brass-hover-20); }

  /* CSS-only hamburger toggle → menu overlay. The checkbox is visually hidden
     and drives both the bar→X animation and the overlay via `:checked ~`. */
  .vnav-toggle{ position:absolute; opacity:0; pointer-events:none; }
  /* Geometry (width, bar thickness/spacing/radius, alignment, and the X-fold
     travel distance) comes from per-vendor CSS vars set inline on the label
     from agent.menu_icon_geo; the fallbacks reproduce the default icon. Each
     bar's width is set inline too. */
  .hamburger{
    display:inline-flex; flex-direction:column; align-items:var(--ham-align, flex-end);
    justify-content:center; width:var(--ham-w, 20px); height:20px; gap:var(--ham-gap, 4px);
    cursor:pointer; color: var(--primary-text, var(--brass));
  }
  .hamburger span{
    display:block; height:var(--ham-th, 1.5px); width:100%; background:currentColor;
    border-radius:var(--ham-radius, 2px);
    transition: transform .22s ease, opacity .22s ease;
  }
  /* On open, both arms snap to full width so the ✕ is even regardless of the
     style's resting bar widths; !important beats the per-bar inline width. */
  .vnav-toggle:checked ~ .vnav .hamburger span:nth-child(1){ transform: translateY(var(--ham-move, 5.5px)) rotate(45deg); width:100% !important; }
  .vnav-toggle:checked ~ .vnav .hamburger span:nth-child(2){ opacity:0; }
  .vnav-toggle:checked ~ .vnav .hamburger span:nth-child(3){ transform: translateY(calc(-1 * var(--ham-move, 5.5px))) rotate(-45deg); width:100% !important; }

  /* Nav controls, gated by the same .light-hero-text toggle as the wordmark/
     eyebrow/name/bio above — same "no single choice holds up for every
     vendor's background" call as the text, so it stays the vendor's to make
     rather than an automatic one, and isn't limited to when a header photo
     is set either. When it's on: the bars inherit white through
     `background:currentColor`. The QR button's hairline follows its glyph to
     translucent white; left on --brass-light-50 it would be the one
     brand-colored thing in a control that no longer has anything
     brand-colored in it. */
  .vhero.light-hero-text .qr-trigger,
  .vhero.light-hero-text .hamburger{
    color:#fff;
  }
  .vhero.light-hero-text .qr-trigger{
    border-color: rgba(255,255,255,.5);
    background: rgba(255,255,255,.1);
  }
  .vhero.light-hero-text .qr-trigger:hover{ background: rgba(255,255,255,.22); }
  /* ...except while the menu overlay is open. .vnav sits above it (z-index 50
     vs 40), so what's actually behind these controls then is the overlay's
     --paper fill, not whatever's behind the collapsed header — white becomes
     white-on-paper and the ✕ that closes the menu disappears. Put all three
     back on the theme colors for that state. */
  .vhero.light-hero-text .vnav-toggle:checked ~ .vnav .qr-trigger,
  .vhero.light-hero-text .vnav-toggle:checked ~ .vnav .hamburger{
    color: var(--primary-text, var(--brass));
  }
  .vhero.light-hero-text .vnav-toggle:checked ~ .vnav .qr-trigger{
    border-color: var(--brass-light-50);
    background: transparent;
  }
  .vhero.light-hero-text .vnav-toggle:checked ~ .vnav .vnav-wordmark{
    color: var(--ink);
  }

  /* Full-page menu overlay — fixed to the viewport so it covers the entire
     page, not just the header card. The nav stays above it (higher z-index) so
     the hamburger-turned-X can close it. */
  .vnav-overlay{
    position:fixed; inset:0; z-index:40;
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:22px;
    padding:78px 24px 34px;
    background: var(--paper);
    opacity:0; visibility:hidden; transform: translateY(10px);
    transition: opacity .3s ease, transform .3s ease, visibility .3s;
  }
  .vnav-toggle:checked ~ .vnav-overlay{ opacity:1; visibility:visible; transform:none; }
  /* Freeze the page while the menu overlay is open, so the header (logo + the
     hamburger-turned-X) stays put instead of scrolling out from under the
     fixed overlay. :has() lets the CSS-only checkbox reach the scroll root. */
  html:has(.vnav-toggle:checked){ overflow:hidden; }
  /* Same freeze for the QR modal — .qr-modal-backdrop.open has no other
     scroll lock of its own, so the page behind it scrolled freely. */
  html:has(.qr-modal-backdrop.open){ overflow:hidden; }
  .vnav-links{ display:flex; flex-direction:column; align-items:center; gap:14px; }
  .vnav-links a{
    font-family:var(--font-display); font-size:clamp(22px, 7vw, 30px); line-height:1;
    color: var(--ink); transition: opacity .15s ease;
  }
  .vnav-links a:hover{ opacity:0.55; }
  /* Socials pinned to the bottom of the overlay; absolute so the nav links stay
     vertically centered (the overlay's justify-content:center) undisturbed. */
  .vnav-social{
    position:absolute; left:0; right:0; bottom:34px;
    justify-content:center;
  }

  /* Hero body — the size container for the shared hero module (.he-hero), so
     its cqi-based type scales with the hero width. Carries the agent's --he-*
     palette vars inline (hero_he_style); the .he-* layout + he-align-* class come
     from hero_layout_classes. See hero_layouts.css. */
  /* Re-narrows the hero's centered content (name, pills, CTA) back to its
     original design width on desktop, where .vhero itself has stretched wide
     to let the nav bar (logo + hamburger) run full-width above it — see the
     --wide-max breakout above. -48px accounts for .vhero's own left+right
     padding, so this matches the hero's pre-breakout content width exactly.
     A no-op below 1024px, where .vhero is already this width. */
  .vhero-body{ position:relative; z-index:1; container-type: inline-size;
    max-width: calc(var(--page-max) - 48px); margin-inline:auto; }
  /* The module marks its buttons cursor:default (they're inert in the gallery
     specimen); on the live page they're real links, so restore the pointer. */
  .vhero .he-cbtn, .vhero .he-quote, .vhero .he-socials .social-icon{ cursor:pointer; }

  /* Socials centered under the hero */
  .vhero .social-row{ justify-content:center; margin-top:18px; }

  /* ---------- SECTIONS ---------- */
  /* Section backgrounds are set per-section by class (e.g. .network-section),
     not positionally, so an optional section (Links) can be inserted without
     shifting a zebra rule onto the wrong block. */
  section{ padding: var(--sp-section) var(--sp-section-x); }
  /* about_layouts.css collapses .ab-sidebyside / .ab-portrait-right / etc. to
     one column via `@container (max-width:420px)` on the SAME element that
     declares `container-type: inline-size` (.about). Per spec a query
     container must be an ANCESTOR of the element it styles, never the element
     itself, so that self-query silently never matches and the two-column grid
     never collapses on mobile — confirmed live (about's own width was 346px,
     well under 420, and it still rendered two columns). Giving the wrapping
     <section> its own containment makes it the real ancestor container the
     query resolves against; .about keeps its container-type for the cqi units
     used inside .ab-name/.ab-copy/etc., which was never the broken part. */
  .about-section{ container-type: inline-size; }
  /* Gap between the heading and its section content scales with the viewport
     like the type and section padding — a fixed value reads cramped once the
     title grows. Same clamp(min, base + vw, max) form as the --sp-* tokens. */
  .section-head{ margin-bottom: clamp(16px, 12px + 1.15vw, 28px); }
  /* Section headings render through the shared section-title module
     (.stitle/.st-*, in section_title_layouts.css), themed per-agent via
     section_title_style. The module sizes the title in cqi so a lab or picker
     sample scales to its own canvas; on this page that would render the same
     heading level at two sizes, because some sections break out to 1024px while
     others stay in the reading column. Pin it to one viewport-based value so
     every section head matches. Both are the kit's --type-display-m (26→32).
     This used to carry a :not(.st-wide) exception — Wide Set was the one style
     whose look depended on its own larger/tracked title — but that style was
     retired in the 2026-08 cull, so every surviving style takes the pinned size
     and the carve-out is gone. */
  .section-head .st-title{ font-size: var(--fs-h2); line-height:1.18; }
  /* The module's own vertical rhythm was tuned for its old 30-46px title. It
     now uses the kit's --stack-eb-gap / --stack-sh-gap, which are already the
     right size against a 26-32px heading, so the eyebrow/subtitle overrides
     below are no longer a correction — they are just the kit's own gaps
     restated. Kept explicit so the underline/overline padding stays paired. */
  .section-head .st-eyebrow{ margin-bottom: var(--stack-eb-gap); }
  .section-head .st-subtitle{ margin-top: var(--stack-sh-gap); }
  .section-head .st-underline .st-title{ padding-bottom:10px; }
  .section-head .st-overline .st-eyebrow{ padding-top:10px; }

  /* ---------- FOOTER ----------
     The page footer is now the reusable footer module (footer_layouts.css): the
     vendor's chosen ft-* layout on <footer class="site-footer">, themed via the
     --ft-* variables from agent.footer_ft_style. All footer styling (CTA, brand,
     nav, socials, watermark, disclosure) lives in that module, so the old bare
     `footer{}
` / `.cta-btn` / `.watermark` / `.disclosure` rules were removed to
     avoid fighting the module (e.g. its left-aligned layouts vs. text-align). */

  /* ---------- CONTACT FORM ----------
     Old .cf-* widgets removed — superseded by the .cff-* module in
     contact_form_layouts.css (see bio_sections/contact.html). */

/* ---- gallery ----
   The public gallery uses the reusable gallery module (gallery_layouts.css:
   .gallery / .gal-* / the gl-* layout classes + shared lightbox), themed via the
   agent's --gal-* variables — the same discipline as the FAQ / footer / pricing
   sections. The old bespoke .gallery-grid / .gallery-layout-* implementation was
   retired when the module was brought over; see templates/core/bio_sections/
   gallery.html and core/js/agent-bio-gallery.js. */

/* ---- Zulli kit v1.0 · responsive token overrides ----
   These two blocks are the reason the token block above exists. Both were in
   the kit from the start, and both were dead code on this page until now: they
   override tokens, and the tokens were undefined, so the layout modules fell
   through to their hardcoded `var(x, <fallback>)` values at every viewport. */

/* Touch: every control clears a 44px target. Without this, the modules'
   var(--btn-h-md, clamp(40px,4.4vw,44px)) resolved to 40px on a phone (4.4vw of
   390px is 17px, so the clamp floor won) — under the kit's own 44px minimum. */
@media (pointer: coarse){
  :root{
    --btn-h-sm: 38px;
    --btn-h-md: 46px;
    --btn-h-lg: 54px;
  }
}

/* Mobile display clamps. Only display-level type steps down; the fixed UI and
   body steps (h2/h3/h4/body/body-sm/label/data) hold at every width, per the
   kit's handoff block. Consumed today by the modules that read
   --track-display-m; the type steps are here as the reference the section-title
   module and the remaining layouts will read from. */
@media (max-width: 560px){
  :root{
    --type-display-xl: 600 38px/1.08 var(--font-display);
    --type-display-l:  600 31px/1.14 var(--font-display);
    --type-display-m:  600 26px/1.20 var(--font-display);
    --type-h1:         700 24px/1.25 var(--font-body);
  }
}
