﻿/* ============================================================================
   Public site shell — header, footer, sidebar, and page frame.

   Extracted from _PublicLayout's inline <style> so the Craft builder iframe can
   load it too. Without this, layout blocks render unstyled in the canvas: the
   .psite-* rules only existed inside the public page's <head>, which the builder
   never sees.

   The tenant's theme (ThemeCss.StyleBlock) is still injected inline on public
   pages and overrides the :root defaults below, since it loads after this file.
   Those defaults exist so the same markup also renders correctly in the builder,
   where no tenant theme is present.
   ========================================================================== */

:root {
  --site: #4338ca;
  --site-2: #7c3aed;
  --site-radius: 16px;
  --site-radius-btn: 9px;
  --site-font: 'Inter', system-ui, 'Segoe UI', Roboto, sans-serif;
  --site-font-head: 'Inter', system-ui, 'Segoe UI', Roboto, sans-serif;

  /* Shell measure. Every region's inner wrapper reads these two, so the "Width" prop on
     a header / nav bar / top bar / footer only has to re-point the variables (see the
     .psite-w-* classes below) instead of restating a max-width per region. */
  --psite-shell-w: 1160px;
  --psite-shell-pad: 24px;

  /* .pub-main's own width/padding, deliberately separate from --psite-shell-w above rather
     than reusing it: header/footer/nav bar default to 1160px, but page content has always
     defaulted to 1120px, and pub-main sits outside those regions in the DOM (a sibling, not a
     descendant) so there is no inheritance to worry about — only the numbers would collide if
     this shared one variable, quietly widening every existing page by 40px. */
  --pub-w: 1120px;
  --pub-pad-t: 40px;
  --pub-pad-b: 64px;
}

/* A closed .cms-menu-sub / .cms-mega stays invisible via opacity:0;visibility:hidden, not
   display:none, so hover transitions have something to animate — but that also means it's
   still laid out and still counts toward the page's scrollable width even while invisible.
   A top-level nav item positioned toward the right of the row anchors a wide dropdown
   (.cms-menu-sub's own min-width, phase2.css) past the viewport edge long before anyone
   opens it, and nothing before this line stopped that from becoming a real horizontal
   scrollbar. overflow-x:hidden on body only ever discards horizontal scroll that
   shouldn't exist in the first place — every real overflow case already has its own correct
   handling (.cms-sec's overflow-x:clip for full-bleed sections, .cms-mega/.cms-menu-sub's
   own clipping when actually open) — so this is a safety net underneath those, not a
   replacement for any of them. */
/* html, not just body: the root scroller in standards mode is the documentElement, so
   overflow-x:hidden on body alone leaves <html> free to grow scrollWidth from the exact
   same invisible-dropdown case this is guarding against. */
html, body { overflow-x: hidden; }

/* Page content width — the "Content width" option on a page's Layouts modal (Pages screen).
   Same "empty class = today's default" convention as .psite-w-*, so no existing page changes
   until an editor picks one. Has no visible effect on a page with a sidebar: .psite-side-main
   .pub-main below already overrides max-width to fill the content column regardless. */
/* Matches .psite-w-narrow / .psite-w-wide's own pixel values exactly (960px / 1400px) — a page
   set to "Wide" should line up flush with a header or footer also set to "Wide", not just be
   wide in the same general direction. Only "Boxed" (the default, no class) stays at its own
   1120px rather than the shell's 1160px default — that one is never an active choice, so
   changing it would silently widen every page that has not opted into anything. */
.pub-w-narrow { --pub-w: 960px; }
.pub-w-wide   { --pub-w: 1400px; }
.pub-w-full   { --pub-w: none; padding:0 !important; }

/* Page content top/bottom padding — the "Vertical spacing" option next to Content width. */
.pub-pad-none     { --pub-pad-t: 0px;  --pub-pad-b: 0px; }
.pub-pad-compact  { --pub-pad-t: 20px; --pub-pad-b: 32px; }
.pub-pad-spacious { --pub-pad-t: 64px; --pub-pad-b: 96px; }

/* ── Width options ────────────────────────────────────────────────────────────
   The class lands on the OUTER element (.psite-header, .psite-navbar, …) and the
   inner -in wrapper inherits the variables. Full width deliberately keeps — and
   widens — its padding: edge-to-edge should mean the background reaches the
   viewport edge, not that the logo touches it. */
.psite-w-narrow { --psite-shell-w: 960px; }
.psite-w-wide   { --psite-shell-w: 1400px; }
.psite-w-full   { --psite-shell-w: none; --psite-shell-pad: clamp(24px, 4vw, 72px); }

/* ── Legacy layout-block wrappers ────────────────────────────────────────────
   Layouts published before getClean unwrapped the block wrappers carry each region
   inside a bare <div class="">. A body-level wrapper is exactly the height of the
   sticky header, and position:sticky is bounded by its parent — so the header could
   never stick (the classic "sticky stopped working after I re-saved the header
   layout"). display:contents removes the wrapper's box without touching the stored
   markup; newly saved layouts have no wrapper at all. */
body > div:has(> .psite-header),
body > div:has(> .psite-topbar),
body > div:has(> .psite-navbar),
body > div:has(> .psite-footer) { display: contents; }

/* ── Preview ribbon (admin preview of a draft) ───────────────────────────── */
.psite-ribbon {
  background: #111827; color: #fff; font-size: 12.5px; text-align: center;
  padding: 7px 16px; display: flex; align-items: center; justify-content: center; gap: 8px;
}
.psite-ribbon i { color: #fbbf24; }

/* ── Header ─────────────────────────────────────────────────────────────── */
.psite-header {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--color-surface) 92%, transparent);
  backdrop-filter: saturate(180%) blur(8px); border-bottom: 1px solid var(--color-border-subtle);
}
.psite-header-in {
  max-width: var(--psite-shell-w); margin: 0 auto;
  padding: 0 var(--psite-shell-pad); height: 100px;
  display: flex; align-items: center; gap: 28px;
  /* Anchors the mega panel — see the .cms-mega note in phase2.css. */
  position: relative;
}
.psite-brand {
  display: inline-flex; align-items: center; gap: 10px; text-decoration: none;
  /* var(--color-heading), not the admin-only --ink this used to read (admin.css happens to load
     on public pages too, for unrelated utilities) — --ink never flips with dark mode, --color-
     heading does. */
  color: var(--color-heading); font-weight: 700; font-size: 18px; font-family: var(--site-font-head);
}
.psite-brand img { height: 90px; max-width: 168px; object-fit: contain; }
/* Logo-or-name fallback, done with selectors rather than an onerror handler.
   The handler used to run in the builder canvas too — where {{sitelogourl}} is still an
   unresolved token, so the image ALWAYS failed — and it wrote inline styles that
   getClean() then published, permanently hiding the logo on the live site. Matching the
   src attribute reaches the same two states without ever mutating the DOM:
     src=""      → Settings has no logo
     src="{{…}}" → the builder canvas, where site tokens are not resolved
   In both cases the image has nothing to show and the site name takes its place. */
.psite-brand-name { display: none; }
.psite-brand img[src=""],
.psite-brand img[src^="{{"] { display: none; }
.psite-brand img[src=""] ~ .psite-brand-name,
.psite-brand img[src^="{{"] ~ .psite-brand-name { display: inline; }

/* ── Brand text: site name + optional subtitle (showTitle/showSub/brandSubtitle) ──
   The lockup nests both lines in a .psite-brand-text column; the toggles are classes on
   the .psite-brand anchor. Defaults keep the long-standing behaviour: the name appears
   only when there is no logo (the img[src] fallback), everything else stays hidden. The
   flat sibling selectors above remain for markup published before the nested shape. */
.psite-brand-text { display: none; flex-direction: column; align-items: flex-start; line-height: 1.2; }
.psite-show-title .psite-brand-text,
.psite-show-sub .psite-brand-text,
.psite-brand img[src=""] ~ .psite-brand-text,
.psite-brand img[src^="{{"] ~ .psite-brand-text { display: flex; }
.psite-show-title .psite-brand-name,
.psite-brand img[src=""] ~ .psite-brand-text .psite-brand-name,
.psite-brand img[src^="{{"] ~ .psite-brand-text .psite-brand-name { display: inline; }
.psite-brand-sub { display: none; font-size: 12.5px; font-weight: 500; opacity: .75; }
.psite-show-sub .psite-brand-sub { display: block; }
.psite-brand-sub:empty { display: none !important; }
.psite-nav { display: flex; align-items: center; gap: 22px; margin-left: auto; }
.psite-nav a {
  text-decoration: none; color: var(--color-text-muted); font-size: 16px; font-weight: 600;
  padding: 6px 2px; border-bottom: 2px solid transparent; transition: color .14s, border-color .14s;
}
.psite-nav a:hover { color: var(--site); }
.psite-nav a.on { color: var(--site); border-bottom-color: var(--site); }
.psite-cta {
  background: var(--site); color: var(--color-on-accent) !important; border: none;
  border-radius: var(--site-radius-btn, 9px); padding: 9px 18px !important;
  font-weight: 600; border-bottom: none !important;
}
.psite-cta:hover { filter: brightness(.94); color: var(--color-on-accent) !important; }
/* The header CTA is optional. Craft substitutes an empty string when the block has no label,
   so :empty hides the anchor — this is what lets one template serve headers with and without a
   button, without the template needing conditionals it cannot express. */
.psite-cta:empty { display: none; }

/* ── Header variants ────────────────────────────────────────────────────────
   Each keeps the outer .psite-header class (site-nav.js's shellFor() looks for
   `.closest('.psite-navbar, .psite-header')`) and reuses .psite-nav / .psite-burger
   verbatim, so the @media collapse and drawer rules above apply with no changes here —
   only the desktop structure differs per variant. */

/* Centered logo: row 1 is brand + actions, row 2 is the nav on its own row below a
   divider. A spacer balances the actions group so flex centres the brand accurately
   regardless of the CTA's width. Row 2 is simply hidden under the header breakpoint —
   the drawer (a plain descendant of .psite-header) opens under row 1 either way. */
.psite-header-in-centered { justify-content: space-between; }
.psite-header-spacer { flex: 1 1 0; }
.psite-header-actions { flex: 1 1 0; display: flex; justify-content: flex-end; align-items: center; gap: 14px; }
.psite-header-navrow { border-top: 1px solid var(--color-border-subtle); }
.psite-header-navrow .psite-nav {
  justify-content: center; margin-left: 0; gap: 26px;
  max-width: var(--psite-shell-w); margin-inline: auto;
  padding: 10px var(--psite-shell-pad); min-height: 46px;
}
@media (max-width: 720px) {
  .psite-header-navrow { display: none; }
  /* .psite-nav becomes position:absolute when the drawer opens (see the generic nav-open
     rule below), but that means nothing while its own parent is display:none — a hidden
     ancestor clips an absolutely-positioned descendant just as it would a static one. */
  .psite-header.nav-open .psite-header-navrow { display: block; border-top: none; }
}

/* Utility bar + nav: a slim strip above the ordinary brand/nav/CTA row. Reuses
   .psite-topbar-link (mailto:/tel: empty-hide included) and the .psite-topbar-dark /
   -light / -brand tone classes from the Top Bar block below, unmodified. */
.psite-header-utilbar { font-size: 12.5px; }
.psite-header-utilbar-in {
  max-width: var(--psite-shell-w); margin: 0 auto; padding: 6px var(--psite-shell-pad);
  display: flex; gap: 18px; flex-wrap: wrap;
}
@media (max-width: 720px) { .psite-header-utilbar { display: none; } }

/* Minimal: no CTA in the markup, a shorter row, and an optional transparent tone for
   sitting over a hero image (the home page is responsible for enough contrast under it). */
.psite-header-minimal .psite-header-in { height: 72px; }
.psite-header-minimal.psite-header-transparent {
  background: transparent; backdrop-filter: none; border-bottom-color: transparent;
}
.psite-header-minimal.psite-header-transparent .psite-nav a,
.psite-header-minimal.psite-header-transparent .psite-brand,
.psite-header-minimal.psite-header-transparent .psite-burger { color: #fff; }

/* ── Top bar ──────────────────────────────────────────────────────────────
   Drops in above the header. Each link disappears when its target is blank —
   an unset prop leaves href="mailto:" / href="tel:" / href="", and these
   attribute selectors catch exactly those. Same trick as :empty on the CTA,
   applied where the element has children and so is never truly empty. */
.psite-topbar { font-size: 13px; border-bottom: 1px solid var(--color-border-subtle); }
.psite-topbar-in {
  max-width: var(--psite-shell-w); margin: 0 auto; padding: 7px var(--psite-shell-pad);
  display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
}
.psite-topbar-left, .psite-topbar-right { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.psite-topbar-link { display: inline-flex; align-items: center; gap: 7px; text-decoration: none; color: inherit; opacity: .9; }
.psite-topbar-link:hover { opacity: 1; text-decoration: underline; }
.psite-topbar-msg:empty { display: none; }
.psite-topbar-link[href="mailto:"], .psite-topbar-link[href="tel:"] { display: none; }

/* The links row: fixed slots on the block, each an icon + text + URL, laid out
   horizontally and pushed to the right of the bar. */
.psite-topbar-right { margin-left: auto; justify-content: flex-end; }
.psite-tblink {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px; font-weight: 500; color: inherit; opacity: .85;
  text-decoration: none; white-space: nowrap;
}
.psite-tblink:hover { opacity: 1; text-decoration: underline; color: inherit; }
.psite-tblink i { font-size: 14.5px; line-height: 1; }
/* An unused slot leaves href="" — that is the whole "leave empty to hide" mechanism. */
.psite-tblink[href=""] { display: none; }
/* No icon chosen leaves class="bi " with no bi-* glyph, which would still occupy the gap. */
.psite-tblink i:not([class*="bi-"]) { display: none; }
/* Text is optional: an icon-only link should not carry an empty span's spacing. */
.psite-tblink span:empty { display: none; }

.psite-topbar-dark  { background: #111827; color: #e5e7eb; }
.psite-topbar-light { background: #f6f7f9; color: #4b5563; }
.psite-topbar-brand { background: var(--site); color: var(--color-on-accent); }
.psite-topbar-brand .psite-topbar-link, .psite-topbar-brand .psite-social { color: var(--color-on-accent); }

/* Below the header breakpoint the bar centres rather than splitting to the edges;
   the phone/email text is dropped so the icons alone carry it. */
@media (max-width: 720px) {
  .psite-topbar-in { justify-content: center; gap: 10px 16px; padding: 6px 16px; }
  .psite-topbar-right { margin-left: 0; justify-content: center; }
  .psite-topbar-link span, .psite-tblink span { display: none; }
}

/* Brand lockup as its own block, for headers composed from Columns. */
.psite-brand.psite-brand-lg img { height: 80px; max-width: 220px; }
.psite-brand.psite-brand-sm img { height: 60px; max-width: 140px; }
/* Opt out of the sticky default (see ShellBlocks' "sticky" prop). */
.psite-header.psite-unstick { position: static; }
.psite-burger { display: none; margin-left: auto; background: none; border: none; font-size: 32px; color: var(--color-text); }
/* Set by site-nav.js while a mobile drawer is open (.cms-nav-locked). The drawer is its own
   scroll container (max-height + overflow-y below), but nothing stopped the page BEHIND it
   from also scrolling — a stray page scroll shifts the sticky header the drawer is anchored
   to, which can push the drawer's own top portion out of view independently of its internal
   scroll position. Locking the page is the standard fix for a mobile drawer/modal. */
.cms-nav-locked { overflow: hidden; }

/* ── Content ────────────────────────────────────────────────────────────── */
.psite-main { flex: 1 0 auto; width: 100%; }
.pub-main { max-width: var(--pub-w); margin: 0 auto; padding: var(--pub-pad-t) 24px var(--pub-pad-b); }

/* Sidebar layout (Layouts module) — a managed sidebar splits the content area. */
.psite-with-side {
  max-width: 1160px; margin: 0 auto; padding: 0 24px;
  display: flex; gap: 34px; align-items: flex-start;
}
.psite-with-side.side-right { flex-direction: row-reverse; }
.psite-side { flex: 0 0 auto; width: 25%; min-width: 220px; padding: 32px 0; }
.psite-side-main { flex: 1 1 auto; min-width: 0; }
.psite-side-main .pub-main { max-width: none; padding-left: 0; padding-right: 0; }
/* Bootstrap col-* classes carry their own widths inside a grid; here the flex basis above
   governs, so neutralise the padding they'd otherwise add. */
.psite-side[class*="col-"] { padding-left: 0; padding-right: 0; }
/* Width overrides are DESKTOP-only, and the media query is what makes that true rather
   than a comment. `.psite-side.col-lg-4` is two classes; the stacking rule below is one
   (`.psite-side`), so without this bound the narrow rule outranked it at every width and a
   sidebar with a col-lg-* class stayed 33% wide on a phone — the column never went
   full-width no matter how narrow the screen. Source order cannot fix that; specificity
   wins over it. */
@media (min-width: 901px) {
  .psite-side.col-lg-2 { width: 17%; }
  .psite-side.col-lg-4 { width: 33%; }
}

/* ── Footer ─────────────────────────────────────────────────────────────── */
.psite-footer { flex-shrink: 0; background: var(--brand-secondary-900); color: #cbd2dd; }
.psite-footer-in {
  max-width: var(--psite-shell-w); margin: 0 auto; padding: 44px var(--psite-shell-pad) 20px;
  display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 30px;
}
.psite-foot-brand {
  font-family: var(--site-font-head); font-weight: 700; font-size: 18px;
  color: #fff; margin-bottom: 10px;
}
.psite-foot-brand .swatch {
  display: inline-block; width: 12px; height: 12px; border-radius: 3px;
  background: linear-gradient(135deg, var(--site), var(--site-2));
  margin-right: 8px; vertical-align: middle;
}
.psite-footer p { font-size: 13.5px; line-height: 1.6; color: #9aa4b2; margin: 0; max-width: 320px; }
.psite-foot-col h4 {
   font-size: 16px; letter-spacing: .06em; font-weight: 700;
   margin: 0 0 12px; padding: 0; color: var(--brand-primary-100);
}
.psite-foot-col a, .psite-foot-col div {
  display: block; color: var(--brand-primary-100); text-decoration: none; font-size: 14px; padding: 4px 0;
}
.psite-foot-col a:hover { color: #fff; }
/* Optional, like the header CTA — an empty note collapses instead of leaving a gap. */
.psite-foot-note:empty { display: none; }
.psite-foot-bottom {
  max-width: var(--psite-shell-w); margin: 0 auto; padding: 16px var(--psite-shell-pad);
  border-top: 1px solid rgba(255,255,255,.08);
  font-size: 12.5px;  color: var(--brand-secondary-100);
  display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px;
}

/* ── Footer variants ──────────────────────────────────────────────────────── */

/* Minimal: one row — brand, an inline menu (the .cms-menu default is already a
   horizontal flex row; only .cms-menu-vertical elsewhere overrides that to a column,
   so this needs no extra layout rule) and the copyright line. */
.psite-footer-minimal-in {
  max-width: var(--psite-shell-w); margin: 0 auto; padding: 26px var(--psite-shell-pad);
  display: flex; align-items: center; justify-content: space-between; gap: 18px 28px; flex-wrap: wrap;
}
.psite-foot-minimal-menu .cms-menu-link, .psite-foot-minimal-menu .cms-menu-label {
  color: #cbd2dd; font-size: 13.5px;
}
.psite-foot-minimal-menu .cms-menu-link:hover { color: #fff; }
.psite-foot-copy { font-size: 12.5px; color: #7d8798; }
@media (max-width: 720px) {
  .psite-footer-minimal-in { flex-direction: column; text-align: center; }
}

/* Newsletter CTA: a brand-gradient band above the standard managed footer — everything
   from .psite-footer-in down is the unmodified FooterTemplate markup, so it keeps that
   styling as-is; only the band above it is new. */
.psite-foot-news-band { background: linear-gradient(135deg, var(--site), var(--site-2)); }
.psite-foot-news-in {
  max-width: var(--psite-shell-w); margin: 0 auto; padding: 32px var(--psite-shell-pad);
  display: flex; align-items: center; justify-content: space-between; gap: 20px 28px; flex-wrap: wrap;
  color: #fff;
}
.psite-foot-news-in h4 { margin: 0 0 4px; font-weight: 700; }
.psite-foot-news-in p { margin: 0; opacity: .85; font-size: 13.5px; max-width: 360px; }
.psite-foot-news-form { display: flex; gap: 8px; flex-wrap: wrap; }
.psite-foot-news-form input {
  border: none; border-radius: var(--site-radius-btn, 9px); padding: 10px 14px;
  min-width: 220px; font-size: 14px;
}
.psite-foot-news-form button {
  border: none; border-radius: var(--site-radius-btn, 9px); padding: 10px 18px;
  background: #111827; color: #fff; font-weight: 600;
}
@media (max-width: 720px) {
  .psite-foot-news-in { flex-direction: column; align-items: stretch; text-align: center; }
  .psite-foot-news-form { justify-content: center; }
}

/* 4 column: the same managed-footer grid with one more column. Restated at both steps
   because the generic 1-column rule at 720px (below) has lower specificity than this —
   without a matching override here it would win at 900px but lose under 720px, leaving
   the layout stuck at 2 columns all the way down instead of finishing the collapse. */
.psite-footer-in.psite-footer-4col { grid-template-columns: 1.2fr 1fr 1fr 1fr; }
@media (max-width: 900px) {
  .psite-footer-in.psite-footer-4col { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 720px) {
  .psite-footer-in.psite-footer-4col { grid-template-columns: 1fr; }
}

/* ── Responsive ─────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
  /* column-reverse, not column: _PublicLayout.cshtml always emits <aside class="psite-side">
     before the content div — that's what row/row-reverse repositions for the desktop
     left/right split — so plain `column` would stack in DOM order (sidebar on top,
     content below) for BOTH positions. Reversed, content shows first and the sidebar
     (left or right) settles to the bottom either way. */
  .psite-with-side, .psite-with-side.side-right { flex-direction: column-reverse; }
  .psite-side { width: 100%; min-width: 0; padding-bottom: 0; }
}
@media (max-width: 720px) {
  .psite-nav { display: none; }
  .psite-burger { display: block; }
  /* The 90px desktop logo leaves little width for the brand name/subtitle next to it on a
     narrow screen — shrinking it here gives that text more room before wrapping, same goal
     as the brand name/subtitle sizing itself. */
  .psite-brand img { height: 48px; max-width: 110px; }
  .psite-footer-in { grid-template-columns: 1fr; gap: 22px; }
  /* Burger-opened mobile menu: nav drops down below the header. position:absolute takes it
     out of flow, so the page itself never grows to fit it and there was nothing to scroll —
     a deep tree (several expanded has-children branches, or a flattened mega panel) could
     extend past the bottom of the screen with no way to reach the rest of it. max-height +
     overflow-y makes the drawer its own scroll container instead; 80vh rather than a fixed
     offset from the header's own height, since that varies by header variant (plain/
     centered/utility/minimal) and this doesn't need to know which one is active. */
  .psite-header.nav-open .psite-nav {
    display: flex; position: absolute; top: 100%; left: 0; right: 0; margin: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--color-surface); border-bottom: 1px solid var(--color-border);
    box-shadow: 0 14px 26px rgba(0,0,0,.09); padding: 6px 24px 14px;
    max-height: 80vh; overflow-y: auto;
  }
  .psite-header.nav-open .psite-nav a { padding: 12px 2px; border-bottom: 1px solid var(--color-border-subtle); }
  .psite-header.nav-open .psite-nav a.on { color: var(--site); }
  .psite-header.nav-open .psite-cta { margin-top: 10px; text-align: center; }
}

/* ── Map block ───────────────────────────────────────────────────────────────
   Dropped from the page builder (see MapsApiController). Lives here rather than in
   the builder's own stylesheet because the same markup has to look identical in the
   canvas, in preview and on the published site — site-shell.css is loaded by all
   three. Caption and link collapse when their prop is unset, which is how one
   template covers "map only" and "map with a caption and a button". */
.cms-map { margin: 0; }
.cms-map-frame {
  position: relative; width: 100%; overflow: hidden;
  background: var(--color-bg-subtle, #f1f3f5);
  border: 1px solid var(--color-border-subtle, rgba(17, 24, 39, .1));
}
.cms-map-frame.cms-map-rounded { border-radius: var(--site-radius, 14px); }
.cms-map-frame iframe { display: block; width: 100%; height: 100%; border: 0; }
.cms-map-foot {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
   font-size: 14px;
}
.cms-map-cap { color: var(--color-text-muted, #6b7280); }
.cms-map-cap:empty { display: none; }
.cms-map-link {
  margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
  color: var(--site, #2563eb); font-weight: 600; text-decoration: none;
}
.cms-map-link:hover { text-decoration: underline; }
/* No label, or no target: either way there is nothing useful to click. */
.cms-map-link:empty, .cms-map-link[href=""] { display: none; }

/* ── Region colour schemes (shell blocks' bgColor / textColor / bgImage props) ──
   The block template writes the scheme inline on the region root; background and the
   image work by normal cascade (inline beats the tone/default classes). Text is the
   hard part: menu links, headings and footer text hold DIRECT colours that beat
   inheritance, so they are re-pointed at `inherit` — but only when the root actually
   carries a colour declaration. [style*="color"] is that switch: the substring appears
   only in the `color:` declaration (backgrounds use the `background` shorthand), and
   empty props never reach the published attribute (the sanitizer drops their invalid
   declarations; the builder canvas strips them to match — see craft-integration.js).
   Header rules stop at the TOP level of the menu: dropdown and mega-panel links sit on
   their own white panels and must keep their own colours. */
.psite-header[style*="color"] .psite-brand,
.psite-header[style*="color"] .psite-nav > a,
.psite-header[style*="color"] .cms-menu > .cms-menu-item > .cms-menu-link,
.psite-header[style*="color"] .cms-menu > .cms-menu-item > .cms-menu-label,
.psite-navbar[style*="color"] .psite-brand,
.psite-navbar[style*="color"] .cms-menu > .cms-menu-item > .cms-menu-link,
.psite-navbar[style*="color"] .cms-menu > .cms-menu-item > .cms-menu-label,
.psite-footer[style*="color"] p,
.psite-footer[style*="color"] .psite-foot-brand,
.psite-footer[style*="color"] .psite-foot-col h4,
.psite-footer[style*="color"] .psite-foot-col a,
.psite-footer[style*="color"] .psite-foot-col div,
.psite-footer[style*="color"] .psite-foot-bottom,
.psite-footer[style*="color"] .cms-menu-link,
.psite-footer[style*="color"] .cms-menu-label,
/* Brand tone paints the left-hand email/phone links white outright (see
   .psite-topbar-brand below), which beat inheritance and left a chosen text colour
   applying to the right-hand link slots only — those carry `color: inherit` and were
   already following it. The utility header reuses those same tone classes but puts
   them on its inner bar rather than the region root, so the tone's own colour lands
   on a DESCENDANT of the styled element and beats inheritance for every tone, not
   just brand — hence the bar itself is re-pointed here as well as the links. */
.psite-topbar[style*="color"] .psite-topbar-link,
.psite-header[style*="color"] .psite-header-utilbar,
.psite-header[style*="color"] .psite-topbar-link,
.psite-side-inner[style*="color"] h4,
.psite-side-inner[style*="color"] .cms-menu-link,
.psite-side-inner[style*="color"] .cms-menu-label {
  color: inherit;
}

/* ── Shell background-image layer (the blocks' bgImage / bgImageFade props) ──
   CSS cannot fade a background-image, so the image rides this absolutely-positioned
   child (same layered approach as the hero's background video) and the fade classes on
   the region root dial its opacity — the region's background COLOUR shows through. The
   content wrappers get position:relative so they paint above the layer; the header is
   already positioned (sticky), the other regions are anchored here. An empty bgImage
   leaves an invisible, pointer-transparent div — harmless in both canvas and page. */
.psite-footer, .psite-topbar, .psite-side-inner, .psite-navbar { position: relative; }
.psite-bg-img {
  position: absolute; inset: 0; pointer-events: none;
  background-size: cover; background-position: center;
}
.psite-header-in, .psite-header-navrow, .psite-header-utilbar,
.psite-footer-in, .psite-footer-minimal-in, .psite-foot-news-band, .psite-foot-bottom,
.psite-topbar-in, .psite-navbar-in,
.psite-side-inner > .psite-side-block, .psite-side-inner > .psite-side-card { position: relative; }
/* The full 5%-step opacity ladder the builder's slider maps onto (class-based because
   the value must survive the sanitizer). No class = 100%. The original 75/50/25 select
   values are points on this same ladder, so older layouts read back unchanged. */
.psite-bgimg-0  .psite-bg-img { opacity: 0; }
.psite-bgimg-5  .psite-bg-img { opacity: .05; }
.psite-bgimg-10 .psite-bg-img { opacity: .1; }
.psite-bgimg-15 .psite-bg-img { opacity: .15; }
.psite-bgimg-20 .psite-bg-img { opacity: .2; }
.psite-bgimg-25 .psite-bg-img { opacity: .25; }
.psite-bgimg-30 .psite-bg-img { opacity: .3; }
.psite-bgimg-35 .psite-bg-img { opacity: .35; }
.psite-bgimg-40 .psite-bg-img { opacity: .4; }
.psite-bgimg-45 .psite-bg-img { opacity: .45; }
.psite-bgimg-50 .psite-bg-img { opacity: .5; }
.psite-bgimg-55 .psite-bg-img { opacity: .55; }
.psite-bgimg-60 .psite-bg-img { opacity: .6; }
.psite-bgimg-65 .psite-bg-img { opacity: .65; }
.psite-bgimg-70 .psite-bg-img { opacity: .7; }
.psite-bgimg-75 .psite-bg-img { opacity: .75; }
.psite-bgimg-80 .psite-bg-img { opacity: .8; }
.psite-bgimg-85 .psite-bg-img { opacity: .85; }
.psite-bgimg-90 .psite-bg-img { opacity: .9; }
.psite-bgimg-95 .psite-bg-img { opacity: .95; }

/* Columns section wrapper: flow-root stops the section's own margins collapsing with the
   Bootstrap row's negative gutter margins inside it — without this, a margin/padding on an
   UNPAINTED section mostly vanished (a painted one worked only because the automatic p-4
   happened to block the collapse). */
.cms-sec { display: flow-root; }

/* ── Section background image (the columns block's bgImage/bgParallax/bgImgOpacity) ──
   Same layered approach as the shell regions: CSS cannot fade a background-image, so
   the image rides this absolutely-positioned child and the opacity slider dials it —
   the section's background colour shows through. cms-parallax pins the image to the
   viewport so it holds still while the page scrolls past (browsers on iOS ignore
   background-attachment:fixed and simply scroll it normally — a graceful fallback).
   The container is position:relative so content always paints above the layer. */
.cms-sec { position: relative; }
.cms-sec-bg {
  position: absolute; inset: 0; pointer-events: none;
  background-size: cover; background-position: center;
}
.cms-sec-bg.cms-parallax { background-attachment: fixed; }
.cms-sec-inner { position: relative; }

/* ── Decorative background overlay pattern (Section's secOverlay / Hero's heroOverlay) ──
   bg-overlays.css supplies the `.bg-{slug}` classes (docs/bg's two SVG packs); this rule
   is only the positioning contract, shared by both blocks. It rides its own absolutely
   positioned layer for the same reason bgImage does — opacity fades a layer, not a
   background-image — and paints between the base colour and the real content.
   Section: appended to .cms-sec before .cms-sec-inner, so the existing bgImg-then-inner
   DOM-order convention (see applyColumnsWrap) already puts it under the container with no
   extra rule needed. Hero has no such convention (its root previously had no absolutely
   positioned children at all outside the bgVideo path), so cms-hero-ov-host supplies one:
   promote every other direct child to position:relative so it paints above this layer
   regardless of DOM order, the same trick .cms-bg-video-host uses for the video layer. */
.cms-ov-pattern {
  position: absolute; inset: 0; pointer-events: none;
  background-size: cover; background-position: center;
}
.cms-hero-ov-host { position: relative; }
.cms-hero-ov-host > *:not(.cms-ov-pattern) { position: relative; z-index: 1; }

/* A row's negative gutter margins are meant to be absorbed by container padding; on a
   Full-width page (pub-w-full, zero page padding) with an unpadded container they poke
   past the viewport instead and put a horizontal scrollbar on the whole page. clip, NOT
   hidden: it clips without creating a scroll container, so it doesn't trigger the
   overflow-axis coupling (see the CSS gotchas in CLAUDE.md) and changes no layout. */
.cms-sec { overflow-x: clip; }

/* The section is the full-width band and never rounds — the Corner radius prop belongs to
   the container. Pages published while a painted section still received an automatic
   rounded-3 carry that class in their static HTML; the extra .rounded-3 specificity (plus
   !important, which the utility itself uses) squares those off without blocking a
   deliberate rounded-* utility added via Extra classes alone. */
.cms-sec.rounded-3 { border-radius: 0 !important; }

/* ── Columns section: content width (the block's Content width prop) ─────────
   The wrapper <section> spans the full block width so its background/video stays
   full-bleed; this inner box caps and centres the row. Values match the public
   shell's Narrow/Boxed/Wide so capped sections line up with headers and footers. */
.cms-sec-inner { width: 100%; margin-left: auto; margin-right: auto; }
.cms-sec-w-narrow { max-width: 960px; }
.cms-sec-w-boxed { max-width: 1120px; }
.cms-sec-w-wide { max-width: 1400px; }

/* A width-capped container always keeps a side gutter, the way Bootstrap's .container
   does — below its max-width the cap doesn't bite, and on a Full-width page (pub-w-full,
   zero page padding) unpainted content would otherwise run flush to the viewport edge.
   Deliberately overridable: the p-* presets are !important and Custom padding is inline,
   so both still win, and an explicit p-0/px-0 still zeroes it. */
.cms-sec-inner[class*="cms-sec-w-"] { padding-left: 1rem; padding-right: 1rem; }

/* ── Stats Row, icon-left mode (Craft's native "stats" block, index.html) ───────
   The icon-left layout wraps the icon+value+label in a plain `d-flex align-items-center`
   row with no horizontal centering of its own — align-items only centers the row's
   CONTENTS vertically, not the row itself within the card, so on anything wider than a
   narrow column the group sits flush against the card's left edge instead of centered as
   a block, which is what the icon-top layout (text-center on the card) gives. Fixed here
   rather than in index.html, which is vendored and not edited directly (CLAUDE.md) — the
   icon's own fs-1 + me-3 combination only appears in this one block, so the selector can't
   catch any other component's d-flex row by accident. Loaded by both the builder canvas
   and the public site, so the fix applies to the canvas preview and to what's published. */
.d-flex.align-items-center:has(> i.fs-1.me-3) { justify-content: center; }

/* ── Module cards (the page builder's per-module blocks, ModulesApiController) ──
   One card style serves every module: icon chip, heading, blurb, action buttons.
   Buttons follow the tenant colour, and an empty label (or empty href) collapses its
   button — same conditional-free trick as the shell CTAs, since the mustache pass
   cannot express an "if". */
.cms-modcard {
  border: 1px solid #e5e7eb; border-radius: 16px; padding: 30px 28px;
  background: #fff; max-width: 720px; margin: 0 auto; text-align: center;
}
.cms-modcard-ico {
  width: 54px; height: 54px; border-radius: 14px;
  background: color-mix(in srgb, var(--site) 12%, transparent); color: var(--site);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 26px; margin-bottom: 12px;
}
.cms-modcard h3 { font-weight: 700; margin: 0 0 6px; font-family: var(--site-font-head); }
.cms-modcard p { color: #64748b; margin: 0 0 18px; font-size: 14.5px; line-height: 1.6; }
.cms-modcard-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.cms-modcard-btn {
  background: var(--site); color: #fff !important; text-decoration: none;
  border-radius: var(--site-radius-btn, 9px); padding: 10px 20px;
  font-weight: 600; font-size: 14px; display: inline-flex; align-items: center;
}
.cms-modcard-btn:hover { filter: brightness(.94); }
.cms-modcard-btn.ghost { background: #fff; color: var(--site) !important; border: 1.5px solid var(--site); }
.cms-modcard-btn:empty, .cms-modcard-btn[href=""], .cms-modcard-btn[href="mailto:"] { display: none; }

/* ── Multi-step forms (FormRenderer's step markers + form-stepper.js) ────────
   Loaded here rather than only on public pages because standalone form pages and the
   admin preview also need it — see the site-shell.css link added to those two views.
   Buttons deliberately use bare Bootstrap classes (.btn-primary/.btn-outline-secondary)
   rather than a bespoke style: bs5-overrides.css and ThemeCss already re-point
   --bs-primary at the tenant colour, so Next/Back follow the brand for free. */
.cms-step-indicator { display: flex; align-items: center; gap: 8px; margin-bottom: 22px; flex-wrap: wrap; }
.cms-step-dot { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #94a3b8; white-space: nowrap; }
.cms-step-dot .n {
  width: 26px; height: 26px; border-radius: 50%; background: #e5e7eb; color: #6b7280;
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 12.5px; flex-shrink: 0; transition: background .15s, color .15s;
}
.cms-step-dot.done .n { background: var(--site); color: #fff; }
.cms-step-dot.active .n { background: var(--site); color: #fff; box-shadow: 0 0 0 3px color-mix(in srgb, var(--site) 25%, transparent); }
.cms-step-dot.active { color: var(--ink, #111827); font-weight: 600; }
.cms-step-sep { flex: 1 1 16px; height: 2px; background: #e5e7eb; min-width: 16px; }
.cms-step-progress { height: 6px; border-radius: 99px; background: #e5e7eb; overflow: hidden; margin-bottom: 22px; }
.cms-step-progress-fill { height: 100%; background: var(--site); transition: width .25s; }
.cms-step-title { font-size: 17px; font-weight: 700; margin-bottom: 4px; font-family: var(--site-font-head); }
.cms-step-desc { font-size: 13.5px; color: #6b7280; margin-bottom: 16px; }
.cms-step-nav { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-top: 22px; }

/* ── List items with icons (applyListIcons in craft-integration.js) ──────────
   The icon and the .cms-li-txt span are separate flex items, so a wrapped line stays
   aligned with the text edge instead of running under the icon — the icon acts as the
   marker. When the item links, the <a> wraps the pair and becomes the flex row. Plain
   <ol> items never get .cms-li-ico: flex display would swallow their numbering. */
.cms-li-ico { display: flex; align-items: flex-start; }
.cms-li-ico > a { display: flex; align-items: flex-start; flex: 1 1 auto; min-width: 0; }
.cms-li-ico .bi { flex-shrink: 0; }
.cms-li-txt { min-width: 0; }

/* ── Announcements ticker (AnnouncementsRenderer + the cms-announcements block) ──
   A pure-CSS marquee: the viewport holds TWO identical tracks, and translating the
   pair by exactly -50% loops seamlessly. The renderer's markup is direction- and
   style-neutral; the builder block's WRAPPER carries the orientation class
   (cms-annc-h / cms-annc-v) and the colour/size custom properties, because the
   marker resolves server-side with no view of block props. Pauses on hover, and
   stands still for prefers-reduced-motion. */
.cms-annc {
  /* The wrapper owns the painted box: its inline background/color/font-size (the block's
     props) override these defaults, and the ticker inside inherits the text styling. */
  background: #fff; color: #1f2937; font-size: 14.5px;
  border: 1px solid rgba(17, 24, 39, .08); border-radius: 12px;
  overflow: hidden;
}
.cms-ticker { overflow: hidden; position: relative; }
/* Horizontal is the default shape, so the ticker also renders sensibly anywhere the
   wrapper is absent (the manager's preview pane supplies cms-annc-h explicitly). */
.cms-ticker-viewport {
  display: flex; width: max-content;
  animation: cms-ticker-x var(--cms-ticker-dur, 24s) linear infinite;
}
/* Seam geometry: the -50% translation is exactly "one track plus its margin", so the
   spacing between the two track copies must equal the gap between items — that margin,
   not padding, is what makes the wrap-around invisible in both directions. */
.cms-ticker-track {
  display: inline-flex; align-items: center; gap: 32px;
  padding: 11px 0; margin-right: 32px; white-space: nowrap;
}
.cms-annc-v .cms-ticker { max-height: 240px; }
.cms-annc-v .cms-ticker-viewport {
  flex-direction: column; width: 100%;
  animation-name: cms-ticker-y;
}
.cms-annc-v .cms-ticker-track {
  display: flex; flex-direction: column; align-items: stretch; gap: 12px;
  padding: 0 18px; margin: 0 0 12px; white-space: normal;
}
@keyframes cms-ticker-x { to { transform: translateX(-50%); } }
@keyframes cms-ticker-y { to { transform: translateY(-50%); } }
/* Corner style (the block's Corners prop). Pill keeps the ends readable by insetting the
   ticker's clip to the same radius — without that, a scrolling item's text is chopped by
   the wrapper's curve mid-glyph. */
.cms-annc-corner-soft { border-radius: 6px; }
.cms-annc-corner-sq { border-radius: 0; }
.cms-annc-corner-pill { border-radius: 999px; }
.cms-annc-corner-pill .cms-ticker { border-radius: inherit; }
.cms-annc-corner-pill.cms-annc-h .cms-ticker-track { padding-top: 12px; padding-bottom: 12px; }

/* Scroll speed (the block's Speed prop): wrapper classes scaling the renderer's inline
   duration. calc over the inherited --cms-ticker-dur, so pacing still tracks the item
   count — "fast" is fast for a long list too, not a fixed sprint. */
.cms-annc-slowest .cms-ticker-viewport { animation-duration: calc(var(--cms-ticker-dur, 24s) * 2.2); }
.cms-annc-slow .cms-ticker-viewport { animation-duration: calc(var(--cms-ticker-dur, 24s) * 1.5); }
.cms-annc-fast .cms-ticker-viewport { animation-duration: calc(var(--cms-ticker-dur, 24s) * 0.6); }
.cms-annc-fastest .cms-ticker-viewport { animation-duration: calc(var(--cms-ticker-dur, 24s) * 0.35); }
.cms-ticker:hover .cms-ticker-viewport { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .cms-ticker .cms-ticker-viewport { animation: none; }
}
.cms-ticker-item {
  display: inline-flex; align-items: center; gap: 9px;
  color: inherit; font-weight: 500; text-decoration: none; font-size: inherit;
}
.cms-ticker-item i { font-size: 1.1em; flex: 0 0 auto; }
a.cms-ticker-item:hover .cms-ticker-text { color: var(--site, #2563eb); text-decoration: underline; }

/* ── Carousel (Craft's Carousel block) ────────────────────────────────────────
   The viewport is a native scroll container with scroll-snap, not a transform-driven
   track: it is swipeable for free on touch, and everything here works with NO JS at all
   (site-carousel.js only adds arrows, dots, loop-wrap and autoplay on top — see its own
   comment). That matters because the Craft canvas and a Template/Form preview both render
   this exact markup and only one of them (the canvas) currently loads the script.

   Three breakpoint tiers (mobile/tablet/desktop) rather than the two the Columns block's
   Responsive mode uses — this was asked for in swiper.js terms, where per-breakpoint slide
   counts are the default expectation, not an edge case.

   Item count is a CSS CLASS (cms-car-m{n}/-t{n}/-d{n}), not a custom property: ContentSanitizer
   strips `--custom-props` out of inline style on save even though nothing deny-lists them — see
   its own comment — so a --cms-car-mobile:1 declaration baked into style="" would survive only
   until the page is actually saved. The gap is plain padding/margin instead of a --cms-car-gap
   custom property for the same reason — see index.html's carousel render case. */
.cms-carousel { position: relative; }
.cms-carousel-viewport {
  overflow-x: auto; overflow-y: hidden; scroll-snap-type: x proximity;
  scrollbar-width: none; -ms-overflow-style: none;
}
.cms-carousel-viewport::-webkit-scrollbar { display: none; }
.cms-carousel-track { display: flex; }
.cms-carousel-slide {
  box-sizing: border-box; flex: 0 0 100%; min-width: 0; scroll-snap-align: start;
}
.cms-carousel.cms-car-m2 .cms-carousel-slide { flex-basis: 50%; }
@media (min-width: 768px) {
  .cms-carousel.cms-car-t1 .cms-carousel-slide { flex-basis: 100%; }
  .cms-carousel.cms-car-t2 .cms-carousel-slide { flex-basis: 50%; }
  .cms-carousel.cms-car-t3 .cms-carousel-slide { flex-basis: 33.3334%; }
  .cms-carousel.cms-car-t4 .cms-carousel-slide { flex-basis: 25%; }
}
@media (min-width: 992px) {
  .cms-carousel.cms-car-d1 .cms-carousel-slide { flex-basis: 100%; }
  .cms-carousel.cms-car-d2 .cms-carousel-slide { flex-basis: 50%; }
  .cms-carousel.cms-car-d3 .cms-carousel-slide { flex-basis: 33.3334%; }
  .cms-carousel.cms-car-d4 .cms-carousel-slide { flex-basis: 25%; }
  .cms-carousel.cms-car-d5 .cms-carousel-slide { flex-basis: 20%; }
  .cms-carousel.cms-car-d6 .cms-carousel-slide { flex-basis: 16.6667%; }
}
/* Hidden until site-carousel.js marks the carousel ready — a non-functional arrow/dot row
   is worse than none; the scroll-snap track alone is already fully usable (swipe/scroll). */
.cms-carousel-prev, .cms-carousel-next, .cms-carousel-dots { display: none; }
.cms-carousel.cms-car-ready .cms-carousel-prev,
.cms-carousel.cms-car-ready .cms-carousel-next { display: flex; }
.cms-carousel.cms-car-ready .cms-carousel-dots { display: flex; }
.cms-carousel-prev, .cms-carousel-next {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  align-items: center; justify-content: center; width: 40px; height: 40px;
  border-radius: 50%; border: 1px solid rgba(17, 24, 39, .1); background: #fff;
  color: #1f2937; font-size: 16px; box-shadow: 0 6px 16px rgba(17, 24, 39, .12);
  cursor: pointer;
}
.cms-carousel-prev:hover, .cms-carousel-next:hover { background: var(--site, #2563eb); color: #fff; }
.cms-carousel-prev:disabled, .cms-carousel-next:disabled { opacity: .35; cursor: default; }
.cms-carousel-prev:disabled:hover, .cms-carousel-next:disabled:hover { background: #fff; color: #1f2937; }
/* Inset (not overhanging past the carousel's own edges): an overhang relied on whatever
   ambient padding its container happened to have (.cms-sec used to always carry a 20px
   gutter) to stay on-screen, so a carousel sitting flush against a zero-padding container —
   or just the viewport edge on a full-width section — pushed the Next button partly past
   the edge, into the scrollbar's own hit area, and real clicks on it silently did nothing.
   Sitting inside the box instead never depends on what's around it. */
.cms-carousel-prev { left: 8px; }
.cms-carousel-next { right: 8px; }
.cms-carousel-dots { justify-content: center; gap: 8px; margin-top: 14px; }
.cms-carousel-dots button {
  width: 8px; height: 8px; border-radius: 50%; border: none; padding: 0;
  background: rgba(17, 24, 39, .18); cursor: pointer;
}
.cms-carousel-dots button.on { background: var(--site, #2563eb); width: 22px; border-radius: 5px; }

/* ── Video backgrounds (hero + columns sections) ─────────────────────────────
   Three stacked layers inside the section: the video (or YouTube iframe), a darkening
   overlay for text legibility, and the section's own content z-indexed above both.
   Loaded here so the builder canvas and the published page render identically.

   The iframe cannot object-fit like a <video>, so it is oversized from the viewport
   ratio (56.25vw ↔ 16:9) and centred — the excess crops behind overflow:hidden, which
   is the standard cover simulation for embedded players. pointer-events:none keeps the
   layer from swallowing clicks (and blocks YouTube's own UI from ever engaging). */
.cms-bg-video-host { position: relative; overflow: hidden; }
.cms-bg-video { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
.cms-bg-video video { width: 100%; height: 100%; object-fit: cover; }
.cms-bg-video iframe {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: max(100%, 177.78vh); height: max(100%, 56.25vw); border: 0;
}
.cms-bg-overlay {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: rgba(2, 6, 23, var(--cms-ov, .35));
}
.cms-bg-video-host > *:not(.cms-bg-video):not(.cms-bg-overlay) { position: relative; z-index: 2; }

/* Ratio-sized hosts (bgVideoRatio prop): the container derives its height from its width via
   the inline aspect-ratio style, so the video frame shows whole instead of being cropped to
   the content's height. Content centres vertically inside the taller box; the iframe drops
   the viewport-based oversizing — the box already matches the frame, so 100%/100% is exact
   for 16:9 sources (other ratios letterbox inside the player, which is the honest result). */
.cms-bg-video-host.cms-bg-ratio {
  display: flex; flex-direction: column; justify-content: center;
}
.cms-bg-ratio .cms-bg-video iframe {
  /* Scaled ~15% past the box: YouTube's title overlay (top) and control strip (bottom)
     occupy fixed bands of the frame that no parameter removes — cropping those bands
     outside the container is the one reliable way to a chromeless background. The frame
     itself stays 16:9, so the scale costs edges, not proportions. */
  top: 50%; left: 50%; width: 100%; height: 100%;
  transform: translate(-50%, -50%) scale(1.15);
}

/* ── Anchor link block (cms-link, BasicBlocksApiController) ──────────────────
   One markup shape, three display modes, no conditionals in the template — the mode
   select's value is a class on the <a> and these rules do the branching (the same
   trick as the map footer above and the shell top bar's slots). "Button" mode is the
   class `btn`: Bootstrap's btn-primary/btn-lg declare only CSS custom properties, so
   in the other two modes those classes are inert and need no hiding at all. */
/* inline-flex so the icon can sit before OR after the text via `order` — the template's
   markup order is fixed (mustache can't branch). Flex children don't inherit the anchor's
   text-decoration, so the underline moves to the text span. */
.cms-anchor-a { display: inline-flex; align-items: center; gap: 7px; text-decoration: none; }
.cms-anchor-a:not(.btn):not(.cms-anchor-mode-img) .cms-anchor-text { text-decoration: underline; }
.cms-anchor-a:not(.btn) { color: var(--site, #2563eb); }
.cms-anchor-ico { flex: 0 0 auto; }
/* No icon chosen leaves class="… bi " — hide it, same trick as the top bar's slots. Image
   mode hides it too: the icon is a text/button affordance. */
.cms-anchor-ico:not([class*="bi-"]) { display: none; }
.cms-anchor-mode-img .cms-anchor-ico { display: none; }
.cms-anchor-ico-after .cms-anchor-ico { order: 3; }
/* The image only exists in Image mode, and only once a file has been chosen. */
.cms-anchor-img { display: none; max-width: 100%; height: auto; }
.cms-anchor-mode-img .cms-anchor-img:not([src=""]) { display: inline-block; }
.cms-anchor-mode-img .cms-anchor-text { display: none; }

/* ── Nav bar block ───────────────────────────────────────────────────────────
   Brand + menu + optional CTA in one row, with a real mobile drawer. The
   packaged .psite-header has this behaviour baked into its own markup; this is
   the standalone version, so a header composed from blocks does not lose the
   burger the moment it stops using that template. */
.psite-navbar { position: sticky; top: 0; z-index: 20; }
.psite-navbar.psite-unstick { position: static; }
.psite-navbar-in {
  max-width: var(--psite-shell-w); margin: 0 auto;
  padding: 0 var(--psite-shell-pad); min-height: 66px;
  display: flex; align-items: center; gap: 24px; flex-wrap: wrap;
  position: relative;
}
.psite-navbar-menu { display: flex; align-items: center; gap: 22px; flex: 1; }
/* Where the menu sits relative to the brand. */
.psite-nav-end    { justify-content: flex-end; }
.psite-nav-center { justify-content: center; }
.psite-nav-start  { justify-content: flex-start; }

/* Row composition is done with `order`, not DOM order, because the CTA has to be able to
   stay on row one while the menu drops to row two. The markup keeps the button next to
   the brand (which is what degrades sanely without this stylesheet); these rules put it
   back after the menu for the normal single-row case. */
.psite-navbar .psite-brand       { order: 1; }
.psite-navbar .psite-navbar-menu { order: 2; }
.psite-navbar .psite-cta         { order: 3; margin-left: 12px; }
.psite-navbar .psite-burger      { order: 4; }

/* ── Menu on its own row ──────────────────────────────────────────────────────
   For a menu with more links than fit beside the logo. flex-basis:100% forces the
   wrap, so row one keeps the brand and the button and row two is the full width of
   the container — no second block and no duplicated props. */
.psite-navbar-stack .psite-navbar-menu {
  order: 5; flex-basis: 100%; flex-grow: 1;
  border-top: 1px solid rgba(17,24,39,.08);
  padding: 4px 0; min-height: 46px;
}
.psite-navbar-stack.psite-navbar-dark  .psite-navbar-menu,
.psite-navbar-stack.psite-navbar-brand .psite-navbar-menu { border-top-color: rgba(255,255,255,.14); }
/* Row one becomes brand | … | button, so the button needs to claim the gap itself. */
.psite-navbar-stack .psite-cta { margin-left: auto; }
/* A stacked bar is two rows tall; 66px of minimum height would double up. */
.psite-navbar-stack .psite-navbar-in { min-height: 0; padding-top: 10px; padding-bottom: 4px; }
.psite-navbar-menu .cms-menu { gap: 22px; }
.psite-navbar-menu .cms-menu-link, .psite-navbar-menu .cms-menu-label {
  color: inherit; font-size: 14.5px; font-weight: 500;
}

/* Tones. "Plain" inherits whatever it is dropped onto. */
.psite-navbar-light { background: rgba(255,255,255,.92); backdrop-filter: saturate(180%) blur(8px); border-bottom: 1px solid rgba(17,24,39,.08); color: #111827; }
.psite-navbar-dark  { background: #111827; color: #e5e7eb; border-bottom: 1px solid rgba(255,255,255,.08); }
.psite-navbar-brand { background: var(--site); color: #fff; }
.psite-navbar-plain { background: transparent; }
.psite-navbar-dark .cms-menu-link:hover,
.psite-navbar-brand .cms-menu-link:hover { color: #fff; opacity: .85; }

/* The burger is desktop-hidden; the media query below turns it on. */
.psite-navbar .psite-burger { display: none; margin-left: auto; color: inherit; }

@media (max-width: 860px) {
  .psite-navbar .psite-burger { display: inline-flex; align-items: center; }
  /* Collapsed by default on narrow screens: the row keeps only brand + burger,
     and the menu becomes a stacked drawer underneath when .nav-open is set. */
  .psite-navbar .psite-navbar-menu { display: none; flex-basis: 100%; }
  .psite-navbar.nav-open .psite-navbar-menu {
    /* .psite-navbar is position:sticky, not absolute, so this drawer stays in normal flow
       and the page can usually just scroll past it — but a sticky element taller than the
       viewport can still end up partly unreachable while it's pinned. Same cap and internal
       scroll as the packaged header's drawer (site-shell.css, the max-height:80vh comment),
       for the same reason. */
    display: flex; flex-direction: column; align-items: stretch;
    gap: 2px; padding: 8px 0 16px;
    max-height: 80vh; overflow-y: auto;
  }
  .psite-navbar.nav-open .psite-navbar-menu .cms-menu {
    flex-direction: column; align-items: stretch; gap: 2px; width: 100%;
  }
  /* Dropdowns have nowhere to go in a drawer — show children inline instead. */
  .psite-navbar.nav-open .cms-menu-sub {
    position: static; opacity: 1; visibility: visible; transform: none;
    background: none; border: none; box-shadow: none; padding: 0 0 0 14px; min-width: 0;
  }
  .psite-navbar.nav-open .cms-menu-link,
  .psite-navbar.nav-open .cms-menu-label { display: block; padding: 8px 2px; }
  /* The CTA sits outside the menu now, so on a phone it would otherwise crowd the burger.
     Hide it with the collapsed menu and give it its own full-width row in the drawer. */
  .psite-navbar .psite-cta { display: none; }
  .psite-navbar.nav-open .psite-cta {
    display: block; order: 6; flex-basis: 100%;
    margin: 4px 0 14px; text-align: center;
  }
  /* :empty is only two classes, so it loses to the rule above — restate it at equal weight
     or a nav bar with no button label shows an empty pill in the drawer. */
  .psite-navbar.nav-open .psite-cta:empty { display: none; }
  /* Stacking is a desktop affordance; the drawer is already one column per row. */
  .psite-navbar-stack .psite-navbar-menu { border-top: none; padding: 0; min-height: 0; }
}

/* ── PDF block (DocsApiController) ───────────────────────────────────────────
   Lives here rather than in the builder's own stylesheet for the same reason the
   Map block does: the markup has to look identical in the canvas, in preview and
   on the published site, and site-shell.css is loaded by all three.

   The mode class on the root decides which half shows. Both halves are always in
   the markup because Craft substitutes props into this template and cannot branch. */
.cms-pdf { margin: 0 0 1rem; }
/* No file chosen yet — collapse rather than render an empty frame and a dead card. The second
   pair is the Button/Link styles' own root — unlike Card, there is no wrapping .cms-pdf div to
   scope through, the mode class sits directly on the <a>, so it doubles as that scope here. */
.cms-pdf .cms-pdf-card[href=""] { display: none; }
.cms-pdf-linked[href=""], .cms-pdf-inline[href=""] { display: none; }
/* The mode class is deliberately NOT .cms-pdf-card — that is the anchor's own class,
   and a root carrying it would make every ".cms-pdf-card" selector (and every
   querySelector in a test or a script) match the wrapper as well as the link. */
.cms-pdf-inline .cms-pdf-card { display: none; }
.cms-pdf-inline .cms-pdf-frame { display: block; }
.cms-pdf:not(.cms-pdf-inline) .cms-pdf-frame { display: none; }

.cms-pdf-frame {
  width: 100%; overflow: hidden; border-radius: var(--site-radius, 14px);
  border: 1px solid var(--color-border-subtle, rgba(17, 24, 39, .12));
  background: var(--color-bg-subtle, #f1f3f5);
}
.cms-pdf-frame iframe { display: block; width: 100%; height: 100%; border: 0; }

.cms-pdf-card {
  display: flex; align-items: center; gap: 14px; text-decoration: none;
  padding: 14px 16px; border-radius: var(--site-radius, 14px);
  /* Filled surface rather than an outline at rest. An outline here sat a few pixels
     inside the page builder's own 2px selection border and read as two nested boxes
     around one block — the card is the only block that drew its own frame, so it was
     the only one where that doubling showed. Transparent (not absent) keeps the box
     model identical so nothing shifts on hover. */
  border: 1px solid transparent;
  background: var(--color-bg-subtle, #f4f6f8);
  color: inherit; transition: border-color .14s, box-shadow .14s, background .14s;
}
.cms-pdf-card:hover {
  background: #fff;
  border-color: var(--site, #4338ca);
  box-shadow: 0 8px 22px rgba(17, 24, 39, .09);
  color: inherit;
}
.cms-pdf-ico {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 42px; height: 42px; border-radius: 10px;
  background: color-mix(in srgb, #dc2626 12%, transparent); color: #dc2626; font-size: 21px;
}
/* Icon is optional — craft-integration.js's own Card render already omits this span entirely
   when unset, so this only matters for the registration-only fallback (DocsApiController),
   which has no way to branch and always emits the span. */
.cms-pdf-ico i:not([class*="bi-"]) { display: none; }
.cms-pdf-meta { display: flex; flex-direction: column; min-width: 0; }
.cms-pdf-title {
  font-weight: 600; font-size: 15px; color: var(--ink, #111827);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Title is optional — same story as the icon above. */
.cms-pdf-title:empty { display: none; }
.cms-pdf-sub { font-size: 12.5px; color: var(--color-text-muted, #6b7280); }
.cms-pdf-btn {
  margin-left: auto; flex: 0 0 auto; white-space: nowrap;
  background: var(--site, #4338ca); color: #fff;
  border-radius: var(--site-radius-btn, 9px); padding: 8px 15px;
  font-size: 14px; font-weight: 600;
}
.cms-pdf-btn:empty { display: none; }

/* Button style's icon sits in a plain inline-block <a class="btn ...">, not a flexbox like
   .cms-inline-link below (which gets its gap for free from `gap:6px`) — scoped to .btn so this
   doesn't also apply inside .cms-inline-link and stack on top of its own flex gap. */
.btn .cms-pdf-btn-ico { margin-right: 8px; }

@media (max-width: 560px) {
  .cms-pdf-card { flex-wrap: wrap; }
  .cms-pdf-btn { margin-left: 0; width: 100%; text-align: center; }
}

/* ── Inline link trigger ──────────────────────────────────────────────────
   Shared by the PDF block's "Link" style and the Modal block's "Link" trigger style — both
   are meant to sit compactly, several to a row inside a Columns cell, rather than stack as
   their own full-width element the way the PDF Card and the Modal's old button-only trigger
   did. */
.cms-inline-link {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--site, #4338ca); font-weight: 500; font-size: 14.5px;
  text-decoration: none; cursor: pointer;
}
.cms-inline-link:hover { text-decoration: underline; color: var(--site, #4338ca); }
.cms-inline-link i:not([class*="bi-"]) { display: none; }

/* Video block in lightbox mode: the inline embed is replaced by a poster card
   (craft-integration.js), because an iframe already playing in the page gains
   nothing from a viewer over the top of it. */
.cms-vid-card {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  min-height: 180px; padding: 24px; text-decoration: none;
  border-radius: var(--site-radius, 14px);
  background: linear-gradient(135deg, var(--site, #4338ca), var(--site-2, #7c3aed));
  color: #fff; font-weight: 600;
}
.cms-vid-card:hover { filter: brightness(1.06); color: #fff; }

/* With a real thumbnail the inline background-image replaces the gradient, so the
   sizing keywords have to be restated here — without them the still tiles at its
   natural 480x360 instead of filling the card. Aspect ratio rather than min-height
   so the card reads as a video frame rather than a banner. */
.cms-vid-thumb {
  position: relative;
  aspect-ratio: 16 / 9; min-height: 0; padding: 0;
  background-size: cover; background-position: center; background-repeat: no-repeat;
}
/* The still is arbitrary photography; the play button and label need to stay legible
   over whatever it happens to be. */
.cms-vid-thumb::before {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(15,20,32,.15), rgba(15,20,32,.55));
  border-radius: inherit;
}
/* Stacked, not side by side: over a 16:9 still the label needs the width, and a
   centred row put the caption half on top of the play button. */
.cms-vid-thumb { flex-direction: column; gap: 10px; }
.cms-vid-thumb .cms-vid-play, .cms-vid-thumb .cms-vid-label { position: relative; z-index: 1; }
.cms-vid-thumb .cms-vid-play {
  background: rgba(255,255,255,.92); color: #111827;
  width: 62px; height: 62px; font-size: 30px;
}
.cms-vid-thumb .cms-vid-label { text-shadow: 0 1px 3px rgba(0,0,0,.55); }
.cms-vid-play {
  display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; border-radius: 50%; font-size: 24px;
  background: rgba(255, 255, 255, .22);
}
/* An image opting into the viewer should say so on hover. */
.cms-lb-on .cms-lb-link { display: inline-block; cursor: zoom-in; }
.cms-lb-on .cms-lb-link img { display: block; }
