/* ── Shared nav ──────────────────────────────────────────────────────── */
.snav-header {
  position: sticky; top: 0; z-index: 50;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  padding: 0;
  /* The size for the styles that set their links uppercase and tracked out
     (centered, rail, underline, and the --caps modifier). Uppercase reads
     larger than its point size, so it comes down against sentence case: but
     ONLY once. A theme that declares --snav-link-size has already made that
     decision inside its own nav rule: grid, lumen, editorial and frame all
     size an uppercase link there. Taking .72rem as the FALLBACK .
     multiplying means a theme's number is used as the theme wrote it, and a
     theme that declares nothing still gets the reduction. Multiplying instead
     applied it twice and put lumen and frame at 9px. */
  --snav-link-caps: var(--snav-link-size, .72rem);
}
.snav-inner {
  /* The theme's own content column. Each theme
     declares --pf-container/--pf-gutter beside its `.container` rule; the
     fallbacks are the values this file used to hardcode, so a theme that
     declares neither renders exactly as before.
     This is what keeps the brand on the same left edge as the page's text. A
     menu style drops the theme's `.nav--<id>` rules: including the per-theme
     `max-width`: so without these the header sat in a 1200px column while the
     page sat in the theme's, and the brand was 80px inside its own site. */
  max-width: var(--pf-container, 1200px); margin: 0 auto;
  display: flex; align-items: center;
  /* min-height, unlike a fixed height: past ~7 pages (or a couple of long titles) the
     desktop menu below wraps to a second row, and a fixed height would clip
     that row. letting the sticky header grow to fit it (#640). */
  padding-block: .5rem;
  padding-inline: var(--pf-gutter, 1.25rem);
  min-height: 60px;
  gap: 1.5rem;
}
.snav-lang { display: flex; align-items: center; gap: .15rem; flex-shrink: 0; margin-left: .5rem; font-size: .8rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.snav-lang a, .snav-lang .snav-lang-cur { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; padding: 0 .35rem; color: var(--text); border-radius: .35rem; text-decoration: none; }
.snav-lang a { opacity: .7; }
.snav-lang a:hover, .snav-lang a:focus-visible { opacity: 1; background: color-mix(in srgb, var(--text) 8%, transparent); }
.snav-lang .snav-lang-cur { opacity: 1; background: color-mix(in srgb, var(--text) 6%, transparent); }

.snav-brand {
  display: flex; align-items: center; gap: .65rem;
  /* Same reasoning as the column above: the size is the THEME's voice, so a
     menu style must not silently shrink it. Lumen's 1.45rem masthead became
     1rem under every variant. */
  font-weight: 700; font-size: var(--snav-brand-size, 1rem); letter-spacing: -.01em;
  text-decoration: none; color: var(--text); white-space: nowrap;
  min-height: 44px;
}
.snav-brand img { height: 32px; width: auto; }
/* On a phone the wordmark must give way. push the hamburger off the
   screen. `.snav-brand` is nowrap and, as a flex item, will not shrink below its
   content without min-width:0: so a long name in a style that tracks it out
   (underline sets .14em and uppercases it) grew from 255px to 338px on
   klein-geluk and put the hamburger at x=430 in a 390px viewport: 40px of
   horizontal scroll on the one control a phone visitor needs.
   Found by sweeping all nine templates; the four the matrix already covered
   have shorter names and never showed it. */
@media (max-width: 639px) {
  .snav-brand { min-width: 0; }
  .snav-brand span { overflow: hidden; text-overflow: ellipsis; }
}

/* ── Desktop nav (≥ 1100px) ──────────────────────────────────────────── */
.snav-desktop {
  display: none;
  align-items: center; gap: 1.5rem; row-gap: .5rem; margin-left: auto;
  list-style: none; margin: 0 0 0 auto; padding: .5rem 0;
  /* A photographer with ~7+ pages, or a couple of long titles, ran the menu
     off the right edge of the viewport with no way back (#640): nowrap +
     no overflow handling took the whole document sideways with it. Wrapping
     to a second row keeps every link reachable, in place, at its existing
     44px touch box, with no horizontal scroll and no earlier collapse into
     the hamburger than the fit genuinely requires. */
  flex-wrap: wrap;
  justify-content: flex-end;
}
.snav-desktop a {
  /* The other half of the theme's type scale. --snav-brand-size was added when
     a menu style was found shrinking lumen's masthead, but a nav has two sizes
     and only the brand was carried back: four themes (grid, lumen, atelier,
     frame) size their links in their own `.nav--<id>` rules, a menu style drops
     those rules wholesale, and the links fell to this generic value on every
     one of them. Measured across nine templates x six styles, the link size was
     the SAME number on all nine: including the four that ask for their own.
     Frame reads worst: it sets a 1rem brand against .74rem links, and got a
     16px brand against 14px links, which is no masthead at all. */
  font-size: var(--snav-link-size, .9rem); font-weight: 500; color: var(--text); opacity: .7;
  text-decoration: none; transition: opacity .15s;
  white-space: nowrap;
  display: inline-flex; align-items: center; min-height: 44px;
}
.snav-desktop a:hover, .snav-desktop a.active { opacity: 1; color: var(--primary-ink, var(--primary)); }

/* ── Desktop dropdown: a parent page with visible children (#1639/#1616) ── */
/* :hover/:focus-within, no JS: tabbing onto the parent link or any child
   link keeps `.snav-has-children` focus-within, so a keyboard visitor reaches
   every child the same way a mouse hover does, and Escape/click-away just
   falls out of the box the same way a native menu does. The parent link
   still navigates on click/Enter: it stays a real page, and the dropdown is
   additive, never a way to intercept the click. */
.snav-has-children { position: relative; }
/* ::before, not ::after: the `underline` menu style already owns `::after` on
   every `.snav-desktop a` for its sliding indicator bar (further down this
   file), and the two would otherwise fight over the same pseudo-element on
   this exact link. */
.snav-has-children > a::before {
  /* A caret hints there is more, decoration only: it carries no semantics of
     its own and nothing here depends on it being present. `order` puts it
     after the label text: the link is an inline-flex row, so a generated
     pseudo-element is a flex item like any other and reordering it costs
     nothing else. */
  content: ""; display: inline-block; width: .4em; height: .4em; margin-left: .4em;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg); opacity: .55;
  order: 1;
}
.snav-dropdown {
  /* No margin-top: a visible gap there is a dead zone the pointer has to
     cross to get from the trigger link to the panel, and neither box is
     hoverable while it's in that gap, so :hover drops and the panel starts
     closing before the pointer ever reaches it (confirmed: opacity was
     already mid-fade a few px below the trigger). The same visual offset
     comes from padding-top instead, which IS part of this element's own
     hit area, so the trigger and the panel form one continuous hoverable
     shape with nothing in between. */
  position: absolute; top: 100%; left: 0; margin: 0; padding: .8rem .4rem .4rem;
  list-style: none; min-width: 180px;
  background: var(--bg); border: 1px solid var(--border); border-radius: .6rem;
  box-shadow: 0 16px 40px rgba(0,0,0,.14);
  opacity: 0; visibility: hidden; transform: translateY(4px);
  transition: opacity .15s ease, visibility .15s, transform .15s ease;
  z-index: 60;
}
.snav-has-children:hover .snav-dropdown,
.snav-has-children:focus-within .snav-dropdown {
  opacity: 1; visibility: visible; transform: none;
}
/* The panel anchors on the trigger's LEFT edge and grows rightward, which
   pushes it straight off-screen for the last (rightmost) top-level item on
   any theme whose --pf-container leaves little margin to the viewport edge
   (confirmed on the frame theme at 1280px: the panel's last ~90px, "Weddings"
   and "Portraits", rendered past the viewport and forced horizontal page
   scroll). Anchoring the last item's panel on its RIGHT edge instead keeps it
   growing leftward, which is always inside the header it belongs to. */
.snav-has-children:last-child .snav-dropdown,
.snav-has-children.snav-dropdown-right .snav-dropdown { left: auto; right: 0; }
.snav-dropdown a {
  /* The app's 44px touch floor applies here too: a mouse-hover dropdown is
     still reachable by a keyboard visitor tabbing through it, and still gets
     tapped open on a touch-capable laptop trackpad/screen. */
  display: flex; align-items: center; min-height: 44px; padding: 0 .75rem;
  font-size: var(--snav-link-size, .9rem); font-weight: 500; color: var(--text);
  opacity: .8; text-decoration: none; white-space: nowrap; border-radius: .4rem;
  transition: background .1s, opacity .1s;
}
.snav-dropdown a:hover, .snav-dropdown a.active {
  opacity: 1; color: var(--primary-ink, var(--primary));
  background: color-mix(in srgb, var(--text) 6%, transparent);
}
/* ── Desktop flyout: level 3, off a dropdown item (#1724) ──────────────── */
/* Same mechanism as the dropdown one level up: :hover/:focus-within on the
   item's <li>, no JS. It opens to the RIGHT of the dropdown, top-aligned with
   its item; padding-left (not margin) keeps the pointer path from the item to
   the panel inside one hoverable shape, for the same reason .snav-dropdown
   has no margin-top. Deeper levels are not drawn on desktop. */
.snav-has-flyout { position: relative; }
/* ::before like the dropdown's caret: the `underline` style owns ::after. */
.snav-has-flyout > a::before {
  content: ""; display: inline-block; width: .4em; height: .4em; margin-left: auto; padding: 0;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); opacity: .55; flex: 0 0 auto; order: 1;
}
.snav-flyout {
  position: absolute; top: -.4rem; left: 100%; margin: 0; padding: .4rem .4rem .4rem .8rem;
  list-style: none; min-width: 180px;
  background: var(--bg); border: 1px solid var(--border); border-radius: .6rem;
  box-shadow: 0 16px 40px rgba(0,0,0,.14);
  opacity: 0; visibility: hidden; transform: translateX(-4px);
  transition: opacity .15s ease, visibility .15s, transform .15s ease;
  z-index: 61;
}
.snav-has-flyout:hover > .snav-flyout,
.snav-has-flyout:focus-within > .snav-flyout {
  opacity: 1; visibility: visible; transform: none;
}
/* The last top-level item's dropdown is right-anchored (above), so its flyout
   opens leftward or it would leave the viewport. */
.snav-has-children:last-child .snav-flyout {
  left: auto; right: 100%; padding: .4rem .8rem .4rem .4rem; transform: translateX(4px);
}
.snav-has-children:last-child .snav-has-flyout:hover > .snav-flyout,
.snav-has-children:last-child .snav-has-flyout:focus-within > .snav-flyout { transform: none; }
.snav-has-children:last-child .snav-has-flyout > a::before { transform: rotate(135deg); }
/* Any other item whose flyout would not fit to the right: nav.ts measures on
   hover/focus and adds .snav-flyout-left. Same shape as the last-child rules. */
.snav-has-flyout.snav-flyout-left > .snav-flyout {
  left: auto; right: 100%; padding: .4rem .8rem .4rem .4rem; transform: translateX(4px);
}
.snav-has-flyout.snav-flyout-left:hover > .snav-flyout,
.snav-has-flyout.snav-flyout-left:focus-within > .snav-flyout { transform: none; }
.snav-has-flyout.snav-flyout-left > a::before { transform: rotate(135deg); }
@media (prefers-reduced-motion: reduce) {
  .snav-dropdown, .snav-flyout { transition: opacity .05s, visibility .05s; }
}


/* ── Hamburger button (640–1099px) ───────────────────────────────────── */
.snav-hamburger {
  display: none;
  margin-left: auto;
  /* Floor the hit box to the app's 44px touch target regardless of the 22px
     icon inside it: padding alone (.5rem = 8px each side, 38x38 total) fell
     short. min-width/min-height + centering keeps the icon visually unchanged. */
  min-width: 44px; min-height: 44px;
  align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer; padding: .5rem;
  color: var(--text); border-radius: .35rem;
}
.snav-hamburger:hover { background: color-mix(in srgb, var(--text) 8%, transparent); }

/* An open menu holds the page still.
   Every panel here is `position: fixed`, so the site behind one can scroll a
   whole viewport without anything appearing to move: measured at 1099px on
   all six styles, a single wheel gesture over the scrim took the page down
   600px, and closing the menu dropped the reader somewhere they never chose.
   The class goes on the ROOT element and not on `body`, because iOS Safari
   ignores `overflow: hidden` there; `scrollbar-gutter: stable` keeps the
   classic reflow-when-the-scrollbar-goes lurch out of it. nav.ts owns the
   class: it is the only thing that knows a panel opened. */
html.snav-locked { overflow: hidden; scrollbar-gutter: stable; }
/* And a menu long enough to scroll must not chain out to the document when it
   reaches its own end, which is the same defect arriving by a second door. */
.snav-drawer, .snav-overlay, .snav-more-sheet { overscroll-behavior: contain; }

/* ── Drawer overlay (640–1099px) ─────────────────────────────────────── */
.snav-scrim {
  display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 200;
}
/* Parked off-screen is not the same as hidden. Both panels below used to be
   moved out of view by `transform` alone, with nothing to take them out of the
   tab order or the accessibility tree: on a closed gallery page a keyboard
   visitor tabs straight into a menu they cannot see, and a screen reader reads
   its links as page content. It is also what paints them into a full-page
   screenshot, which is how this was found: on the gallery capture the closed
   more-sheet drew a 45px white band with a left-aligned "Contact" across the
   photo grid at the 900px fold. `.snav-overlay` in this same file already gets
   this right; these two were missed. The transition keeps the slide: on open
   visibility flips with no delay, on close it waits out the transform. */
.snav-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(320px, 85vw);
  background: var(--bg); z-index: 201; padding: 1.5rem;
  transform: translateX(100%); visibility: hidden;
  transition: transform .25s ease, visibility 0s linear .25s;
  overflow-y: auto; display: flex; flex-direction: column; gap: .25rem;
}
.snav-scrim.open { display: block; }
.snav-drawer.open {
  transform: translateX(0); visibility: visible;
  transition: transform .25s ease, visibility 0s;
}
.snav-drawer-close {
  align-self: flex-end; background: none; border: none; cursor: pointer;
  font-size: 1.5rem; color: var(--text); padding: .25rem; margin-bottom: .75rem;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px;
}
.snav-drawer a {
  display: block; padding: .85rem 1rem; font-size: 1rem; font-weight: 500;
  color: var(--text); text-decoration: none; border-radius: .5rem;
  transition: background .1s;
}
.snav-drawer a:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
.snav-drawer a.active { color: var(--primary-ink, var(--primary)); background: color-mix(in srgb, var(--primary) 8%, transparent); }

/* ── Nested group: drawer + overlay, a parent with visible children
   (#1639/#1616). Shared shape (.snav-nested), styled per panel below. ───── */
/* The parent's own link stays a real, separately-tappable link ("the parent
   stays a clickable page", #1616), instead of being folded into the toggle
   button, which is why this is two elements in a row rather than one
   overloaded as both a link and a disclosure trigger. A native
   <details>/<summary> owns the expand/collapse: no JS, works with a tap and
   with the keyboard (Enter/Space toggles a focused summary), and needs no
   open/close wiring of its own. flex-wrap is what puts the children list on
   its own row once open: `.snav-nested-children` is the last flex item and
   claims a fresh line with `flex-basis: 100%`, no absolute positioning.
   `.snav-nested-toggle` (the <details>) is `display: contents`: flex-basis
   only does anything for a DIRECT flex item of `.snav-nested`, and
   `.snav-nested-children` is markup-nested one level deeper, inside
   <details>, as its expand/collapse target. `display: contents` drops the
   <details> element's own box and promotes <summary> and
   `.snav-nested-children` to sit directly in `.snav-nested`'s flex context,
   without changing the DOM or its native toggle behaviour: clicking
   <summary> still flips `open` on the same <details> node. The native
   `details:not([open]) > *:not(summary) { display: none }` rule still keys
   off that DOM relationship, not the box, so the closed state is unaffected. */
.snav-nested { display: flex; align-items: center; flex-wrap: wrap; }
.snav-nested-link { flex: 1 1 auto; }
.snav-nested-toggle { display: contents; }
.snav-nested-toggle summary {
  list-style: none; cursor: pointer; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px;
}
.snav-nested-toggle summary::-webkit-details-marker { display: none; }
.snav-nested-toggle summary::after {
  content: ""; display: inline-block; width: .5em; height: .5em;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg); transition: transform .15s ease; opacity: .55;
}
.snav-nested-toggle[open] summary::after { transform: rotate(-135deg); }
.snav-nested-children { flex-basis: 100%; display: flex; flex-direction: column; }
@media (prefers-reduced-motion: reduce) {
  .snav-nested-toggle summary::after { transition: none; }
}

/* Deeper groups (#1724) nest their own .snav-nested-children inside this one,
   so the container padding stacks once per level; the link rule sets a fixed
   inset and does not stack. Both are 1rem, so every level steps in by 1rem
   (with a .75rem container the step was 12px from level 3 down, half of the
   level 1 to 2 step, and depth stopped reading). */
.snav-drawer .snav-nested-children { padding-left: 1rem; }
.snav-drawer .snav-nested-children a { padding-left: 1rem; }

/* ── Bottom tab bar (< 640px) ────────────────────────────────────────── */
.snav-bottom {
  display: none; position: fixed; bottom: 0; left: 0; right: 0;
  background: var(--bg);
  border-top: 1px solid var(--border);
  box-shadow: 0 -2px 12px rgba(0,0,0,.08);
  z-index: 100; height: 60px;
}
.snav-bottom-inner {
  display: flex; align-items: stretch; height: 100%;
}
.snav-tab {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 2px;
  background: none; border: none; cursor: pointer; padding: .25rem .1rem;
  color: var(--text); opacity: .5; text-decoration: none; font-size: .6rem;
  font-weight: 600; transition: opacity .15s, color .15s; letter-spacing: .01em;
}
.snav-tab:hover, .snav-tab.active { opacity: 1; color: var(--primary-ink, var(--primary)); }
.snav-tab svg { width: 20px; height: 20px; flex-shrink: 0; }
/* Long page titles ("Design compare · block matrix") wrapped past the label's
   own space inside the bar's fixed 60px height and got cropped by the bar's
   edge. Clamp the label to 2 lines with an ellipsis so it truncates cleanly
   instead: the bar's height and the icon size stay untouched. This has to
   target the label wrapper, not `.snav-tab` itself: `.snav-tab` also contains
   the icon `<svg>`, and `-webkit-line-clamp` on a box with a non-text sibling
   does not clamp at all (verified: it falls back to unclamped flow layout). */
.snav-tab-label {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; text-overflow: ellipsis;
}

/* More sheet: slides UP from above the tab bar when open, hidden below when closed */
.snav-more-sheet {
  position: fixed; bottom: 60px; left: 0; right: 0; z-index: 150;
  background: var(--bg);
  border-top: 1px solid var(--border);
  box-shadow: 0 -4px 20px rgba(0,0,0,.12);
  /* Hidden: translate down so it sits BELOW the bottom of the sheet's own area
     (i.e. completely off-screen below the tab bar) */
  transform: translateY(calc(100% + 60px)); visibility: hidden;
  transition: transform .2s ease, visibility 0s linear .2s;
  max-height: 50vh; overflow-y: auto;
}
.snav-more-sheet.open {
  transform: translateY(0); visibility: visible;
  transition: transform .2s ease, visibility 0s;
}
.snav-more-sheet a {
  display: block; padding: .9rem 1.25rem; font-size: 1rem; font-weight: 500;
  color: var(--text); text-decoration: none; border-bottom: 1px solid var(--border);
}
.snav-more-sheet a:last-child { border-bottom: none; }
.snav-more-sheet a.active { color: var(--primary-ink, var(--primary)); font-weight: 600; }
.snav-more-scrim {
  display: none; position: fixed; inset: 0; bottom: 60px; z-index: 149;
  background: rgba(0,0,0,.3);
}
.snav-more-scrim.open { display: block; }

/* Content padding for bottom bar + PWA/safe-area support */
@media (max-width: 639px) {
  .snav-bottom {
    display: block;
    /* Grow the bar to fill the home-indicator / PWA safe area so it never
       sits flush against the very bottom edge of the screen. */
    padding-bottom: env(safe-area-inset-bottom);
    height: calc(60px + env(safe-area-inset-bottom));
  }
  /* Pad the whole body: this covers <main> AND <footer> so neither
     gets hidden under the fixed tab bar regardless of page structure. */
  body {
    padding-bottom: calc(60px + env(safe-area-inset-bottom));
  }
  main { padding-bottom: 1rem !important; }
  /* Ensure the more-sheet scrim also clears the grown bar height */
  .snav-more-scrim { bottom: calc(60px + env(safe-area-inset-bottom)); }
}

/* The bottom tab bar, its "more" sheet and scrim are part of the <640px
   layout only. Pin them to display:none above that breakpoint so a fixed,
   transform-hidden sheet can never leak onto a wider viewport. */
@media (min-width: 640px) {
  .snav-bottom, .snav-more-sheet, .snav-more-scrim { display: none !important; }
}

@media (min-width: 640px) and (max-width: 1099px) {
  .snav-hamburger { display: flex; align-items: center; }
}

@media (min-width: 1100px) {
  .snav-desktop { display: flex; }
}

/* ══ Mobile menu forms ═════════════════════════════════════════════════════
   Everything above is the ORIGINAL responsive layout and stays untouched: a
   site that never opened the menu picker carries no `snav-m-` class and reads
   exactly these rules.

   The six menu styles used to be desktop-only. Every `.snav--<variant>` rule is
   unconditional or gated at min-width 640/1100, so below 640px all six rendered
   the identical tab bar: the style disappeared at the width most portfolio
   traffic arrives on. A `snav-m-<form>` class on the body element (menu_style.go:
   MobileStyleFor)
   picks the phone form, defaulting to the one that belongs to the site style.

   SPECIFICITY. Everything here is `body.snav-m-…` (0,2,0 or more) against the
   base rules' single class or bare element, so it wins without !important and
   without touching what it overrides. */

/* ── drawer / sheet: no tab bar, hamburger at every width ─────────────── */
@media (max-width: 639px) {
  body.snav-m-drawer .snav-bottom,
  body.snav-m-sheet  .snav-bottom { display: none; }
  /* The bar is gone, so the reserved strip under the page must go with it or
     the footer floats above a band of nothing. */
  body.snav-m-drawer, body.snav-m-sheet { padding-bottom: 0; }
  body.snav-m-drawer .snav-hamburger,
  body.snav-m-sheet  .snav-hamburger { display: flex; align-items: center; margin-left: auto; }
}

/* A rail is a left-edge idea, so its drawer comes in from the left .
   sliding in from the side the rail is not on. Only under the rail variant: an
   explicit `drawer` pick under any other style keeps the right-hand drawer that
   640-1099px already uses, so the two widths agree. */
body.snav-m-drawer.snav-v-rail .snav-drawer {
  right: auto; left: 0;
  transform: translateX(-100%);
}
body.snav-m-drawer.snav-v-rail .snav-drawer.open { transform: translateX(0); }

/* ── dock: the tab bar as a detached capsule ──────────────────────────── */
@media (max-width: 639px) {
  body.snav-m-dock .snav-bottom {
    left: .75rem; right: .75rem;
    bottom: calc(.75rem + env(safe-area-inset-bottom));
    height: 58px; padding-bottom: 0;
    border: 1px solid var(--border);
    border-radius: 999px;
    box-shadow: 0 10px 30px rgba(0,0,0,.18);
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    backdrop-filter: saturate(180%) blur(14px);
    overflow: hidden;
  }
  body.snav-m-dock { padding-bottom: calc(58px + .75rem + env(safe-area-inset-bottom) + .75rem); }
  /* The capsule floats, so the more-sheet has to clear its full footprint
    . the flush bar's 60px. */
  body.snav-m-dock .snav-more-sheet { bottom: calc(58px + 1.5rem + env(safe-area-inset-bottom)); }
  body.snav-m-dock .snav-more-scrim { bottom: calc(58px + 1.5rem + env(safe-area-inset-bottom)); }
}

/* ── sheet: the overlay panel, at phone width ─────────────────────────── */
/* One panel serves two purposes and they disagree about width: the overlay
   VARIANT wants it from 640px up, the sheet FORM below 640px. data-open-at says
   which (overlayOpenAt in menu_style.go), and nav.ts reads the same attribute.
   These rules are the CSS half: a panel that must not be reachable at a width
   must not be paintable there either, or a stale `.open` would show it. */
@media (min-width: 640px) {
  .snav-overlay[data-open-at="phone"] { display: none; }
}
@media (max-width: 639px) {
  .snav-overlay[data-open-at="wide"] { display: none; }
}

/* ── bare: icons without labels ───────────────────────────────────────── */
/* The label element stays in the DOM and every tab carries an aria-label, so hiding
   the text costs no accessible name. */
@media (max-width: 639px) {
  body.snav-m-bare .snav-tab-label { display: none; }
  body.snav-m-bare .snav-bottom { height: calc(50px + env(safe-area-inset-bottom)); }
  body.snav-m-bare { padding-bottom: calc(50px + env(safe-area-inset-bottom)); }
  body.snav-m-bare .snav-more-sheet { bottom: calc(50px + env(safe-area-inset-bottom)); }
  body.snav-m-bare .snav-more-scrim { bottom: calc(50px + env(safe-area-inset-bottom)); }
  body.snav-m-bare.snav-m-dock .snav-bottom { height: 50px; }
  body.snav-m-bare.snav-m-dock { padding-bottom: calc(50px + 1.5rem + env(safe-area-inset-bottom)); }
  body.snav-m-bare .snav-tab svg { width: 22px; height: 22px; }
}

/* ── the phone CTA ────────────────────────────────────────────────────── */
/* .snav-inner > .snav-cta is hidden below 1100px, so a photographer whose only
   call to action is the menu button has none on a phone. This is its own bar,
   docked above whatever the bottom form is. */
.snav-mcta { display: none; }
@media (max-width: 639px) {
  .snav-mcta {
    display: flex; align-items: center; justify-content: center;
    position: fixed; left: .75rem; right: .75rem; z-index: 99;
    bottom: calc(60px + env(safe-area-inset-bottom) + .6rem);
    min-height: 46px; padding: .6rem 1.1rem; border-radius: 10px;
    background: var(--primary); color: #fff; text-decoration: none;
    font-size: .92rem; font-weight: 700; letter-spacing: .01em;
    box-shadow: 0 10px 26px rgba(0,0,0,.22);
  }
  .snav-mcta:hover { text-decoration: none; filter: brightness(1.06); }
  /* Follow the bottom form's own footprint, and reserve room for the CTA on
     top of it so the last block still scrolls clear. */
  body.snav-m-dock .snav-mcta { bottom: calc(58px + 1.5rem + env(safe-area-inset-bottom) + .6rem); }
  body.snav-m-bare .snav-mcta { bottom: calc(50px + env(safe-area-inset-bottom) + .6rem); }
  body.snav-m-drawer .snav-mcta,
  body.snav-m-sheet  .snav-mcta { bottom: calc(env(safe-area-inset-bottom) + .75rem); }
  body:has(.snav-mcta) { padding-bottom: calc(60px + env(safe-area-inset-bottom) + 52px); }
  body.snav-m-dock:has(.snav-mcta) { padding-bottom: calc(58px + 1.5rem + env(safe-area-inset-bottom) + 52px); }
  body.snav-m-bare:has(.snav-mcta) { padding-bottom: calc(50px + env(safe-area-inset-bottom) + 52px); }
  body.snav-m-drawer:has(.snav-mcta),
  body.snav-m-sheet:has(.snav-mcta) { padding-bottom: calc(env(safe-area-inset-bottom) + 52px); }
}

/* ══ Menu styles ═══════════════════════════════════════════════════════════
   Six menu looks a photographer picks independently of their site theme
   (MenuConfig.Variant, internal/features/portfolio/domain.go). The header
   carries EITHER `nav--<template>` or `snav--<variant>`, never both: see
   menu_style.go: so nothing below competes with a theme's own nav rules.

   SPECIFICITY, and why some selectors here look heavier than they need to be.
   The inlined <style> order is not the same on both surfaces: a public page
   emits theme → nav → blocks (_blocks.tmpl), the builder canvas emits
   theme → blocks → nav (_canvas.tmpl). blocks/01-motion.css styles
   `.tpl-atelier .snav-header.scrolled` at 0,3,0, so a rule here that merely
   TIED with it would win in the builder and lose on the site: a canvas that
   shows a menu the visitor never gets. Anything that touches a property a
   theme or motion rule also touches is therefore written with the body hook in
   front (`body[class*="snav-v-"] …`) to land at 0,4,0 and resolve the same way
   in both orders.

   Every variant keeps the three responsive layouts intact: the desktop row at
   ≥1100px, the drawer at 640–1099px, the bottom tab bar below 640px. Only
   `overlay` changes which of them is used, and only above 640px.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Shared: the menu CTA ────────────────────────────────────────────────── */
.snav-cta {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; box-sizing: border-box;
  padding: .5rem 1.1rem; border-radius: .5rem;
  background: var(--primary); color: var(--on-primary);
  font-size: .82rem; font-weight: 700; letter-spacing: .01em;
  text-decoration: none; white-space: nowrap;
  transition: filter .15s;
}
.snav-cta:hover { filter: brightness(1.08); text-decoration: none; }
/* In the header band the CTA follows the link row, so it must not inherit the
   `margin-left:auto` that pushes that row right: otherwise the two are shoved
   apart to opposite ends. The row already owns the free space. */
.snav-inner > .snav-cta { flex-shrink: 0; margin-left: .5rem; }
/* Below 1100px the desktop row is hidden, which leaves the CTA as the only
   thing between the brand and the hamburger. Hide it there: the drawer and the
   overlay each carry their own copy, so the button is never lost. */
@media (max-width: 1099px) { .snav-inner > .snav-cta { display: none; } }
.snav-drawer-cta { margin-top: 1rem; width: 100%; }

/* The CTA keeps its own colours inside a panel.
   `.snav-drawer a` and `.snav-overlay-nav a` are both (0,1,1) and set `color`
   and `opacity` for the page links; `.snav-cta` is (0,1,0), so the button lost
   its --on-primary and got the panel's body colour on a saturated fill: dark
   navy text on blue in the drawer, and a washed 0.75 opacity in the sheet.
   Invisible while these panels were a tablet-only detail; the phone forms put
   them in front of everyone. (0,2,1) is the smallest thing that wins. */
.snav-drawer a.snav-cta,
.snav-overlay-nav a.snav-cta {
  color: var(--on-primary);
  opacity: 1;
  /* The overlay sets display-face type on its links; the button is a button. */
  font-family: inherit;
  font-size: .95rem;
  letter-spacing: .01em;
  /* And its own box. `.snav-drawer a` sets display:block and both panels set a
     1.25 line-height for the page links; inherited onto a min-height:44px
     button that leaves the label sitting near the top of the pill .
     in the middle of it. */
  display: inline-flex; align-items: center; justify-content: center;
  line-height: 1;
  /* Padding of its own, not `.snav-cta`'s. That rule's .5rem/1.1rem is measured
     against a .82rem label in a 60px header band; in a panel whose links are set
     in display type at up to 2.6rem, the same pill reads as a chip with the
     words pressed against its edges. A button gets bigger with its surroundings,
     and `line-height: 1` above removed the leading that was padding it out. */
  padding: .95rem 1.9rem;
  min-height: 52px;
  border-radius: .6rem;
}
.snav-drawer a.snav-cta:hover,
.snav-overlay-nav a.snav-cta:hover { color: var(--on-primary); background: var(--primary); }

/* ── Shared: settings ────────────────────────────────────────────────────── */
/* Sticky is the default and the pre-existing behaviour, so the class marks the
   DEPARTURE from it (menu_style.go, navModifiers). */
.snav-header.snav--unpinned { position: static; }
/* A page that hides the site menu renders no header, tab bar or drawer, so the
   padding that reserved room for the tab bar goes too (menu_style.go,
   navBodyClass). !important out-ranks the phone-form body rules above. */
body.snav-hidden { padding-bottom: 0 !important; }
/* A text-first page gets the room the header used to reserve. A page that opens
   with a hero stays flush against the top. */
body.snav-hidden--inset main { padding-top: calc(env(safe-area-inset-top, 0px) + 48px) !important; }


/* Transparent: the header sits over the first block until the page scrolls.
   nav.ts already toggles `.scrolled` past 24px, so this is styling only.
   Written at 0,4,0 for the reason in the header comment: atelier, frame,
   studio and lumen all style `.snav-header.scrolled` themselves.

   NAMED `onhero`, not `over`. `snav--over` is a substring of `snav--overlay`,
   the full-screen VARIANT's own class, so every `contains`-style check: a
   test, a grep, a harness: matched the wrong one; the E2E asserting "this
   page did NOT get the transparent treatment" passed on a header that read
   `snav-header snav--overlay`. And `snav-scrim` was already taken by the
   drawer's backdrop. `onhero` also says what the rule is FOR: the renderer
   only emits it on a page that opens with a hero block (opensEdgeToEdge). */
/* The scrim, and the type on it, follow the GROUND: not the page.
   ────────────────────────────────────────────────────────────────────────────
   A transparent menu floats over the first BLOCK, and a block can carry the
   opposite scheme to its page. frame-studio is a white page that opens with a
   black hero: the menu painted a white scrim across it and put near-black
   brand text on top, which reads as a rendering fault. as a style.
   `snav-hero-dark` / `-light` come from the first block itself (heroGround,
   menu_style.go), so the scrim darkens a dark ground and lightens a light one.

   The colours here are NEUTRAL. theme tokens on purpose: this is a
   legibility floor over an arbitrary photograph, not a place to express the
   brand. The accent still marks the active link. */
body[class*="snav-v-"] .snav-header.snav--onhero {
  border-bottom-color: transparent;
  transition: background .3s ease, border-color .3s ease, box-shadow .3s ease;
  /* Runway for the fade. The header's own band is ~60px, and a gradient that
     has to finish inside it ends in a visible edge across the photo. The extra
     padding is transparent and sits above the first block either way. */
  padding-bottom: 2.5rem;
  /* no-repeat, and it is load-bearing. The header still carries a 1px
     border-bottom, and a background image is SIZED to the padding box but
     PAINTED to the border box: so a repeating gradient tiles, and that 1px
     border row redrew the gradient's darkest first stop as a hairline straight
     across the photo. Measured 1px at y=101. */
  background-repeat: no-repeat;
  background-color: transparent;
}
body[class*="snav-v-"] .snav-header.snav--onhero.scrolled { padding-bottom: 0; }

/* Dark ground: a black scrim, light type. Also the default for an image hero,
   which lays its own dark scrim under light text whatever the page says. */
body.snav-hero-dark .snav-header.snav--onhero {
  /* Reaches the BOTTOM of the header, because two of the styles put type there.
     The ramp used to hit zero at 100%, which is fine for a one-row bar but not
     for `centered`, whose links sit on a second row against the lower edge of a
     140px header: measured at 4.08:1 on klein-geluk's bright hero, under the
     4.5:1 small-text floor, while every one-row style came in between 6:1 and
     13:1. Keeping a little wash all the way down costs the photo almost nothing
     and is the difference between a legible second row and a decorative one. */
  background-image: linear-gradient(to bottom,
    rgba(0, 0, 0, .58) 0%, rgba(0, 0, 0, .34) 45%, rgba(0, 0, 0, .16) 100%);
}
body.snav-hero-dark .snav-header.snav--onhero,
body.snav-hero-dark .snav-header.snav--onhero .snav-brand,
body.snav-hero-dark .snav-header.snav--onhero .snav-desktop a,
body.snav-hero-dark .snav-header.snav--onhero .snav-hamburger { color: #fff; }
body.snav-hero-dark .snav-header.snav--onhero .snav-desktop a { opacity: .82; }
body.snav-hero-dark .snav-header.snav--onhero .snav-desktop a:hover,
body.snav-hero-dark .snav-header.snav--onhero .snav-desktop a.active { opacity: 1; color: #fff; }

/* Light ground: a white scrim, dark type. */
body.snav-hero-light .snav-header.snav--onhero {
  /* Same reach as the dark ground above, and for the same second row. */
  background-image: linear-gradient(to bottom,
    rgba(255, 255, 255, .66) 0%, rgba(255, 255, 255, .36) 45%, rgba(255, 255, 255, .18) 100%);
}
body.snav-hero-light .snav-header.snav--onhero,
body.snav-hero-light .snav-header.snav--onhero .snav-brand,
body.snav-hero-light .snav-header.snav--onhero .snav-desktop a,
body.snav-hero-light .snav-header.snav--onhero .snav-hamburger { color: #14130f; }
body.snav-hero-light .snav-header.snav--onhero .snav-desktop a { opacity: .78; }
body.snav-hero-light .snav-header.snav--onhero .snav-desktop a:hover,
body.snav-hero-light .snav-header.snav--onhero .snav-desktop a.active { opacity: 1; color: #14130f; }

/* The dropdown panel is its own opaque box (`.snav-dropdown`, above), never
   painted over the hero photo the way the header band is, so it never takes
   the onhero white/dark-ground treatment: without this override the two
   rules above reached through `.snav-desktop a` and set dropdown link text
   white-on-white against the panel's own light `--bg`. Same specificity as
   what it corrects, so it has to come after it. */
body.snav-hero-dark .snav-header.snav--onhero .snav-dropdown a,
body.snav-hero-light .snav-header.snav--onhero .snav-dropdown a {
  color: var(--text); opacity: .8;
}
body.snav-hero-dark .snav-header.snav--onhero .snav-dropdown a:hover,
body.snav-hero-dark .snav-header.snav--onhero .snav-dropdown a.active,
body.snav-hero-light .snav-header.snav--onhero .snav-dropdown a:hover,
body.snav-hero-light .snav-header.snav--onhero .snav-dropdown a.active {
  opacity: 1; color: var(--primary-ink, var(--primary));
}

/* Once the page scrolls the header solidifies into the page's own palette,
   because by then it is over the page and not over the hero. */
body[class*="snav-v-"] .snav-header.snav--onhero.scrolled {
  background-image: none;
  background-color: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: saturate(1.3) blur(10px);
  border-bottom-color: var(--border);
  box-shadow: 0 4px 24px rgba(0, 0, 0, .08);
}
body[class*="snav-v-"] .snav-header.snav--onhero.scrolled,
body[class*="snav-v-"] .snav-header.snav--onhero.scrolled .snav-brand,
body[class*="snav-v-"] .snav-header.snav--onhero.scrolled .snav-desktop a,
body[class*="snav-v-"] .snav-header.snav--onhero.scrolled .snav-hamburger { color: var(--text); }
/* Once the page scrolls the header solidifies, and the runway would then be
   40px of empty bar under the links. */
body[class*="snav-v-"] .snav-header.snav--onhero.scrolled { padding-bottom: 0; }
/* A transparent header has no band of its own, so the page must start UNDER it
  . after it: otherwise the setting just paints the header the page
   colour and leaves the gap, which is the state four themes are in today.
   `fixed` (not a negative margin) is what does it: it takes the header out of
   flow entirely, so the first block starts at the top of the viewport with no
   number here that has to match a header height it cannot see. The <main>
   padding goes with it for the same reason: a full-bleed hero must reach the
   top edge, not stop 2rem short of it.

   Sticky only. An unpinned header is one that scrolls away, and `fixed` would
   pin the thing the photographer asked not to be pinned. */
body[class*="snav-v-"] .snav-header.snav--onhero:not(.snav--unpinned) {
  position: fixed; top: 0; left: 0; right: 0;
}
/* A page that opens edge to edge starts flush against the menu: whatever the
   menu style, transparent or not.
   ────────────────────────────────────────────────────────────────────────────
   <main> AND the article inside it. Zeroing only <main> leaves the theme's own
   `.pf-custom-page--roomy { padding: 2rem 0 4rem }` in place, and the hero
   still starts 32px down.
   Two defects, one rule. With a transparent header that 32px was a band of
   page background ABOVE the photo, so the scrim ended in a hard seam: the
   exact thing the scrim exists to avoid. With an opaque one it was 64px of
   dead background between the header's bottom edge and the hero's top, which
   reads as a mistake. as spacing. The theme navs get away with the
   same 64px because most of them are transparent and you cannot see where the
   header stops.
   `snav-hero` is emitted only when a variant is chosen AND the first block is
   edge to edge (navBodyClass), so no site that never opened the picker moves. */
body.snav-hero main,
body.snav-hero main > .pf-custom-page {
  padding-top: 0;
}
/* The same seam under a THEME nav (no menu style picked, so no `snav-hero`).
   Every theme nav is opaque once it sits in flow, and the starter home opens
   with a photo banner, so all twelve templates showed 48-80px of page
   background between the header and the photo. Keyed off the page's own
   first block, the photo-ground hero types from menu_style.go's
   imageHeroTypes, so a page that opens with text keeps its breathing room.
   The builder canvas renders the same main > article pair, so it moves too.
   "First block" is `:not(.page-block ~ .page-block)`: the article opens with
   the inlined block <style>, so `:first-child` never matches a block there. */
main:has(> .pf-custom-page > .page-block:not(.page-block ~ .page-block):is(.page-block--banner, .page-block--portfolio_hero, .page-block--full_bleed, .page-block--gallery_slideshow)),
main > .pf-custom-page:has(> .page-block:not(.page-block ~ .page-block):is(.page-block--banner, .page-block--portfolio_hero, .page-block--full_bleed, .page-block--gallery_slideshow)) {
  padding-top: 0;
}

/* Uppercase applies to the links in all three layouts, so a photographer who
   picks it does not get a shouty desktop and a quiet drawer. The drawer and
   the overlay are siblings of the header, hence the `~`.

   CASE AND TRACKING ONLY outside the desktop row. Setting a font-size here as
   well is what the first version did, and `.snav--caps ~ .snav-overlay a`
   (0,2,1) out-ranks `.snav-overlay-nav a` (0,1,1): so turning capitals on
   collapsed the overlay's `clamp(1.75rem, 5vw, 3.25rem)` display links to
   .74rem, which is the whole point of that style gone. Each layout keeps the
   size its own layout chose. */
.snav--caps ~ .snav-drawer a,
.snav--caps ~ .snav-overlay a {
  text-transform: uppercase; letter-spacing: .12em;
}
/* Uppercase, tracked-out links read larger than their point size and need to
   come down against it. Four rules used to spell that as .72, .73, .74 and
   .75rem: a 0.5px spread nobody chose, and four more places the theme's own
   scale could not reach. One multiplier, applied to whatever scale the theme
   set, says the same thing and says it once. */
.snav-header.snav--caps .snav-desktop a {
  text-transform: uppercase; letter-spacing: .12em; font-weight: 600;
  font-size: var(--snav-link-caps);
}

/* ── bar: brand left, links right ───────────────────────────────────────── */
/* The neutral baseline: what most themes already do, now available on all of
   them. It sets its properties explicitly. leaning on nav.css's
   defaults, because a photographer switching to `bar` FROM another variant
   must land somewhere predictable. */
.snav-header.snav--bar { border-bottom: 1px solid var(--border); }
.snav--bar .snav-inner { min-height: 64px; }
.snav-brand { font-family: var(--font-display, inherit); }
.snav--bar .snav-brand { font-weight: 700; letter-spacing: -.01em; }
.snav--bar .snav-desktop { gap: 1.75rem; }
/* No font-size: bar is the plain full-width header, so it takes the theme's
   link scale unchanged. It used to pin .88rem, which is a 0.3px difference from
   the base on a theme that declares nothing and a silent override on the four
   that do. */
.snav--bar .snav-desktop a { font-weight: 500; opacity: .7; }
.snav--bar .snav-desktop a:hover,
.snav--bar .snav-desktop a.active { opacity: 1; color: var(--primary-ink, var(--primary)); }

/* ── centered: masthead over a ruled link row ───────────────────────────── */
/* Generalises the magazine theme's masthead to every theme. Only above 1100px:
   stacking two rows on a phone would eat a third of the screen, and below that
   width the drawer and the tab bar are the menu anyway. */
.snav-header.snav--centered { border-bottom: 1px solid var(--border); }
.snav--centered .snav-brand { font-family: var(--font-display); }
@media (min-width: 1100px) {
  .snav--centered .snav-inner {
    flex-direction: column; gap: .5rem; min-height: 0;
    padding-top: 1.25rem; padding-bottom: .5rem;
  }
  .snav--centered .snav-brand {
    /* Bigger than the theme's inline masthead because this one has a line to
       itself, but derived FROM it so a theme with a small wordmark does not
       get a huge one here. */
    font-size: calc(var(--snav-brand-size, 1rem) * 1.35);
    font-weight: 600; letter-spacing: .03em; margin: 0 auto;
  }
  .snav--centered .snav-desktop {
    margin: 0 auto; gap: 1.75rem; width: 100%;
    justify-content: center; padding-top: .4rem;
    border-top: 1px solid var(--border);
  }
  .snav--centered .snav-desktop a {
    text-transform: uppercase; letter-spacing: .1em;
    font-size: var(--snav-link-caps);
    font-weight: 600; opacity: .8;
  }
  /* The CTA would break the symmetry of a centred row if it stayed inline, so
     it centres under it on its own line. */
  .snav--centered .snav-inner > .snav-cta { margin: .5rem auto 0; }
}

/* ── rail: fixed left sidebar ───────────────────────────────────────────── */
/* Generalises the mono theme's sidebar. Above 1100px only; below that the
   header returns to a normal band and the existing drawer / tab bar apply
   unchanged.

   --snav-rail is the single number: the body inset and the shrink of --pf-vw
   (the width full-bleed blocks break out to, see blocks/02-layout.css) are
   both derived from it, so they can never drift apart and leave a banner
   hanging off the right edge. */
.snav-header.snav--rail { border-bottom: 1px solid var(--border); }
@media (min-width: 1100px) {
  body.snav-v-rail {
    --snav-rail: 240px;
    --pf-vw: calc(100vw - var(--snav-rail));
    padding-left: var(--snav-rail);
  }
  .snav-header.snav--rail {
    position: fixed; top: 0; left: 0; bottom: 0; width: var(--snav-rail, 240px);
    border-right: 1px solid var(--border); border-bottom: none; z-index: 50;
    background: var(--bg);
  }
  .snav--rail .snav-inner {
    flex-direction: column; align-items: flex-start; justify-content: flex-start;
    height: 100%; padding: 2.25rem 1.5rem; gap: 0; max-width: none;
  }
  .snav--rail .snav-brand {
    /* Stepped down: the rail is 240px wide and a 1.5rem masthead wraps. */
    font-size: min(var(--snav-brand-size, 1rem), 1.2rem);
    font-weight: 700; margin-bottom: 2.5rem; white-space: normal;
  }
  .snav--rail .snav-desktop {
    flex-direction: column; align-items: flex-start; margin: 0; gap: .9rem;
  }
  .snav--rail .snav-desktop a {
    text-transform: uppercase; letter-spacing: .1em;
    font-size: var(--snav-link-caps);
    font-weight: 600; opacity: .7;
  }
  /* Push the CTA to the bottom of the rail: it is the last thing in the
     column, and `auto` here means "all the leftover height". */
  .snav--rail .snav-inner > .snav-cta { margin: auto 0 0; width: 100%; }
  /* The footer sits inside the inset body; it must not be inset twice. */
  body.snav-v-rail .site-footer { padding-left: 0; }
}

/* ── overlay: hamburger at every width, full-viewport menu ──────────────── */
/* The one variant that changes WHICH layout is used: there is no desktop link
   row at all, so the hamburger is visible from 640px up and opens the panel
   below. Under 640px the bottom tab bar still owns navigation, unchanged.
   snavOpen() in nav.ts picks this path by looking for #snav-overlay. */
.snav-header.snav--overlay { border-bottom: 1px solid var(--border); }
.snav--overlay .snav-brand { font-family: var(--font-display); letter-spacing: .04em; }
@media (min-width: 640px) {
  .snav--overlay .snav-desktop { display: none; }
  .snav--overlay .snav-hamburger { display: flex; align-items: center; margin-left: auto; }
}
.snav-overlay {
  position: fixed; inset: 0; z-index: 300;
  background: var(--bg);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; visibility: hidden;
  transition: opacity .32s ease, visibility .32s;
}
.snav-overlay.open { opacity: 1; visibility: visible; }
/* The wordmark inside the panel, parked where the header put it so opening the
   menu does not move it. Same box as .snav-brand; only the position differs. */
.snav-overlay-brand {
  position: absolute; top: 0; left: 0; right: 0;
  max-width: var(--pf-container, 1200px); margin: 0 auto;
  padding-block: .5rem; padding-inline: var(--pf-gutter, 1.25rem);
  min-height: 60px;
  display: flex; align-items: center; gap: .65rem;
  font-weight: 700; font-size: var(--snav-brand-size, 1rem); letter-spacing: -.01em;
  font-family: var(--font-display);
  color: var(--text); text-decoration: none; white-space: nowrap;
}
.snav-overlay-brand img { height: 32px; width: auto; }
.snav-overlay-brand:hover { text-decoration: none; }

.snav-overlay-close {
  position: absolute; top: 1.25rem; right: 1.5rem;
  background: none; border: none; cursor: pointer; color: var(--text);
  font-size: 1.5rem; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px;
}
.snav-overlay-nav {
  display: flex; flex-direction: column; align-items: center; gap: .35rem;
  max-height: 100vh; overflow-y: auto; padding: 4rem 1.5rem;
}
/* A column flex container shrinks its items before it scrolls, so `overflow-y:
   auto` above never engaged: with seven pages on a 844px phone the items
   absorbed the overflow instead, and the CTA measured 38px against the 44px
   touch floor it sets for itself. Pinning flex-shrink hands the overflow back
   to the scroller, which is what was wanted. */
.snav-overlay-nav > * { flex-shrink: 0; }
.snav-overlay-nav a {
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 5vw, 3.25rem); font-weight: 500; line-height: 1.25;
  letter-spacing: -.01em; color: var(--text); text-decoration: none;
  opacity: .75; transition: opacity .18s, color .18s;
  /* Staggered reveal. `both` holds the start frame through the delay, and the
     animation is bound to `.open` so it replays every time the menu opens
    . once on first paint. */
  display: block; padding: .1rem 0;
}
.snav-overlay-nav a:hover,
.snav-overlay-nav a.active { opacity: 1; color: var(--primary-ink, var(--primary)); }
.snav-overlay.open .snav-overlay-nav > * { animation: snavOverlayIn .45s both cubic-bezier(.2, .7, .2, 1); }
.snav-overlay.open .snav-overlay-nav > :nth-child(1) { animation-delay: .06s }
.snav-overlay.open .snav-overlay-nav > :nth-child(2) { animation-delay: .11s }
.snav-overlay.open .snav-overlay-nav > :nth-child(3) { animation-delay: .16s }
.snav-overlay.open .snav-overlay-nav > :nth-child(4) { animation-delay: .21s }
.snav-overlay.open .snav-overlay-nav > :nth-child(5) { animation-delay: .26s }
.snav-overlay.open .snav-overlay-nav > :nth-child(n+6) { animation-delay: .31s }
.snav-overlay-cta { margin-top: 1.5rem; }
.snav-overlay-nav .snav-nested { justify-content: center; width: 100%; }
.snav-overlay-nav .snav-nested-children { align-items: center; gap: .1rem; margin-top: .15rem; }
.snav-overlay-nav .snav-nested-children a {
  /* Smaller and quieter than the top-level clamp() size, so the submenu
     reads as a level below its parent rather than repeating it verbatim. */
  font-size: clamp(1.05rem, 3.2vw, 1.5rem); opacity: .6;
  min-height: 44px; display: flex; align-items: center;
}
@keyframes snavOverlayIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .snav-overlay.open .snav-overlay-nav > * { animation: none; }
  .snav-overlay { transition: opacity .2s; }
}

/* ── floating: a detached, blurred capsule ──────────────────────────────── */
/* The header leaves the top edge and floats as a pill. It needs no band, so
   the border goes and the page starts underneath it: which is why the body
   gets the top padding instead. Written at 0,4,0 where it collides with the
   theme/motion `.scrolled` rules. */
body[class*="snav-v-"] .snav-header.snav--floating {
  background: transparent; border-bottom: none; box-shadow: none;
  padding: 1rem; pointer-events: none;
}
/* On a phone the same 1rem inset plus a 56px capsule came to 91px of header.
   11% of an 844px screen, against 61-70px for every other style. The inset is
   what makes it read as floating, so it shrinks. goes. */
@media (max-width: 639px) {
  body[class*="snav-v-"] .snav-header.snav--floating { padding: .625rem .75rem; }
  .snav--floating .snav-inner { min-height: 50px; padding-left: 1rem; }
}
.snav--floating .snav-inner {
  /* The capsule is deliberately tighter than the page, but never wider. */
  max-width: min(1100px, var(--pf-container, 1200px)); min-height: 56px; pointer-events: auto;
  /* --snav-glass is the capsule's fill. It defaults to the page's own
     background, and `snav-hero-*` below overrides it with a neutral when the
     capsule is floating on a hero: otherwise a white page over a black hero
     put a flat light-grey pill on the photograph. */
  border-radius: 999px;
  padding: .4rem .5rem .4rem 1.35rem;
  background: var(--snav-glass, color-mix(in srgb, var(--bg) 86%, transparent));
  backdrop-filter: saturate(1.4) blur(14px);
  border: 1px solid var(--border);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .1);
  transition: box-shadow .3s ease, background .3s ease;
}
body[class*="snav-v-"] .snav-header.snav--floating.scrolled { background: transparent; }
.snav--floating.scrolled .snav-inner {
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  box-shadow: 0 14px 38px rgba(0, 0, 0, .16);
}
.snav--floating .snav-brand { font-weight: 700; }
.snav--floating .snav-desktop { gap: 1.4rem; padding-right: .35rem; }
/* No font-size, for the same reason as bar: floating sets its links in
   sentence case, so the theme's own link scale is the right size. The .85rem it
   used to pin was 0.8px off the base and overrode the four themes that ask. */
.snav--floating .snav-desktop a { font-weight: 500; opacity: .72; }
.snav--floating .snav-desktop a:hover,
.snav--floating .snav-desktop a.active { opacity: 1; color: var(--primary-ink, var(--primary)); }
.snav--floating .snav-inner > .snav-cta { border-radius: 999px; }

/* The floating capsule on a hero: a neutral glass, and type to match. */
body.snav-hero-dark .snav--floating.snav--onhero .snav-inner {
  --snav-glass: rgba(18, 18, 20, .52);
  border-color: rgba(255, 255, 255, .22);
}
body.snav-hero-dark .snav--floating.snav--onhero .snav-brand,
body.snav-hero-dark .snav--floating.snav--onhero .snav-desktop a,
body.snav-hero-dark .snav--floating.snav--onhero .snav-hamburger { color: #fff; }
body.snav-hero-light .snav--floating.snav--onhero .snav-inner {
  --snav-glass: rgba(255, 255, 255, .62);
  border-color: rgba(0, 0, 0, .12);
}
body.snav-hero-light .snav--floating.snav--onhero .snav-brand,
body.snav-hero-light .snav--floating.snav--onhero .snav-desktop a,
body.snav-hero-light .snav--floating.snav--onhero .snav-hamburger { color: #14130f; }

/* ── underline: tracked caps with a sliding rule ────────────────────────── */
/* The indicator is a ::after on the link. a separately-positioned
   element: it needs no JS, it cannot fall out of sync with the active page,
   and it survives the desktop row wrapping to a second line (#640). */
.snav-header.snav--underline { border-bottom: 2px solid var(--text); }
.snav--underline .snav-inner { min-height: 68px; }
.snav--underline .snav-brand {
  font-family: var(--font-display);
  /* Uppercase and tracked reads larger than it measures, so this one steps DOWN
     from the theme's size. matching it: but not below 1rem. On
     `frame`, whose wordmark is already the smallest at 1rem, the bare .8 landed
     at 13px against 11.7px links: a brand/link ratio of 1.08, which is not a
     brand, it is one more item in the row. Measured across four themes; frame
     was the only one that fell through, and a floor is what a multiplier on a
     value someone else owns always needed. */
  font-size: max(1rem, calc(var(--snav-brand-size, 1rem) * .8));
  font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
}
/* Tighter than it looks like it should be, because .13em of tracking on the
   links is already doing the separating. At 1.9rem, klasgenoot's seven items
   plus its 354px wordmark measured 1278px inside a 1280px column: one pixel
   of slack: so the row wrapped and the header doubled to 152px. Gaps that sit
   on a cliff edge fall off it the next time anything upstream moves; this one
   fell off a 1px change to the brand size. */
.snav--underline .snav-desktop { gap: 1.25rem; }
.snav--underline .snav-desktop a {
  position: relative;
  text-transform: uppercase; letter-spacing: .13em;
  font-size: var(--snav-link-caps); font-weight: 600; opacity: .62;
}
.snav--underline .snav-desktop a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 10px; height: 2px;
  background: var(--primary);
  transform: scaleX(0); transform-origin: left center;
  transition: transform .28s cubic-bezier(.2, .7, .2, 1);
}
.snav--underline .snav-desktop a:hover,
.snav--underline .snav-desktop a.active { opacity: 1; color: var(--text); }
.snav--underline .snav-desktop a:hover::after,
.snav--underline .snav-desktop a.active::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .snav--underline .snav-desktop a::after { transition: none; }
}

/* ── mono theme: the same rail arithmetic ────────────────────────────────── */
/* mono has had a fixed sidebar since long before menu styles existed, and it
   inset the body without shrinking the bleed width: so every mono site with a
   banner has had the block hanging off the right edge. Same fix, applied here
   because the value lives with the rest of the rail maths.
   Scoped to a mono site that has NOT chosen a menu style: with a variant set,
   mono's own nav CSS no longer applies (the header lost its nav--mono class)
   and leaving the inset would open a 220px gutter beside a normal header. */
@media (min-width: 1100px) {
  body.tpl-mono:not([class*="snav-v-"]) { --pf-vw: calc(100vw - 220px); }
}

/* A page shorter than the viewport used to leave its footer wherever the
   content stopped: on the portal's "access link required or expired" screen
   the rule sat at about 58% of the viewport height with roughly 370px of bare
   ground under it (#1361). No theme sets a body height, so the footer had
   nothing to reach for.

   The body gets a viewport floor and the footer sticks to the bottom of it.
   `main` keeps sizing to its own content and a long page is untouched: the
   floor never caps the box, and a footer already past the fold has nowhere to
   stick to, since sticky is clamped by the body's own bottom edge. It lives
   here rather than in nine theme files because every themed page and every
   portal page loads this stylesheet (shared/_nav.tmpl), and `.pf-main` and
   `body.snav-v-rail .site-footer` already put the page frame here.

   The body stays a block box on purpose. A flex column is the usual way to
   write a sticky footer, and it breaks every page here: `.container` carries
   `margin: 0 auto` in all nine themes, auto inline margins on a flex item turn
   off cross-axis stretch, and `main` then collapses to fit-content. The
   fidelity gate measured the published page's main at 544px on /portfolio and
   at 1600px on the block matrix while the canvas held 1344px. `position:
   sticky` leaves every child's formatting context alone.

   `100dvh` is the unit here because on iOS Safari it counts the address bar
   that is actually on screen; a `100vh` floor pushes the footer a bar's
   height below the fold on first paint. `html { overflow-x: clip }` (every
   theme, line 2) is what keeps sticky working: `clip` does not open a scroll
   container, where `hidden` would and the footer would stick to nothing.

   The builder canvas is excluded. `body.pf-canvas` already pins a min-height
   to the PREVIEWED viewport (`--pb-vh`, _canvas.tmpl) and the iframe is sized
   to its own content, so a second floor on the same document moved every
   vh-sized hero: the gate measured hero/b and hero/c 7-9% off the published
   page at mobile. The canvas has nothing under the footer to push down
   anyway, since the iframe ends where the document does. */
body:not(.pf-canvas) { min-height: 100dvh; }
body:not(.pf-canvas) > .site-footer { position: sticky; top: 100dvh; }

/* ── Header extras ────────────────────────────────────────────────────────
   The <main class="container"> top/bottom padding differs per theme; extracted
   verbatim from the former inline styles on each theme's _header.tmpl. */
.pf-main { padding-top: 2rem; padding-bottom: 1.5rem; }
.pf-main--roomy { padding-top: 2rem; padding-bottom: 3rem; }
.pf-main--tall { padding-top: 2.5rem; padding-bottom: 1.5rem; }

/* Custom-page <article> wrapper (custom_page.tmpl). Base is the common padding;
   the --roomy modifier is the atelier/frame/lumen variant. */
.pf-custom-page { width: 100%; max-width: none; margin: 0; padding: 1rem 0 0; }
.pf-custom-page--roomy { padding: 2rem 0 4rem; }

/* ── Footer ───────────────────────────────────────────────────────────────
   Chrome for the site footer (_footer.tmpl, all themes). De-inlined verbatim
   from the former inline styles; theme tokens (var(--muted)) are reused as-is.
   The columns grid keeps its per-instance column count on the --sf-cols custom
   property since that value is data-driven (len $fc.Columns). */
.sf-container { width: 100%; }
.sf-cols {
  display: grid;
  grid-template-columns: repeat(var(--sf-cols), 1fr);
  gap: 2rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid rgba(127, 127, 127, .15);
  margin-bottom: 1.5rem;
}
.sf-col-title {
  font-size: .75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
  opacity: .5;
  margin-bottom: .6rem;
}
.sf-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .35rem;
}
.sf-link { color: inherit; text-decoration: none; font-size: .87rem; opacity: .75; }
.sf-col-text { font-size: .87rem; opacity: .75; line-height: 1.6; margin: 0; }
.sf-bar {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
}
.sf-bar--center { align-items: center; }
.sf-social { display: flex; align-items: center; gap: .75rem; }
.sf-social-link { display: inline-flex; align-items: center; color: inherit; }
.sf-social-link--muted { color: var(--muted); }
.sf-cta { margin-left: .25rem; }
.sf-cta--muted { color: var(--muted); }
.sf-cta-btn { padding: .45rem 1rem; font-size: .8rem; }
.sf-note { margin-left: .5rem; opacity: .7; font-size: .85rem; }
.sf-name { font-weight: 700; }
.sf-meta { margin-left: .75rem; color: var(--muted); }
.sf-meta-note { margin-left: .75rem; color: var(--muted); font-size: .85rem; }
.sf-serif { font-family: Georgia, 'Times New Roman', serif; font-style: italic; }

/* "Made with profotograaf" attribution (Free + Coffee; white_label removes it).
   Deliberately quiet: it sits below the footer bar on its own hairline rule so
   it reads as a platform mark, not as the photographer's own copy. Inherits the
   theme's colours so it is legible on all nine themes, light and dark, without
   per-theme overrides. */
.sf-madewith {
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid rgba(127, 127, 127, .15);
  text-align: center;
}
.sf-madewith-link {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  justify-content: center;
  gap: .3rem;
  color: inherit;
  text-decoration: none;
  font-size: .78rem;
  letter-spacing: .01em;
  opacity: .55;
  transition: opacity .15s;
  /* Floor the hit box to the app's 44px touch target regardless of the
     .78rem text: padding alone (not a font-size or align-items change)
     keeps the baseline alignment with .sf-madewith-cta and the visual
     weight unchanged. */
  min-height: 44px;
  padding: 11px 0;
}
.sf-madewith-link:hover { opacity: .9; text-decoration: none; }
.sf-madewith-link strong { font-weight: 700; }
/* Free tier gets the CTA tail; on Coffee the badge is the muted line only. */
.sf-madewith--lead .sf-madewith-link { opacity: .72; font-size: .82rem; }
.sf-madewith--lead .sf-madewith-link strong { color: var(--primary-ink, var(--primary)); }
.sf-madewith-cta { opacity: .8; }

/* ══ The site style, expressed on a phone ══════════════════════════════════
   Swapping the layout is half the inheritance. These are the small carries
   that make the phone read as the same menu. a generic one: the
   masthead stays a masthead, the sliding indicator stays the indicator.
   Keyed on `snav-v-<variant>`, so only a site that chose a style gets them. */

/* centered: the masthead is the style, so keep the brand centred .
   letting it fall back to the left-aligned bar below 1100px. The hamburger is
   positioned. pushed by margin, or centring the brand in the
   remaining space would put it off-centre by half the button. */
@media (max-width: 1099px) {
  body.snav-v-centered .snav-inner { justify-content: center; position: relative; }
  body.snav-v-centered .snav-brand { margin: 0; font-family: var(--font-display); }
  body.snav-v-centered .snav-hamburger { position: absolute; right: 0; margin-left: 0; }
}

/* underline: the tracked caps and the indicator both carry down. The bar is
   thicker than a hover underline because at tab size a hairline reads as an
   artefact. a mark. */
@media (max-width: 639px) {
  body.snav-v-underline .snav-tab { position: relative; }
  body.snav-v-underline .snav-tab.active::before {
    content: ""; position: absolute; top: 0; left: 22%; right: 22%;
    height: 2px; background: var(--primary);
  }
  body.snav-v-underline .snav-tab-label {
    /* Uppercase and tracked, at the bar's own size and no smaller. .55rem put
       it at 8.8px: the smallest type anywhere in the product, in caps, on the
       control a phone visitor navigates with. */
    text-transform: uppercase; letter-spacing: .05em;
  }
}

/* floating: the desktop capsule is unconditional, so the phone bar only needs
   to agree with it. Under the dock form that is already true; under the tab bar
   form (an explicit pick) soften the flush edge so the two do not look like two
   different menus stacked on one site. */
@media (max-width: 639px) {
  body.snav-v-floating.snav-m-tabbar .snav-bottom {
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    backdrop-filter: saturate(180%) blur(14px);
  }
}

/* overlay: under the sheet form the panel IS the phone menu, so the type it
   uses at desktop needs relaxing or a 390px screen wraps every item onto two
   lines. The panel's own rules are width-independent.

   `:not(.snav-cta)` is load-bearing. Without it this ties with the button's own
   (0,2,1) rule above and wins on source order, and the CTA came out set in
   2.6rem display type: a button the size of a headline. */
@media (max-width: 639px) {
  body.snav-m-sheet .snav-overlay-nav a:not(.snav-cta) { font-size: clamp(1.6rem, 9vw, 2.6rem); }
}
