/* Design tokens read by every block stylesheet. Values are the literals the blocks used
   before tokenising, so resolved styles are unchanged. Later layers (template, site, page,
   block) override these on a narrower selector. Keep this file first: block CSS is
   concatenated in filename order. */
:root {
  --pf-amber: #f5b53d;
  --pf-black: #000;
  --pf-black-00: rgba(0,0,0,0);
  --pf-black-04: rgba(0,0,0,.04);
  --pf-black-05: rgba(0,0,0,.05);
  --pf-black-06: rgba(0,0,0,.06);
  --pf-black-12: rgba(0,0,0,.12);
  --pf-black-15: rgba(0,0,0,.15);
  --pf-black-16: rgba(0,0,0,.16);
  --pf-black-18: rgba(0,0,0,.18);
  --pf-black-20: rgba(0,0,0,.2);
  --pf-black-22: rgba(0,0,0,.22);
  --pf-black-25: rgba(0,0,0,.25);
  --pf-black-30: rgba(0,0,0,.30);
  --pf-black-35: rgba(0,0,0,.35);
  --pf-black-40: rgba(0,0,0,.4);
  --pf-black-42: rgba(0,0,0,.42);
  --pf-black-45: rgba(0,0,0,.45);
  --pf-black-50: rgba(0,0,0,.5);
  --pf-black-55: rgba(0,0,0,.55);
  --pf-black-60: rgba(0,0,0,.6);
  --pf-black-62: rgba(0,0,0,.62);
  --pf-black-65: rgba(0,0,0,.65);
  --pf-black-66: rgba(0,0,0,.66);
  --pf-black-68: rgba(0,0,0,.68);
  --pf-black-70: rgba(0,0,0,.7);
  --pf-black-74: rgba(0,0,0,.74);
  --pf-black-78: rgba(0,0,0,.78);
  --pf-hairline-dark: rgba(241,245,249,.22);
  --pf-lightbox-bg: rgba(8,8,10,.94);
  --pf-photo-ground: #111;
  /* --pf-radius-k (radius) and --pf-density (spacing) are emitted per request by
     the site Design (sitetheme.Design.TokensCSS) and deliberately NOT declared
     here: the block sheet loads after the theme block, so a declaration here
     would override the site's choice. Every reader carries the fallback 1. */
  --pf-radius-md: calc(.6rem * var(--pf-radius-k,1));
  --pf-slate-55: rgba(15,23,42,.55);
  --pf-slate-60: rgba(15,23,42,.6);
  --pf-slate-82: rgba(15,23,42,.82);
  --pf-slate-85: rgba(15,23,42,.85);
  --pf-white-50: rgba(255,255,255,.5);
  --pf-white-65: rgba(255,255,255,.65);
  --pf-white-70: rgba(255,255,255,.7);
  --pf-white-84: rgba(255,255,255,.84);
  --pf-white-85: rgba(255,255,255,.85);
  --pf-white: #fff;
}
/* Per-block settings (#1898, epic #1917 S2): spacing scale, height tiers and the
   rules that apply a block's own overrides. pages.ResolveBlockSettings emits the
   classes (pb-*, bk-h-*) and the custom properties (--pb-*) on the block wrapper
   from closed allowlists; nothing here reads a free value. Width tiers live in
   02-layout.css next to the width hooks.

   A block with no overrides carries none of these classes, so none of these
   rules touch it. The overrides use !important because the block defaults
   they replace come from rules with far higher specificity (the coloured-band
   breakout in 02-layout.css is six selectors deep) and an explicit choice
   has to win over all of them. */
:root {
  --pf-space-0: 0;
  --pf-space-1: .5rem;
  --pf-space-2: 1rem;
  --pf-space-3: 1.5rem;
  --pf-space-4: 2.5rem;
  --pf-space-5: 4rem;
  --pf-space-6: 6rem;
  --pf-space-7: 8rem;
  --pf-space-8: 12rem;
}

.page-block.pb-mt, .bk-child-settings.pb-mt { margin-top: var(--pb-mt) !important; }
.page-block.pb-mb, .bk-child-settings.pb-mb { margin-bottom: var(--pb-mb) !important; }
.page-block.pb-pt, .bk-child-settings.pb-pt { padding-top: var(--pb-pt) !important; }
/* Side padding stops at a fifth of the viewport per side, so the largest step
   cannot squeeze a block to nothing on a phone (#1917 S7). */
.page-block.pb-pr, .bk-child-settings.pb-pr { padding-right: min(var(--pb-pr), 20vw) !important; }
.page-block.pb-pb, .bk-child-settings.pb-pb { padding-bottom: var(--pb-pb) !important; }
.page-block.pb-pl, .bk-child-settings.pb-pl { padding-left: min(var(--pb-pl), 20vw) !important; }

/* Height tiers: a minimum height as a share of the viewport, the content
   centred in the extra height. The builder canvas clamps each against its own
   --pb-vh in _canvas.tmpl (TestCanvasClampsEveryViewportHeightBlock). */
.page-block[class*="bk-h-"], .bk-child-settings[class*="bk-h-"] { display: grid; align-content: center; }
.bk-h-40 { min-height: 40vh; }
.bk-h-50 { min-height: 50vh; }
.bk-h-60 { min-height: 60vh; }
.bk-h-80 { min-height: 80vh; }
.bk-h-100 { min-height: 100vh; }
/* full_bleed has no height of its own (#2020): its photo box fills the tier the
   wrapper sets, so the Height setting sizes the visible block, not just the
   space around it. */
[class*="bk-h-"] > .bk-full-bleed { min-height: inherit; }

/* Width tiers for a section child (#2204). A top-level block gets these from
   .page-block in 02-layout.css; a child has no .page-block, so its settings box
   carries them. The tiers resolve against the section the child sits in, and
   bleed breaks out of it to the page width as it does at the top level. A
   columns child never carries a width tier (its column sets it). */
.bk-child-settings.bk-w-measure { width: min(var(--w-measure, 820px), 100%) !important; margin-inline: auto !important; }
.bk-child-settings.bk-w-default { width: min(var(--w-default, 1360px), 100%) !important; margin-inline: auto !important; }
.bk-child-settings.bk-w-wide    { width: min(var(--w-wide, 1600px), 100%) !important; margin-inline: auto !important; }
.bk-child-settings.bk-w-bleed   { width: var(--pf-vw,100vw) !important; max-width: none; flex-shrink: 0; margin-left: calc(50% - var(--pf-vw,100vw) / 2) !important; margin-right: 0 !important; }
/* ── Lightbox overlay (inert until .open; activated by the motion IIFE) ────── */
.lb-overlay { position:fixed; inset:0; z-index:9999; display:none; align-items:center; justify-content:center; background:var(--pf-lightbox-bg); }
.lb-overlay.open { display:flex; animation:lb-fade .2s ease; }
@keyframes lb-fade { from { opacity:0; } to { opacity:1; } }
.lb-img { max-width:92vw; max-height:88vh; object-fit:contain; box-shadow:0 20px 60px var(--pf-black-50); }
.lb-btn { position:absolute; background:none; border:none; color:var(--pf-white); cursor:pointer; opacity:.75; transition:opacity .15s; line-height:1; -webkit-tap-highlight-color:transparent; }
.lb-btn:hover { opacity:1; }
.lb-close { top:.5rem; right:1rem; font-size:2.6rem; }
.lb-prev, .lb-next { top:50%; transform:translateY(-50%); font-size:3.5rem; padding:0 1.25rem; }
.lb-prev { left:.5rem; } .lb-next { right:.5rem; }
.page-block--gallery_row img, .page-block--gallery_photos img { cursor:zoom-in; }
@media (max-width:640px){ .lb-prev,.lb-next{ font-size:2.5rem; padding:0 .5rem; } }

/* ── Scroll-reveal (every template) ─────────────────────────────────────────── */
/* The motion IIFE adds .in-view as each block scrolls into view. The whole
   system sits inside no-preference so reduced-motion never hides anything.
   Which also keeps per-theme distance overrides (themes/*.css) from outliving
   a reduce-scoped reveal on specificity and stranding blocks mid-transform.
   html.no-js reveals for crawlers and the script-less demo-preview iframes;
   it outranks the theme overrides (0,2,1 vs 0,2,0), so it always wins. */
@media (prefers-reduced-motion: no-preference) {
  .page-block { opacity: 0; transform: translateY(22px); transition: opacity .75s ease, transform .75s ease; }
  .page-block.in-view { opacity: 1; transform: none; }
  html.no-js .page-block { opacity: 1; transform: none; }
}

/* ── 2026 entrance + living background (motion templates only) ──────────────── */
/* All effects live inside prefers-reduced-motion:no-preference so reduced-motion
   and no-CSS-animation fallbacks keep everything visible: nothing starts hidden
   outside the guarded blocks. Accent-tinted, theme-driven via --primary. */
@media (prefers-reduced-motion: no-preference) {
  /* Living gradient mesh, fixed behind the page, slow accent-tinted drift. */
  .tpl-lumen::before, .tpl-atelier::before, .tpl-frame::before {
    content:""; position:fixed; inset:-25vmax; z-index:-1; pointer-events:none;
    background:
      radial-gradient(38vmax 38vmax at 16% 12%, color-mix(in srgb, var(--primary) 20%, transparent), transparent 60%),
      radial-gradient(36vmax 36vmax at 86% 20%, color-mix(in srgb, var(--primary) 13%, transparent), transparent 62%),
      radial-gradient(46vmax 46vmax at 60% 96%, color-mix(in srgb, var(--primary) 10%, transparent), transparent 60%);
    filter: blur(6px); opacity:.65;
    animation: meshDrift 28s ease-in-out infinite alternate;
    will-change: transform;
  }
  @keyframes meshDrift {
    0%   { transform: translate3d(0,0,0) scale(1); }
    50%  { transform: translate3d(2.5vmax,-2vmax,0) scale(1.06); }
    100% { transform: translate3d(-2vmax,1.5vmax,0) scale(1.03); }
  }
  /* Entrance: the brand "logo" clears in, nav links rise in staggered, once on
     first paint (CSS auto-plays; `both` holds the start frame during the delay). */
  .tpl-lumen .snav-brand, .tpl-atelier .snav-brand, .tpl-frame .snav-brand {
    animation: brandIn 1s .1s both cubic-bezier(.2,.7,.2,1);
  }
  @keyframes brandIn {
    from { opacity:0; transform:translateY(-10px); filter:blur(7px); letter-spacing:.34em; }
    to   { opacity:1; transform:none; filter:none; }
  }
  .tpl-lumen .snav-desktop a, .tpl-atelier .snav-desktop a, .tpl-frame .snav-desktop a {
    animation: navLinkIn .7s both cubic-bezier(.2,.7,.2,1);
  }
  .tpl-lumen .snav-desktop a:nth-child(1), .tpl-atelier .snav-desktop a:nth-child(1), .tpl-frame .snav-desktop a:nth-child(1){ animation-delay:.30s }
  .tpl-lumen .snav-desktop a:nth-child(2), .tpl-atelier .snav-desktop a:nth-child(2), .tpl-frame .snav-desktop a:nth-child(2){ animation-delay:.38s }
  .tpl-lumen .snav-desktop a:nth-child(3), .tpl-atelier .snav-desktop a:nth-child(3), .tpl-frame .snav-desktop a:nth-child(3){ animation-delay:.46s }
  .tpl-lumen .snav-desktop a:nth-child(4), .tpl-atelier .snav-desktop a:nth-child(4), .tpl-frame .snav-desktop a:nth-child(4){ animation-delay:.54s }
  .tpl-lumen .snav-desktop a:nth-child(5), .tpl-atelier .snav-desktop a:nth-child(5), .tpl-frame .snav-desktop a:nth-child(5){ animation-delay:.62s }
  .tpl-lumen .snav-desktop a:nth-child(n+6), .tpl-atelier .snav-desktop a:nth-child(n+6), .tpl-frame .snav-desktop a:nth-child(n+6){ animation-delay:.70s }
  @keyframes navLinkIn { from { opacity:0; transform:translateY(-7px); } to { opacity:1; transform:none; } }
  /* Slow Ken-Burns drift on full-viewport cover-hero imagery. */
  .tpl-lumen .bk-portfolio-hero--cover .bph-bg, .tpl-atelier .bk-portfolio-hero--cover .bph-bg, .tpl-frame .bk-portfolio-hero--cover .bph-bg {
    animation: kenburns 18s ease-out both;
  }
  @keyframes kenburns { from { transform: scale(1.12); } to { transform: scale(1); } }
}
/* Frosted nav once the page scrolls (the motion IIFE toggles .scrolled). */
.tpl-atelier .snav-header, .tpl-frame .snav-header { transition: background .4s ease, box-shadow .4s ease, backdrop-filter .4s ease; }
.tpl-atelier .snav-header.scrolled, .tpl-frame .snav-header.scrolled {
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.2) blur(12px);
  box-shadow: 0 1px 0 var(--border);
}

/* ── Public control reset (#1501 A2) ──────────────────────────────────────── */
/* A form control's UA stylesheet sets its own `font` shorthand, not the
   theme's --font: `.bk-contact-form button[type=submit]`, `.bk-cf2-submit`
   and every input/select/textarea a photographer drops into a block (plus
   the controls inside columns/compare/banner nav) rendered in the browser's
   system font while every letter around them used the theme's Google Font.
   `font`, not just `font-family`: button/input reset size and line-height
   too in the UA sheet, so family alone would still leave the size wrong.
   This file is only ever inlined into rendered PAGE content — the public
   site and the canvas iframe — never into the builder's own React chrome,
   which lives in web/src/theme.css and keeps its own --pb-* tokens; nothing
   here can reach it. */
button, input, select, textarea { font:inherit; }

/* ── Section layout utilities ─────────────────────────────────────────────── */
.bs-flex         { display:flex; }
.bs-dir-col      { flex-direction:column; }
.bs-dir-row      { flex-direction:row; flex-wrap:wrap; }
.bs-gap-none     { gap:0; }
.bs-gap-sm       { gap:.5rem; }
.bs-gap-md       { gap:1rem; }
.bs-gap-lg       { gap:2rem; }
.bs-align-stretch{ align-items:stretch; }
.bs-align-start  { align-items:flex-start; }
.bs-align-center { align-items:center; }
.bs-align-end    { align-items:flex-end; }
.bs-just-start   { justify-content:flex-start; }
.bs-just-center  { justify-content:center; }
.bs-just-end     { justify-content:flex-end; }
.bs-just-between { justify-content:space-between; }
.bs-pad-none     { padding:0; }
.bs-pad-sm       { padding:.75rem; }
.bs-pad-md       { padding:1.5rem; }
.bs-pad-lg       { padding:3rem; }

/* ── Width tiers (top-level blocks own their width) ───────────────────────── */
/* The page article is full-width; each top-level .page-block picks a tier.
   Centered-viewport breakout works through the .container because the block is
   viewport-centred (margin-inline:auto on the contained default).

   Four concentric, centred tiers sharing one centreline: mixing them reads as
   deliberate rhythm. a ragged edge:

     measure  prose, held at a comfortable reading measure
     default  structural blocks (services, process, forms…)
     wide     media: galleries, columns, split, video
     bleed    edge-to-edge

   Width is a property of the block TYPE, assigned below; there is no per-block
   width setting. IMPORTANT: these tier rules must stay in THIS file. Block CSS
   is concatenated in filename order (render.go, sort.Strings), so the
   unconditional bleed for full_bleed (03-blocks-content) and banner
   (07-links-banner), plus every per-variant bleed rule in 08-fullwidth (which
   also out-ranks these on specificity), come after and keep winning. Moving
   these tiers to a higher-numbered file would silently un-bleed those. */
/* The shared _header.tmpl opens `<main class="container">`, and each theme
   caps .container between 1100px and 1400px. A custom page is an <article>
   inside that main, so a block's `100% - gutter` resolves against the CAP, not
   the viewport: no block could ever exceed it, which is why full-bleed has
   always had to hand-roll a 100vw breakout. Release the cap for the one case
   where the page owns its own width; the tiers below then mean what they say.
   Scoped with :has() so ordinary pages keep their container. Where :has() is
   unsupported this simply no-ops back to the old, narrower behaviour.

   Only the cap is released: the container's inline padding stays, because on
   phones it IS the outer gutter (zeroing it drops the margin beside prose from
   ~48px to ~20px). It costs nothing on desktop: the widest tier still fits
   inside the padded box, and the 100vw breakouts are unaffected because they
   measure from the viewport, not the parent. */
main.container:has(> .pf-custom-page) { max-width:none; }

/* One gutter for every tier: the tiers differ ONLY by their cap. That is what
   makes them collapse onto a single edge once the viewport is narrower than the
   smallest cap: on a phone all three land at the same left edge .
   three ragged ones a few pixels apart. */
.pf-custom-page {
  --w-measure:820px; --w-default:1360px; --w-wide:1600px; --w-gutter:3rem;
}

/* --pf-vw: the width a full-bleed block breaks out to.
   ────────────────────────────────────────────────────────────────────────────
   Every bleed rule in this directory is `width:var(--pf-vw,100vw);
   margin-left:calc(50% - var(--pf-vw,100vw)/2)`, never a bare 100vw, and the
   fallback keeps a page that loads no nav CSS behaving exactly as before.

   It is a variable because a menu can take width away from the document. The
   `rail` menu style (and the mono theme, which has had a fixed sidebar since
   long before menu styles existed) insets the body by the rail's width. The
   old formula centred on the NARROWED container while still measuring 100vw,
   so a bleed block slid under the rail on the left and ran off the screen on
   the right: visible on any mono site with a banner today. nav.css shrinks
   --pf-vw by the same amount it insets the body, and the arithmetic lands. */
.page-block {
  width:min(var(--w-default,1360px), 100% - var(--w-gutter,3rem));
  margin-inline:auto; box-sizing:border-box; margin-bottom:calc(2.5rem * var(--pf-density,1));
  /* Per-block width tier (#1898, pages.ResolveBlockSettings emits the class).
     !important because the type tiers below and the coloured-band breakout
     further down out-rank a lone class, and an explicit choice must win. The
     non-bleed tiers re-centre with margin-inline:auto so a block that
     bleeds by default (banner, full_bleed, a coloured band) comes back in. */
  &.bk-w-measure { width:min(var(--w-measure,820px), 100% - var(--w-gutter,3rem)) !important; margin-inline:auto !important; }
  &.bk-w-default { width:min(var(--w-default,1360px), 100% - var(--w-gutter,3rem)) !important; margin-inline:auto !important; }
  &.bk-w-wide    { width:min(var(--w-wide,1600px), 100% - var(--w-gutter,3rem)) !important; margin-inline:auto !important; }
  &.bk-w-bleed   { width:var(--pf-vw,100vw) !important; margin-left:calc(50% - var(--pf-vw,100vw)/2) !important; margin-right:0 !important; }
}

/* Wide tier: imagery earns the space.
   `image` is deliberately absent: its size IS its variant (a small · b medium ·
   c large · d bleed, see 06-schemes-variants), so the photographer already
   chooses. A tier here would be out-ranked by those variant rules anyway. */
.page-block--gallery_photos, .page-block--gallery_list, .page-block--gallery_row,
.page-block--gallery_cover, .page-block--gallery_slideshow, .page-block--columns,
.page-block--split, .page-block--video, .page-block--hero, .page-block--stats,
.page-block--sticky_list, .page-block--compare, .page-block--collage,
.page-block--blog_posts, .page-block--child_pages {
  width:min(var(--w-wide,1600px), 100% - var(--w-gutter,3rem));
}
/* The BLEED tier is not assigned here, because it is not a cap: full_bleed
   (03-blocks-content), banner (07-links-banner), marquee (11-marquee) and
   cta_band (08-fullwidth) each take the 100vw breakout in their own file, which
   is ordered after this one so it wins. Look there before concluding a type has
   no tier. */

/* `testimonials` deliberately sits at the default tier: its cards
   stretch to fill the row, and a typical two-or-three-quote set blown out to
   800px-per-card reads sparse. generous. Its bk-variant-b remains
   the full-width option for anyone who wants it. */

/* Measure tier: prose reads better pulled in than stretched out. */
.page-block--text, .page-block--quote, .page-block--statement,
.page-block--callout, .page-block--faq {
  width:min(var(--w-measure,820px), 100% - var(--w-gutter,3rem));
}
/* …unless the block carries a GROUND (#1312). The measure tier exists for
   readability, which only matters while the block's background is
   transparent (the `light` scheme, the only one this cap still applies to
   below). Once a block is tinted, filled or dark, its band is what the eye
   reads as one shape with its neighbours: a coloured rectangle that stops a
   third of the way across the row reads as a mistake, not a design choice,
   next to a full-bleed statement band on the same palette. Promote the band
   itself to the default tier; `.bk-text`/`.bk-statement` already cap their
   own copy at a prose measure independently of the wrapper (72ch / 820px).
   `.bk-quote`/`.bk-callout`/`.bk-faq` get the same self-cap just below,
   scoped to this same non-light case, so their box doesn't stretch across
   the wider band.
   `:not(.bk-cs-light)` (a class) makes this (0,2,1): more specific than the
   plain rule above, so it wins regardless of source order. */
.page-block--text:not(.bk-cs-light),
.page-block--quote:not(.bk-cs-light),
.page-block--statement:not(.bk-cs-light),
.page-block--callout:not(.bk-cs-light),
.page-block--faq:not(.bk-cs-light) {
  width:min(var(--w-default,1360px), 100% - var(--w-gutter,3rem));
}
/* The self-cap below matches each block's own root element directly
   (`.bk-quote`/`.bk-callout`/`.bk-faq`/`.bk-statement`/`.bk-text`, all now
   carrying their own `bk-cs-*`, block_render_registry.go), not through the
   `.page-block--<type>` ancestor above (#1422, following the portfolio_bio
   fix for the same pattern in #1421/#1411). A `columns` child never gets that
   wrapper at all — render_block's "columns" branch recurses straight into
   `.bk-col` — so the ancestor-combinator form used to leave a scheme'd block
   nested in a columns block with no self-cap at all: its box then stretched
   to the column's own width with no ground beneath the padding it does keep
   from its unconditional base rule. */
.bk-quote:not(.bk-cs-light),
.bk-callout:not(.bk-cs-light),
.bk-faq:not(.bk-cs-light) {
  max-width:820px;
  margin-inline:auto;
}
/* #1312 promoted the BAND to the default tier and left the capped copy inside
   it pinned to the left edge: on a 1360px band a 72ch/820px column of prose
   leaves the rest of the band as flat colour with nothing in it (#1352
   section B). `.bk-statement` centres unconditionally at its own rule
   (03-blocks-content.css, #1449: was `:not(.bk-cs-light)` here, so the light
   scheme and any wide band, e.g. a `section` or a `columns` child, rendered
   it off-centre); the author's own text-align (left/center/right) only
   controls how the prose reads inside that column, never the column's own
   position in the band. */
/* .bk-text can hold a markdown "---" divider (goldmark renders it as <hr>),
   and a rule drawn inside a band has to span the band, not the paragraph it
   separates: centering the whole .bk-text column like .bk-statement would
   drag the <hr> in to 72ch too, so it would stop in the middle of the
   band instead of running its full width. Leave the wrapper at the band's own
   width and cap+centre every child except <hr>, so prose reads at 72ch while
   a divider underneath it spans what the eye reads as one band. */
.bk-text:not(.bk-cs-light) {
  max-width:none;
}
.bk-text:not(.bk-cs-light) > :not(hr) {
  max-width:72ch;
  margin-inline:auto;
}

/* Vertical rhythm. Media blocks carry no internal padding of their own (most
   other blocks: statement, services, faq…: self-space), and the wider they
   run the more air they need to not feel crammed against their neighbours. */
.page-block--gallery_row, .page-block--split, .page-block--image,
.page-block--gallery_photos, .page-block--gallery_cover, .page-block--columns {
  margin-block:calc(clamp(1.75rem, 3.5vw, 3.5rem) * var(--pf-density,1));
}
/* Page title aligns to the default tier */
article > h1 { max-width:min(var(--w-default,1360px), 100% - var(--w-gutter,3rem)); margin-inline:auto; }

/* ── Coloured bands (#1501 A2) ─────────────────────────────────────────────
   A block's colour scheme (bk-cs-*, 06-schemes-variants.css) paints the
   `.page-block` wrapper's own background, and the wrapper before this rule
   was still capped to the block's width TIER: a coloured rectangle that
   stops a gutter short of the viewport, beside `cta_band` and `full_bleed`
   which already bleed edge to edge, reads as a mistake next to them, not a
   deliberate choice. Give every non-light scheme the same `--pf-vw` breakout
   those two already use, pull the CONTENT back in with inline padding sized
   to the block's own tier (so copy still reads at its usual measure), and
   move the vertical spacing that used to come from `.page-block`'s own
   margin-bottom into padding-block instead, the way contact_form and
   cta_band already do it — a band needs its own top/bottom edge, not a gap
   that sits outside its background and shows the page through.

   Excluded: `cta_band`, `banner`, `marquee` and `full_bleed` already bleed
   unconditionally in their own files with their own inner-padding scheme
   (07/08/11, and full_bleed in 03), so this would only fight them over the
   same pixels; `contact_form` already puts its own padding on this exact
   wrapper (08-fullwidth.css) for every variant, scheme or not; and
   `portfolio_hero` already bleeds unconditionally with its own content
   padding (04-portfolio.css). A block variant that ALSO bleeds unconditionally
   regardless of scheme (statement b, callout b, services b, process b, faq d,
   video b, gallery_list b, link_columns d, section b, hero d/e, split d, text
   c, quote d/e) sets its own padding on its INNER element instead of this
   wrapper (08-fullwidth.css), and re-zeroes this rule's wrapper padding right
   there so the two padding schemes don't add up. */
.page-block:not(.bk-cs-light):not(.page-block--cta_band):not(.page-block--banner):not(.page-block--marquee):not(.page-block--full_bleed):not(.page-block--contact_form):not(.page-block--portfolio_hero) {
  width:var(--pf-vw,100vw);
  margin-left:calc(50% - var(--pf-vw,100vw)/2);
  margin-right:0;
  padding-block:calc(clamp(2.5rem,6vw,5rem) * var(--pf-density,1));
  padding-inline:max(var(--w-gutter,3rem), calc((var(--pf-vw,100vw) - var(--w-default,1360px))/2));
}
/* Wide-tier types keep their own wider measure inside the same bleed. */
.page-block--gallery_photos:not(.bk-cs-light), .page-block--gallery_list:not(.bk-cs-light),
.page-block--gallery_row:not(.bk-cs-light), .page-block--gallery_cover:not(.bk-cs-light),
.page-block--gallery_slideshow:not(.bk-cs-light), .page-block--columns:not(.bk-cs-light),
.page-block--split:not(.bk-cs-light), .page-block--video:not(.bk-cs-light), .page-block--hero:not(.bk-cs-light),
.page-block--stats:not(.bk-cs-light), .page-block--sticky_list:not(.bk-cs-light), .page-block--compare:not(.bk-cs-light),
.page-block--collage:not(.bk-cs-light), .page-block--blog_posts:not(.bk-cs-light), .page-block--child_pages:not(.bk-cs-light) {
  padding-inline:max(var(--w-gutter,3rem), calc((var(--pf-vw,100vw) - var(--w-wide,1600px))/2));
}
/* The band above keeps `.page-block`'s ordinary `margin-bottom:2.5rem`
   (02-layout.css base rule), so a coloured band still sits a normal gutter
   from whatever comes next, same as an uncoloured block would. Two bands of
   the SAME scheme back to back are the one case that gutter is wrong for:
   a visible gap of page background between them reads as two separate
   colours nearly matching by accident, not one continuous band. Zero the
   FIRST one's margin so they touch; each keeps its own padding-block, so the
   seam simply doesn't exist rather than being a thin light-coloured line. */
.page-block.bk-cs-light-accent:has(+ .page-block.bk-cs-light-accent),
.page-block.bk-cs-accent:has(+ .page-block.bk-cs-accent),
.page-block.bk-cs-dark-accent:has(+ .page-block.bk-cs-dark-accent),
.page-block.bk-cs-dark:has(+ .page-block.bk-cs-dark) {
  margin-bottom:0;
}

/* ── Block types ──────────────────────────────────────────────────────────── */
.bk-hero                 { text-align:center; padding:2rem 0 1rem;
  & h2 { font-size:clamp(1.5rem,4vw,2.5rem); margin:0 0 .75rem; font-weight:700; }
  & p  { color:var(--muted); margin:0 0 1.25rem; max-width:42rem; margin-inline:auto; }
}

.bk-text                 { line-height:1.7; max-width:72ch;
  & p { margin:0 0 1rem; }
}

/* `width:100%` here is a no-op at top level (an image block's own variant
   rule already sizes the WRAPPER div, e.g. #1501 A3's `.page-block--image.
   bk-variant-b`, and a plain block box fills its container regardless), but
   it matters the one place `.bk-image` renders with no such wrapper: as a
   section or columns child (renderSectionChild/the columns case in
   _blocks.tmpl both skip the wrapper for a nested block, #1316/#1485's
   comment above). There, `.bk-image` is a flex item with `width:auto`, and
   flexbox gives `margin-inline:auto` priority over `align-items:stretch`: an
   auto margin on the cross axis absorbs the free space itself rather than
   letting stretch fill it, so the figure shrank to its image's own content
   size and centered there - a few hundred px of blank band on either side
   of a postage-stamp photo. `width:100%` leaves no free space for the auto
   margins to absorb, which is what actually fixes it (#1511 follow-up). */
.bk-image                { margin:0; margin-inline:auto; width:100%;
  & img { width:100%; display:block; border-radius:calc(.5rem * var(--pf-radius-k,1)); }
  & figcaption { text-align:center; color:var(--muted); margin-top:.5rem; font-size:.9rem; }
}

.bk-quote                { border-left:4px solid var(--primary); margin:0; padding:.75rem 1.25rem;
                           background:var(--card); border-radius:0 calc(.5rem * var(--pf-radius-k,1)) calc(.5rem * var(--pf-radius-k,1)) 0;
  & p { margin:0 0 .5rem; font-style:italic; font-size:1.1rem; }
  & footer { color:var(--muted); font-size:.9rem; margin-top:.5rem; font-style:normal; }
}

.bk-callout              { border:1px solid var(--border); border-radius:calc(.75rem * var(--pf-radius-k,1));
                           padding:1.25rem 1.5rem; background:var(--card);
                           display:flex; gap:1rem; align-items:flex-start;
  & .bk-icon { font-size:1.5rem; line-height:1.4; flex-shrink:0; }
  & h3 { margin:0 0 .4rem; font-size:1.05rem; }
  & p { margin:0; color:var(--muted); }
}

.bk-gallery-row          { display:grid; gap:.5rem;
  & img { width:100%; aspect-ratio:1; object-fit:cover; display:block; border-radius:calc(.25rem * var(--pf-radius-k,1)); }
  /* A captioned item (#2432) wraps its photo in a figure; the grid cell stays one item. */
  & .bk-gallery-row-item { margin:0; break-inside:avoid; }
  & .bk-gallery-row-item figcaption { margin-top:.35rem; font-size:.85rem; opacity:.75; }
}

.bk-faq                  { display:flex; flex-direction:column; gap:1rem; list-style:none; margin:0; padding:0;
  & > div { border:1px solid var(--border); border-radius:var(--pf-radius-md); padding:1rem 1.25rem; }
  & dt { font-weight:600; margin:0 0 .4rem; }
  & dd { margin:0; color:var(--muted); }
}

/* auto-fit (not auto-fill) so N cards always fill the full content width as N
   equal columns: no empty trailing track that leaves them left-of-centre. */
.bk-services             { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:1.5rem; margin:.5rem 0 3rem; }
.bk-services-card        { border-radius:calc(.9rem * var(--pf-radius-k,1)); padding:2rem 1.75rem; }
.bk-services-name        { font-size:1.1rem; font-weight:700; margin-bottom:.35rem; }
.bk-services-price       { font-size:1.9rem; font-weight:800; color:var(--primary-ink-lg); margin:.6rem 0 1rem; }
.bk-services-desc        { font-size:.9rem; color:var(--muted); line-height:1.6; }

/* ── Stats: a trust strip of value/label pairs ────────────────────────────── */
.bk-stats                { margin:1rem 0 2.5rem; }
.bk-stats-heading        { text-align:center; font-family:var(--font-display); font-size:clamp(1.4rem,3vw,2rem); font-weight:700; margin:0 0 2rem; }
.bk-stats-grid           { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:1.5rem 1rem; text-align:center; }
.bk-stat                 { padding:.5rem; }
.bk-stat-value           { font-family:var(--font-display); font-size:clamp(2.25rem,5vw,3.5rem); font-weight:800; line-height:1; color:var(--primary-ink-lg); letter-spacing:-.02em; }
.bk-stat-label           { margin-top:.5rem; font-size:.8rem; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); }
/* The variant lives on .bk-stats itself (bk-stats--<v>), clear of the
   .page-block--stats wrapper: a columns child never gets that wrapper, so
   rules keyed on it were dead there (#686, mirrors the compare fix in #679). */
/* A viewport-wide breakout plus a `margin-inline:auto` re-centre: the original two-element
   shape had the auto-centering on a SEPARATE inner element, so it never met
   the escape margins below. Collapsed onto one element, `margin-inline` is a
   margin-left/margin-right SHORTHAND: declared after the explicit ones it
   silently overwrote them back to `auto auto`, cancelling the escape and
   overflowing the viewport by the container's own side gutter (#686 fixed the
   scoping; this is the box-model bug that shipped inside that same fix,
   caught by `visual:gate`'s public-blocks/block-matrix overflow findings).
   `padding-inline` caps the content the same way `max-width` + auto-margin
   did, without touching the escape margins at all. */
.bk-stats--b {
  width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; margin-top:0; margin-bottom:0;
  background:var(--card);
  padding-block:3rem; padding-inline:max(clamp(1.5rem,6vw,6rem), calc((100% - 1500px) / 2));
}
/* In a columns cell the breakout above has nothing to break out of: a
   viewport-wide element centred on a narrow cell runs far past it, over the
   neighbouring columns and off the cell's own ground (#1976). The band fills
   the cell instead, keeping its card ground and vertical padding. */
.bk-col :is(.bk-stats--b,.bk-testimonials--b) {
  width:auto; margin-left:0; margin-right:0; padding-inline:clamp(1rem,3vw,2rem);
}
/* Boxed: each stat sits in its own bordered card */
.bk-stats--c .bk-stats-grid { gap:1rem; }
.bk-stats--c .bk-stat { border:1px solid var(--border); border-radius:calc(1rem * var(--pf-radius-k,1)); background:var(--card); padding:1.75rem 1rem; }

/* ── Testimonials: client quotes with author/role/avatar ──────────────────── */
.bk-testimonials         { margin:1.5rem 0 2.5rem; }
.bk-testimonials-grid    { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:1.5rem; }
.bk-testimonial          { margin:0; padding:2rem 1.85rem; border:1px solid var(--border); border-radius:calc(1rem * var(--pf-radius-k,1)); background:var(--card); display:flex; flex-direction:column; gap:1.25rem; }
.bk-testimonial-quote    { margin:0; font-size:1.075rem; line-height:1.65; font-style:italic;
  &::before { content:"\201C"; color:var(--primary); font-size:1.4em; font-weight:700; margin-right:.1em; }
}
.bk-testimonial-cite     { display:flex; align-items:center; gap:.85rem; font-style:normal; margin-top:auto; }
.bk-testimonial-avatar   { width:44px; height:44px; border-radius:50%; object-fit:cover; flex-shrink:0; }
.bk-testimonial-author   { font-weight:700; font-size:.95rem; }
.bk-testimonial-role     { font-size:.82rem; color:var(--muted); }
/* The variant lives on .bk-testimonials itself (bk-testimonials--<v>), not on
   the .page-block--testimonials wrapper: a columns child never gets that
   wrapper, so rules keyed on it were dead there (#686, mirrors #679). */
/* Same box-model fix as .bk-stats--b above: padding-inline caps the content
   width without a conflicting `margin-inline:auto` cancelling the escape
   margins declared on this same element. */
.bk-testimonials--b {
  width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  padding-inline:max(clamp(1.5rem,6vw,6rem), calc((100% - 1500px) / 2));
}
/* Cards: lift each quote onto an elevated accent-topped card */
.bk-testimonials--c .bk-testimonial { border:none; box-shadow:0 10px 30px -12px var(--pf-black-22); border-top:3px solid var(--primary); }

.bk-divider              { border:none; border-top:1px solid var(--border); margin:1rem 0; }
.bk-divider-space        { height:1.5rem; }
/* A divider's margin settings (pb-mt, pb-mb) are the WHOLE gap around it. Left
   alone they would be swallowed by margin collapsing: the rule's own 1rem
   margin and the previous block's bottom margin both sit next to the wrapper's,
   and the largest wins, so any step below that did nothing. With a step chosen,
   the rule gives up its own margin on that side (unbanded dividers only: inside
   a coloured band the margin is part of the band) and the block above gives up
   its bottom margin. Nothing here applies to a divider with no settings. The
   :where() keeps the previous-block rule below a block's own pb-mb. */
.page-block--divider.bk-cs-light.pb-mt > .bk-divider { margin-top:0; }
.page-block--divider.bk-cs-light.pb-mb > .bk-divider { margin-bottom:0; }
.page-block:where(:has(+ .page-block--divider.pb-mt)) { margin-bottom:0 !important; }

.bk-video                { margin:0; display:block;
  & figcaption { text-align:center; color:var(--muted); margin-top:.5rem; font-size:.9rem; }
}
.bk-video-ratio          { position:relative; padding-bottom:56.25%; height:0;
                           overflow:hidden; border-radius:var(--pf-radius-md);
  & iframe { position:absolute; top:0; left:0; width:100%; height:100%; }
}

.page-block--full_bleed  { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }
.bk-full-bleed           { position:relative; display:flex; align-items:center;
                           overflow:hidden; margin:0; }
.bk-full-bleed-img       { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.bk-full-bleed-overlay   { position:absolute; inset:0; }
.bk-full-bleed-content   { position:relative; z-index:1; padding:2rem; max-width:700px; }
.bk-full-bleed-heading   { font-size:clamp(2rem,6vw,4.5rem); font-weight:700; line-height:1.05;
                           margin:0 0 .75rem; color:var(--pf-white); }
.bk-full-bleed-subtext   { font-size:1.1rem; opacity:.85; margin:0 0 1.5rem; color:var(--pf-white); }
.bk-full-bleed-cta       { display:inline-block; padding:.75rem 2rem; background:var(--primary);
                           color:var(--on-primary,#fff); border-radius:calc(.4rem * var(--pf-radius-k,1)); font-weight:600; text-decoration:none; }

.bk-split                { display:grid; grid-template-columns:1fr 1fr; gap:clamp(2rem,4vw,5rem); align-items:center; }
/* The 3/4 portrait is right at a narrow measure, but the split runs at the wide
   tier: unchecked, a ~770px column would render a ~1030px-tall image that
   dwarfs the text beside it. width:100% keeps the box filling its column while
   max-height clamps the drop (aspect-ratio yields once both are constrained;
   without the explicit width the ratio shrinks the box to ~465px and opens a
   dead gap beside the text); object-fit does the cropping. */
.bk-split-img            { overflow:hidden; width:100%; aspect-ratio:3/4; max-height:min(70svh, 620px); border-radius:calc(.5rem * var(--pf-radius-k,1));
  & img { width:100%; height:100%; object-fit:cover; display:block; }
}
.bk-split-img--empty     { background:var(--card); }
.bk-split-body           { display:flex; flex-direction:column; justify-content:center; }
.bk-split-heading        { margin:0 0 1rem; }
.bk-split-text           { color:var(--muted); }
.bk-split-cta            { margin-top:1.5rem; }
@media(max-width:640px)  { .bk-split { grid-template-columns:1fr; } }

.bk-statement            { padding:2rem 0 1rem; max-width:820px; margin-inline:auto; }
/* When a statement is the page heading (first block) it doesn't need the full
   top padding on top of the page's own padding: tighten it so the heading
   doesn't float in a large void below the nav. (Blocks are the first DIVs in
   the article, after an injected <style>.) */
.page-block:first-of-type .bk-statement { padding-top:.5rem; }
.bk-statement-label      { font-size:.75rem; font-weight:700; letter-spacing:.12em;
                           text-transform:uppercase; color:var(--primary-ink); margin-bottom:.75rem; }
.bk-statement-heading    { font-size:clamp(1.75rem,4vw,3rem); font-weight:800;
                           line-height:1.1; letter-spacing:-.02em; }

.bk-process              { }
.bk-process-intro        { color:var(--muted); margin-bottom:2rem; font-size:1.05rem; }
.bk-process-steps        { display:grid; gap:1.75rem; }
.bk-process-step         { display:grid; grid-template-columns:2.5rem 1fr; gap:1.25rem; align-items:start; }
.bk-process-num          { width:2.5rem; height:2.5rem; border-radius:50%; background:var(--primary);
                           color:var(--on-primary,#fff); display:flex; align-items:center; justify-content:center;
                           font-weight:700; font-size:1rem; flex-shrink:0; }
.bk-process-step-heading { display:block; margin-bottom:.25rem; font-size:1rem; }
.bk-process-step-body    { color:var(--muted); font-size:.9rem; line-height:1.55; }

.bk-contact-form                    { display:flex; flex-direction:column; gap:1rem; max-width:36rem; width:100%; margin-inline:auto;
  & label { display:flex; flex-direction:column; gap:.35rem; }
  & input,
  & textarea { padding:.6rem .8rem; border-radius:calc(.4rem * var(--pf-radius-k,1)); min-height:44px;
               border:1px solid var(--border); background:var(--card);
               color:var(--text); font-family:inherit; }
  & button[type=submit] { display:inline-flex; align-items:center; justify-content:center; min-height:44px;
                          background:var(--primary); color:var(--on-primary,#fff); border:none;
                          padding:.65rem 1.5rem; border-radius:calc(.4rem * var(--pf-radius-k,1));
                          font-size:1rem; cursor:pointer; font-weight:600; }
}
.bk-contact-form-alt                { margin-top:2rem; color:var(--muted); max-width:36rem; width:100%; margin-inline:auto;
  & a { display:inline-flex; align-items:center; min-height:44px; }
}
.bk-contact-form-success            { padding:1rem 1.25rem; border-radius:calc(.5rem * var(--pf-radius-k,1));
                                      background:color-mix(in srgb,var(--primary) 10%,var(--card));
                                      border:1px solid color-mix(in srgb,var(--primary) 25%,transparent); }

/* ── sticky_list: pinned intro beside a scrolling list ─────────────────────── */
/* Layout first, effect second: this reads as a plain two-column band with no JS,
   under reduced motion, and in the editor. blocks.ts adds .bk-sl--reveal only
   when it takes the effect over, so every rule that HIDES or DIMS anything is
   scoped to that class: nothing here can strand content. */
.bk-sl {
  display:grid; grid-template-columns:1fr 1fr; gap:clamp(2rem,5vw,5rem);
  align-items:start;
  /* Generous vertical air is essential: the pin runway IS the block's
     height minus the intro, so a cramped block leaves no scroll distance for
     the words to light through and the effect snaps on. */
  padding-block:clamp(2rem,7vw,7rem);
}
.bk-sl-intro {
  /* top: the pin offset. Sticky needs no JS: the browser does it. JS only
     drives the word-lighting and the fade, so a no-JS visitor still gets a
     correctly pinned column. */
  position:sticky; top:18vh;
  opacity:var(--sl-o,1);
}
.bk-sl-heading {
  font-size:clamp(2rem,4.6vw,4.2rem); line-height:1.04; letter-spacing:-.02em;
  font-weight:700; margin:0; text-wrap:balance;
}
.bk-sl-body { margin:1.25rem 0 0; color:var(--muted); max-width:38ch; }

.bk-sl-list { list-style:none; margin:0; padding:0; display:grid; gap:clamp(2rem,4vw,3.5rem); }
.bk-sl-item { display:grid; grid-template-columns:auto 1fr; gap:1.25rem; align-items:start; }
.bk-sl-num {
  font-size:clamp(2.2rem,4vw,3.6rem); line-height:.9; font-weight:700;
  font-variant-numeric:tabular-nums; color:var(--muted);
}
.bk-sl-item-heading { margin:0 0 .4rem; font-size:1.3rem; font-weight:700; line-height:1.2; }
.bk-sl-item-body { margin:0; color:var(--muted); }

.bk-sl-cta-wrap { margin-top:clamp(2rem,4vw,3.5rem); display:grid; gap:.75rem; justify-items:center; }
.bk-sl-cta {
  display:inline-block; padding:.9rem 2.25rem; border-radius:999px;
  /* --on-primary, not --bg: the server picks black-or-white per theme by
     measured contrast against the accent (onPrimaryColor, render.go). --bg
     happens to work on a dark accent and fails on a light one: atelier's dusty
     pink on cream measured 2.67:1, well under AA's 4.5:1. */
  background:var(--primary); color:var(--on-primary,#fff); font-weight:700;
  letter-spacing:.08em; text-transform:uppercase; font-size:.85rem;
  text-decoration:none;
}
.bk-sl-cta:hover { text-decoration:none; filter:brightness(1.08); }
.bk-sl-note { margin:0; font-size:.85rem; color:var(--muted); }

/* Word-by-word darkening. Only ever applies once JS has opted in; the words are
   in the server HTML either way, so crawlers and no-JS readers get the sentence
   at full contrast.
   The resting tone used to mix in only 25% of currentColor (#1486): reader
   contrast for a word the scroll hasn't reached yet fell as low as 1.5:1,
   under the 3:1 large-text floor these words qualify for, on every scheme.
   70% clears that floor on every scheme's own ink/ground pair (the tightest
   is the accent scheme, where on-primary itself only clears ~4.7:1 against
   the brand colour). The lighter weight carries the rest of the "not read
   yet" signal, so the reveal still reads clearly even with contrast held
   above the floor. */
.bk-sl--reveal .bk-sl-w { color:color-mix(in srgb, currentColor 70%, transparent);
                           font-weight:500; transition:color .25s ease, font-weight .25s ease; }
.bk-sl--reveal .bk-sl-w.is-lit { color:currentColor; font-weight:700; }

/* Variants: a = numbered (default) · b = bullets · c = plain */
.page-block--sticky_list.bk-variant-b .bk-sl-num { font-size:1rem; line-height:1.5; }
.page-block--sticky_list.bk-variant-c .bk-sl-num { display:none; }
.page-block--sticky_list.bk-variant-c .bk-sl-item { grid-template-columns:1fr; }

@media (prefers-reduced-motion: reduce) {
  /* No word-dimming and no scroll-driven fade: the pin itself is not motion,
     so it stays. */
  .bk-sl--reveal .bk-sl-w, .bk-sl--reveal .bk-sl-w.is-lit { color:currentColor; font-weight:700; transition:none; }
  .bk-sl-intro { opacity:1; }
}

@media (max-width:820px) {
  /* Single column: a pinned intro above a list you scroll is a hijacked screen
     on a phone, so unpin entirely. */
  .bk-sl { grid-template-columns:1fr; gap:2.5rem; }
  .bk-sl-intro { position:static; opacity:1; }
  .bk-sl--reveal .bk-sl-w { color:currentColor; font-weight:700; }
}

/* ── compare: the before/after handle ─────────────────────────────────────
   Layout first, behaviour second, like sticky_list. Both photos and a working
   native range are in the server HTML: with no JS the block still shows a
   comparison at the author's --pos and the range still moves it (the browser
   updates value, and the clip follows because JS is the only thing that would
   need to translate one to the other: which is why blocks.ts sets --pos from
   `input`, and why the SERVER renders value and --pos to the same number).
   Nothing here hides content on its own; .bk-compare--eased is the one class JS
   adds, and all it does is turn the transition on. */
.bk-compare { display:grid; gap:1rem; }
.bk-compare-heading {
  margin:0; font-size:clamp(1.35rem,2.6vw,2rem); line-height:1.15;
  font-weight:700; text-wrap:balance;
}
.bk-compare-caption { margin:0; color:var(--muted); font-size:.95rem; }

.bk-compare-frame {
  position:relative; overflow:hidden; border-radius:calc(.5rem * var(--pf-radius-k,1));
  /* A fixed ratio: the two frames are stacked, so without
     one the taller image would decide the box and the shorter would letterbox
     against it. 3:2 is the ratio most cameras hand over. */
  aspect-ratio:3/2; background:var(--card);
  /* The frame is the drag surface, so it has to claim ONE axis from the
     browser: but only one. `touch-action:none` claims both, and a 3:2 frame is
     most of a phone screen: a visitor who starts a scroll on the photo then
     cannot scroll the page at all (measured at 390: the swipe moved nothing).
     The horizontal wipe needs the X axis and gives back Y; the vertical wipe
     (below) swaps them; side-by-side has no drag and gives back both. */
  touch-action:pan-y;
  /* Same gesture, the other half: a drag that starts on a photo must not begin
     a native image drag or a text selection. draggable="false" on the <img> is
     the other half, and preventDefault() in blocks.ts the third. */
  user-select:none; -webkit-user-select:none;
}
.bk-compare-img {
  position:absolute; inset:0; width:100%; height:100%;
  -webkit-user-drag:none;
  /* No focal declaration here: not even in a comment. Centre is already the
     default, and TestGalleryMosaicTilesCarryPerPhotoFocal counts that property
     name across the WHOLE rendered document, block CSS included, to prove a
     photo without a focal point gets no crop hint. The real focal, when the
     asset has one, arrives as an inline style from focalStyleAttr. */
  object-fit:cover; display:block;
}
/* The empty side of a half-filled block. A styled panel, never an <img> with no
   src: an empty src is a BROKEN image, and the visual gate counts it.
   OPAQUE, and that is the point: a translucent stripe over the OTHER photo is
   what this used to be, and it read as "the same photo on both sides with a
   handle that does nothing": the one state where a photographer most needs to
   be told a photo is still missing. */
.bk-compare-img--empty {
  /* --bg, not --card: every scheme's --card is a translucent tint (measured:
     4% of --text in light, 8% in dark), so painting it here left the other
     photo showing straight through the "empty" half. --bg is the only opaque
     ground the injected palette gives a block. */
  background-color:var(--bg);
  background-image:repeating-linear-gradient(45deg,
    color-mix(in srgb,var(--text) 12%,transparent) 0 10px,
    transparent 10px 20px);
}
/* The reveal. inset(top right bottom left): clipping `--pos` off the LEFT means
   the after photo is visible to the RIGHT of the handle, which is the direction
   every one of these behaves in. */
.bk-compare-img--after { clip-path:inset(0 0 0 var(--pos,50%)); }
.bk-compare--eased .bk-compare-img--after,
.bk-compare--eased .bk-compare-divider { transition:clip-path .12s linear, left .12s linear, top .12s linear; }

.bk-compare-divider {
  position:absolute; top:0; bottom:0; left:var(--pos,50%);
  width:2px; margin-left:-1px; background:var(--pf-white);
  box-shadow:0 0 0 1px var(--pf-black-25); pointer-events:none;
}

.bk-compare-chip {
  position:absolute; bottom:.75rem; z-index:2;
  padding:.3rem .7rem; border-radius:999px;
  background:var(--pf-black-55); color:var(--pf-white);
  font-size:.75rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase;
  pointer-events:none;
}
.bk-compare-chip--before { left:.75rem; }
.bk-compare-chip--after  { right:.75rem; }
/* A label the author chose can be any length. Cap it. let it run out
   of the frame. The cap is HALF only where the two chips genuinely share a
   baseline: the wide horizontal wipe: because everywhere else a half-width
   cap truncates a label that had the whole row to itself ("STRAIGHT OUT …" on a
   390px vertical wipe, measured). */
.bk-compare-chip { max-width:calc(100% - 1.5rem); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.bk-compare--a .bk-compare-chip { max-width:calc(50% - 1rem); }

/* The handle IS the range's thumb, so the control a keyboard reaches and the
   grip a finger drags are the same object. The track is transparent; the frame
   underneath is what the visitor sees. 44px so it clears the touch floor. */
.bk-compare-range {
  position:absolute; left:0; right:0; top:50%; z-index:3;
  transform:translateY(-50%);
  width:100%; height:44px; margin:0; padding:0;
  background:transparent; border:none; appearance:none; -webkit-appearance:none;
  cursor:ew-resize;
}
.bk-compare-range::-webkit-slider-runnable-track { height:44px; background:transparent; border:none; }
.bk-compare-range::-moz-range-track { height:44px; background:transparent; border:none; }
.bk-compare-range::-webkit-slider-thumb {
  -webkit-appearance:none; appearance:none;
  width:44px; height:44px; border-radius:50%;
  background:var(--pf-white); border:2px solid var(--pf-black-15);
  box-shadow:0 2px 10px var(--pf-black-35); cursor:ew-resize;
}
.bk-compare-range::-moz-range-thumb {
  width:40px; height:40px; border-radius:50%;
  background:var(--pf-white); border:2px solid var(--pf-black-15);
  box-shadow:0 2px 10px var(--pf-black-35); cursor:ew-resize;
}
.bk-compare-range:focus-visible { outline:3px solid var(--primary); outline-offset:2px; }

/* Variants: a = slider (default) · b = vertical · c = side by side */

/* b: the wipe runs top-to-bottom, so the clip and the divider swap axes and
   the range turns with them. writing-mode is the standards-track way to get a
   vertical range; where it is unsupported the control stays horizontal and
   still works, which is why nothing depends on it visually except the thumb's
   position. */
.bk-compare--b {
  & .bk-compare-img--after { clip-path:inset(var(--pos,50%) 0 0 0); }
  & .bk-compare-divider {
    top:var(--pos,50%); bottom:auto; left:0; right:0;
    width:auto; height:2px; margin-left:0; margin-top:-1px;
  }
  /* The vertical wipe drags on Y, so it is Y it has to claim. */
  & .bk-compare-frame { touch-action:pan-x; }
  & .bk-compare-range {
    writing-mode:vertical-lr;
    left:50%; right:auto; top:0; bottom:0;
    transform:translateX(-50%);
    width:44px; height:100%;
    cursor:ns-resize;
  }
  & .bk-compare-range::-webkit-slider-thumb { cursor:ns-resize; }
  & .bk-compare-range::-moz-range-thumb { cursor:ns-resize; }
  & .bk-compare-chip--before { left:.75rem; bottom:auto; top:.75rem; }
  & .bk-compare-chip--after  { right:auto; left:.75rem; bottom:.75rem; }
}

/* c: no reveal at all: two frames side by side, which is the version that
   needs no JavaScript, no pointer and no handle. The range has nothing left to
   control, so it goes; the chips label the two halves instead.

   The chips are placed by GRID CELL, not by offset from the frame's edges, and
   that is the whole trick. Offsets (`bottom:.75rem`, `left:calc(50% + …)`) are
   measured against the frame, which in this variant is a container holding
   BOTH photos: so the moment the columns collapse to one on a phone, "bottom
   of the frame" is the bottom of the SECOND photo and both chips pile onto it.
   Sharing a cell with its own image makes each chip follow its photo through
   any reflow. */
.bk-compare--c {
  & .bk-compare-frame {
    /* No drag here at all, so the browser keeps BOTH axes. */
    touch-action:auto; user-select:auto; -webkit-user-select:auto;
    aspect-ratio:auto; overflow:visible; background:none;
    display:grid; grid-template-columns:1fr 1fr; grid-template-rows:auto; gap:.5rem;
  }
  & .bk-compare-img {
    position:static; clip-path:none; width:100%; height:auto;
    aspect-ratio:3/2; border-radius:calc(.5rem * var(--pf-radius-k,1));
  }
  & .bk-compare-img--before { grid-column:1; grid-row:1; }
  & .bk-compare-img--after  { grid-column:2; grid-row:1; }
  & .bk-compare-divider { display:none; }
  & .bk-compare-range { display:none; }
  & .bk-compare-chip {
    position:static; align-self:end; justify-self:start; margin:.75rem;
  }
  & .bk-compare-chip--before { grid-column:1; grid-row:1; }
  & .bk-compare-chip--after  { grid-column:2; grid-row:1; }
}

@media (prefers-reduced-motion: reduce) {
  /* The handle still moves: that is the control doing its job, unlike motion the
     reader did not ask for. Only the easing goes. */
  .bk-compare--eased .bk-compare-img--after,
  .bk-compare--eased .bk-compare-divider { transition:none; }
}

@media (max-width:620px) {
  /* On a phone the frame is ~350px wide and two chips on the same baseline
     collide: measured at 390: "STRAIGHT OUT OF CAMERA" ran straight under
     "DELIVERED". Put them on opposite corners instead, which keeps each one on
     its own side of the seam AND gives each the full width. Variant b already
     labels top and bottom, and c gives each chip its own photo, so this is the
     horizontal wipe's problem alone. */
  .bk-compare--a {
    /* Opposite corners means each chip has the whole row again. */
    & .bk-compare-chip { max-width:calc(100% - 1.5rem); }
    & .bk-compare-chip--after { top:.75rem; bottom:auto; }
  }

  /* Two 3:2 frames side by side on a phone are two thumbnails. Stack them:
     one column, two rows, and each chip moves with its own photo because it
     shares that photo's cell. */
  .bk-compare--c {
    & .bk-compare-frame { grid-template-columns:1fr; grid-template-rows:auto auto; }
    & .bk-compare-img--after,
    & .bk-compare-chip--after { grid-column:1; grid-row:2; }
  }
}

/* ── collage: the editorial spread (#688) ─────────────────────────────────
   Two or three photos arranged like a magazine page, with a short story beside
   or beneath them. Entirely static: there is no browser JS for this block, and
   nothing animates, so a crawler, a failed bundle and prefers-reduced-motion
   all get the picture a visitor gets.

   Every offset is a GRID PLACEMENT or a top margin: never a negative
   horizontal margin and never a translate on the X axis. That is deliberate:
   the frames overlap by sharing grid cells inside a media box that is itself
   100% wide, so no arrangement can push past the block's own edge. Horizontal
   overflow on a phone is the failure mode this block is most exposed to, and
   the way to not have it is to make it unreachable. to test for it.

   The variant class lives on the block's OWN element (.bk-collage--a), not on
   the .page-block--collage wrapper, so the rules survive anywhere the block is
   rendered without that wrapper (a columns child: #686). */
.bk-collage {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2.5rem);
}
.bk-collage-heading {
  margin: 0;
  font-size: clamp(1.4rem, 2.8vw, 2.2rem);
  line-height: 1.12;
  font-weight: 700;
  text-wrap: balance;
}
.bk-collage-body {
  margin: .75rem 0 0;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.65;
  max-width: 46ch;
}
/* Use justify-self as well as align-self. `.bk-collage-text` is a GRID, and
   on a grid item align-self controls only the BLOCK axis: it stops the button
   growing taller, and does nothing about its width, so the default `stretch`
   on the inline axis made "See the full gallery" a 714px bar with its label
   parked at the left edge in variants a and c (b only escaped it because it
   sets justify-items: start on the whole panel). A button is as wide as its
   label. */
.bk-collage-cta { margin-top: 1.25rem; align-self: start; justify-self: start; }
.bk-collage-text { display: grid; align-content: start; }
/* An empty text panel (photos only, no story) must not reserve a column's worth
   of nothing beside the pictures. :empty is exact here: renderCollage emits
   the div with no whitespace inside when every field is blank. */
.bk-collage-text:empty { display: none; }

.bk-collage-fig {
  margin: 0;
  min-width: 0;
}
.bk-collage-img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  /* No crop-origin declaration here, and not one in a comment either:
     TestGalleryMosaicTilesCarryPerPhotoFocal counts that property name across
     the whole rendered document, block CSS included, to prove a photo with no
     focal point gets no crop hint: and the CSS is inlined comments and all, so
     merely NAMING the property in here fails it (measured: 3 declarations where
     the test wants 2). Centre is already the default, and the real focal
     arrives as an inline style from focalStyleAttr when the asset has one. */
  object-fit: cover;
  border-radius: calc(.5rem * var(--pf-radius-k,1));
  background: var(--card);
}
/* The "no photos yet" frame. A styled panel, never an <img> with an empty src.
   An empty src is a BROKEN image (the browser re-requests the document) and the
   visual gate counts it. --bg, not --card: every scheme defines --card as a
   translucent tint of --text, and a translucent placeholder over a coloured
   band reads as a photo that failed to load. as a slot to fill. */
.bk-collage-img--empty {
  background-color: var(--bg);
  background-image: repeating-linear-gradient(45deg,
    color-mix(in srgb, var(--text) 12%, transparent) 0 10px,
    transparent 10px 20px);
  border: 1px dashed var(--border);
}

/* ── a: overlap: frames off the grid, the story beside them ──────────────
   A STAGE with a fixed aspect ratio and absolutely-placed frames, .
   flow with per-frame offsets. Both render; the stage is chosen because every
   inset is then a fraction of a box whose height is already settled, so the
   arrangement is the SAME PICTURE at 1440 and at 390 and cannot leave its own
   box in either direction. The flow version restages itself at each breakpoint,
   because each frame's offset is a percentage of its own column.

   The three placements are chosen so all THREE photos read. The first pass had
   frame 1 at the back with frames 2 and 3 laid over it, and at 390 what was
   left of the photographer's FIRST photo was an L a few pixels wide (opening
   the PNG is the only thing that finds that: every gate was green through it).
   Each frame now overlaps only the EDGE of the one before it. */
.bk-collage--a {
  grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  align-items: center;
}
/* Photos and no story is a real state: isEmpty only drops the block when there
   is NEITHER a photo nor a heading: and the text panel is display:none there.
   A fixed two-column track then keeps the second column's .95fr, so the whole
   composition sat in the left half of a 1344px block with the right half blank.
   The story's column only exists when there is a story. */
.bk-collage--a:has(> .bk-collage-text:empty) { grid-template-columns: minmax(0, 1fr); }
.bk-collage--a .bk-collage-media {
  position: relative;
  aspect-ratio: 1 / 1;
}
.bk-collage--a .bk-collage-fig {
  position: absolute;
  box-shadow: 0 14px 40px var(--pf-black-18);
  border-radius: calc(.5rem * var(--pf-radius-k,1));
  overflow: hidden;
}
.bk-collage--a .bk-collage-img { height: 100%; aspect-ratio: auto; border-radius: 0; }
.bk-collage--a .bk-collage-fig--1 { left: 0; top: 0; width: 62%; height: 74%; z-index: 2; }
.bk-collage--a .bk-collage-fig--2 { right: 0; top: 20%; width: 44%; height: 62%; z-index: 1; }
/* The third photo is an accent: a small frame tucked under the
   first one. It only appears when the photographer picked three. */
.bk-collage--a .bk-collage-fig--3 { left: 8%; bottom: 0; width: 36%; height: 34%; z-index: 3; }
/* Two photos have a whole corner of the stage to spread into that three do not. */
.bk-collage--a .bk-collage-media[data-count="2"] .bk-collage-fig--1 { width: 60%; height: 84%; }
.bk-collage--a .bk-collage-media[data-count="2"] .bk-collage-fig--2 { width: 52%; top: 16%; height: 84%; }
/* One frame (or none: the placeholder) has nothing to overlap, so it takes the
   whole stage. leaving two thirds of it empty. */
.bk-collage--a .bk-collage-media[data-count="0"] .bk-collage-fig--1,
.bk-collage--a .bk-collage-media[data-count="1"] .bk-collage-fig--1 {
  left: 0; top: 0; width: 100%; height: 100%;
}

/* ── b: stagger: frames stepped diagonally, the story underneath ─────────
   Flow, not a stage: the step is a downward offset on an otherwise ordinary row
   of columns, and the frames never overlap, so nothing needs stacking. The
   offset is PADDING. margin so it is part of the frame's own box
  : the row is then sized from the same number the eye reads as the step. */
.bk-collage--b .bk-collage-media {
  display: grid;
  /* auto-flow column with 1fr auto-columns gives exactly as many equal columns
     as there are photos: two frames fill the row, three step across it: with
     no track left empty by a fixed repeat(3, …). */
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: clamp(.6rem, 1.8vw, 1.4rem);
  align-items: start;
}
.bk-collage--b .bk-collage-fig--2 { padding-top: 14%; }
.bk-collage--b .bk-collage-fig--3 { padding-top: 28%; }
.bk-collage--b .bk-collage-img {
  aspect-ratio: 3 / 4;
  box-shadow: 0 14px 40px var(--pf-black-16);
}
.bk-collage--b .bk-collage-text { justify-items: start; }
.bk-collage--b .bk-collage-heading { max-width: 24ch; }

/* ── c: polaroid: tilted prints, the story as the caption beside them ─────
   display:contents on the media box dissolves it so the three figures and the
   text card are all flex items of the block itself. That is what lets the
   caption wrap into the arrangement. sitting under it, and it keeps
   the markup identical to the other two variants. */
.bk-collage--c {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: clamp(1rem, 3vw, 2.25rem);
}
.bk-collage--c .bk-collage-media { display: contents; }
.bk-collage--c .bk-collage-fig,
.bk-collage--c .bk-collage-text {
  /* Paper, mixed from the scheme's own tokens. hardcoded white: a
     print stays a print on the dark and accent schemes. turning into
     a white rectangle with invisible ink on it. */
  background: color-mix(in srgb, var(--text) 6%, var(--bg));
  border: 1px solid var(--border);
  box-shadow: 0 12px 32px var(--pf-black-20);
  border-radius: calc(2px * var(--pf-radius-k,1));
}
.bk-collage--c .bk-collage-fig {
  /* A flex-basis, and one sized so that three prints AND the caption
     card fit on a single desktop row: at 26% they came to 1348px inside a
     1344px block and the card wrapped alone onto a second line, which reads as
     three photos with an orphan beneath them. as one arrangement.
     Narrower prints keep all four together at 1440 and still wrap sensibly
     below it. */
  flex: 0 1 clamp(170px, 24%, 280px);
  /* The bottom lip is what makes it read as a print. a bordered
     photo. */
  padding: .65rem .65rem 2.4rem;
  transform: rotate(-2.4deg);
}
.bk-collage--c .bk-collage-fig--2 { transform: rotate(1.8deg); margin-top: 1.75rem; }
.bk-collage--c .bk-collage-fig--3 { transform: rotate(-1.1deg); margin-top: .75rem; }
.bk-collage--c .bk-collage-img { aspect-ratio: 1 / 1; border-radius: 0; }
.bk-collage--c .bk-collage-text {
  /* Wider than a print, and with a floor of 240px: at 200 the caption broke
     "See the full gallery" across two lines inside its own button. */
  flex: 0 1 clamp(240px, 28%, 340px);
  padding: 1.4rem 1.3rem 1.6rem;
  margin-top: 2.5rem;
  transform: rotate(1.2deg);
}
.bk-collage--c .bk-collage-heading { font-size: clamp(1.15rem, 1.8vw, 1.5rem); }
.bk-collage--c .bk-collage-body { font-size: .95rem; max-width: none; }

/* The overlap needs a column of real width beside the story; at 810 a 2:1 split
   leaves the stage 425px wide and the text 289, which is a thumbnail beside a
   narrow column. a spread. It goes single-column a good deal earlier
   than the other two variants: the stage keeps its arrangement at any width,
   so stacking costs nothing but height. */
@media (max-width: 900px) {
  .bk-collage--a {
    grid-template-columns: 1fr;
    align-items: start;
  }
  /* Full-width now, so the square stage would be 714px tall at 810 and the
     block would need most of a screen to itself. Landscape here, and portrait
     only once the stage is narrow enough that a landscape one would make three
     frames postage stamps. */
  .bk-collage--a .bk-collage-media { aspect-ratio: 5 / 4; }
}

@media (max-width: 620px) {
  .bk-collage--a .bk-collage-media { aspect-ratio: 4 / 5; }
}

@media (max-width: 720px) {
  /* A 2.4° tilt on a full-width card is 15px of horizontal bounding box the
     phone does not have, and three frames side by side at 390 are three
     thumbnails. Both go. */
  .bk-collage--b .bk-collage-media {
    grid-auto-flow: row;
    grid-auto-columns: auto;
    /* The column gap was ~10px, which between three stacked frames reads as one
       long strip that happens to change colour. Stacked frames need a seam you
       can see. */
    gap: 1rem;
  }
  .bk-collage--b .bk-collage-fig--2,
  .bk-collage--b .bk-collage-fig--3 { padding-top: 0; }
  .bk-collage--b .bk-collage-img { aspect-ratio: 4 / 3; }

  .bk-collage--c .bk-collage-fig,
  .bk-collage--c .bk-collage-text {
    flex-basis: 100%;
    transform: none;
    margin-top: 0;
  }
  .bk-collage--c .bk-collage-img { aspect-ratio: 4 / 3; }
}

/* ── vendor_credits (#583) ─────────────────────────────────────────────────
   The wedding-team credit strip: a list of role + linked business name, with a
   visible "credited by" line under it. Every colour comes from the scheme's own
   tokens, so the band inverts correctly on the dark/accent schemes with no
   per-scheme rules of its own (06-schemes-variants owns those tokens).

   The link is the product here, not decoration: so it is underlined on hover
   and never dimmed below the surrounding text. */
.bk-vendor-credits       { display:flex; flex-direction:column; gap:1.25rem; }

.bk-vc-heading           { margin:0; font-family:var(--font-display, inherit);
                           font-size:clamp(1.25rem, 2.6vw, 1.9rem); font-weight:600; letter-spacing:-.01em; }
.bk-vc-intro             { margin:0; color:var(--muted); max-width:52ch; line-height:1.65; }

.bk-vc-list              { list-style:none; margin:0; padding:0; display:grid; gap:.15rem; }

.bk-vc-item              { display:grid; grid-template-columns:minmax(7rem, 12rem) 1fr auto;
                           gap:.5rem 1.25rem; align-items:baseline;
                           padding:.7rem 0; border-bottom:1px solid var(--border); }
.bk-vc-item:last-child   { border-bottom:0; }

.bk-vc-role              { color:var(--muted); font-size:.78rem; letter-spacing:.09em;
                           text-transform:uppercase; }

.bk-vc-name              { color:var(--text); font-weight:600; text-decoration:none;
                           text-underline-offset:.18em; }
a.bk-vc-name:hover       { text-decoration:underline; }

/* A member credit points back into the platform: worth marking, quietly. */
.bk-vc-item--member .bk-vc-name { text-decoration:underline; text-decoration-color:var(--primary);
                           text-decoration-thickness:2px; }

.bk-vc-social            { color:var(--muted); font-size:.85rem; text-decoration:none; white-space:nowrap; }
.bk-vc-social:hover      { text-decoration:underline; }

.bk-vc-provenance        { margin:0; color:var(--muted); font-size:.85rem; }
.bk-vc-by                { color:var(--muted); font-weight:500; }

/* Owner-only hint; never rendered for a visitor (the template gates it on
   edit mode, so a crawler can never be shown an editorial note). */
.bk-vc-empty             { margin:0; padding:1rem 1.25rem; border:1px dashed var(--border);
                           border-radius:var(--pf-radius-md); color:var(--muted); font-size:.9rem; }

/* Variants: CSS only, same HTML.
   a = rows (default) · b = cards · c = inline strip */
.page-block--vendor_credits.bk-variant-b .bk-vc-list {
  grid-template-columns:repeat(auto-fit, minmax(13rem, 1fr)); gap:.75rem;
}
.page-block--vendor_credits.bk-variant-b .bk-vc-item {
  grid-template-columns:1fr; align-items:start; gap:.3rem;
  padding:1rem 1.15rem; border:1px solid var(--border); border-radius:calc(.7rem * var(--pf-radius-k,1)); background:var(--card);
}
.page-block--vendor_credits.bk-variant-c .bk-vc-list {
  display:flex; flex-wrap:wrap; gap:.4rem 1.5rem;
}
.page-block--vendor_credits.bk-variant-c .bk-vc-item {
  display:flex; gap:.5rem; align-items:baseline; padding:.2rem 0; border-bottom:0;
}
.page-block--vendor_credits.bk-variant-c .bk-vc-role { font-size:.72rem; }

@media (max-width:640px) {
  /* The role above the name: a 7rem label column next to a long business name
     is two cramped columns on a phone. */
  .bk-vc-item { grid-template-columns:1fr; gap:.15rem; }
}

/* ── logo_strip: the "as seen in" band (#696) ─────────────────────────────
   Publications, awards, venues and brands as a row of MARKS: small, evenly
   spaced, sitting under the work. competing with it. Entirely
   static: there is no browser JS for this block: so a crawler, a failed
   bundle and prefers-reduced-motion all get a readable strip.

   Width: the DEFAULT tier, which is why this block appears in no list in
   02-layout.css. A logo strip is punctuation between sections, not media: run
   at the wide tier it becomes a second hero, and the marks drift so far apart
   that the row stops reading as one claim.

   The variant class lives on the block's OWN element (.bk-logo-strip--a), not on
   the .page-block--logo_strip wrapper, so every rule below survives inside a
   columns child, which renders with no wrapper around it (#686). */
.bk-logo-strip {
  padding-block: clamp(1rem, 2.5vw, 2rem);
}
.bk-ls-heading {
  margin: 0 0 clamp(1rem, 2vw, 1.75rem);
  text-align: center;
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}
.bk-ls-lane {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: clamp(1.25rem, 4vw, 3.25rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.bk-ls-item { min-width: 0; }
/* A real tap target. The mark itself is deliberately small: that is what makes
   it read as a credential. as an image: so the LINK, not the logo,
   carries the 44px a finger needs. Also the reason the padding is here and not
   on the item: a bare <li> is not something anyone presses. */
.bk-ls-link {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  min-height: 44px;
  min-width: 44px;
  padding: .25rem .35rem;
  color: inherit;
  text-decoration: none;
}
.bk-ls-logo {
  display: block;
  height: clamp(1.5rem, 3.2vw, 2.5rem);
  width: auto;
  /* A wordmark is wide. Without this cap a 2000px-wide PNG sets the flex item's
     basis and the row stops wrapping the way the gap intends. */
  max-width: min(11rem, 40vw);
  object-fit: contain;
}
/* The no-logo mark. A photographer who has the credential but not the artwork
   still gets the credit, and it has to look deliberate. like a
   picture that failed to load: hence a bordered chip in the block's own
   type, not a grey rectangle. */
.bk-ls-chip {
  display: inline-block;
  padding: .4rem .85rem;
  border: 1px solid var(--border);
  border-radius: 2rem;
  font-size: .82rem;
  font-weight: 600;
  letter-spacing: .04em;
  white-space: nowrap;
  color: var(--muted);
}
.bk-ls-name {
  font-size: .78rem;
  color: var(--muted);
  text-align: center;
}

/* ── a: row: marks under the work ─────────────────────────────────────────
   Desaturated so a dozen brand palettes do not fight the photography above
   them; full colour on hover, which is also what tells a visitor the mark is a
   link. filter, not opacity alone: opacity would fade the chip's border too and
   leave coloured logos coloured. */
.bk-logo-strip--a .bk-ls-logo,
.bk-logo-strip--b .bk-ls-logo {
  filter: grayscale(1) opacity(.7);
  transition: filter .25s ease;
}
.bk-logo-strip--a .bk-ls-link:hover .bk-ls-logo,
.bk-logo-strip--a .bk-ls-link:focus-visible .bk-ls-logo,
.bk-logo-strip--b .bk-ls-link:hover .bk-ls-logo,
.bk-logo-strip--b .bk-ls-link:focus-visible .bk-ls-logo {
  filter: none;
}

/* ── b: drift: the row scrolls, slowly ───────────────────────────────────
   The marquee trick (11-marquee.css): the track holds the same lane twice and
   slides by -50%, which is exactly one lane's width, so the restart lands on a
   matching mark and the seam never shows. Duration rides in as --ls-dur from
   the renderer (seconds-per-mark × count), so more logos means a longer strip
  . a faster one.

   overflow:hidden on the BLOCK, not a 100vw breakout: the block keeps its
   default width tier, and the clip is what stops the max-content track from
   widening the document. */
.bk-logo-strip--b { overflow: hidden; }
.bk-logo-strip--b .bk-ls-track {
  display: flex;
  width: max-content;
  flex-wrap: nowrap;
}
.bk-logo-strip--b .bk-ls-lane {
  flex: none;
  flex-wrap: nowrap;
  justify-content: flex-start;
  padding-right: clamp(1.25rem, 4vw, 3.25rem);
}
@media (prefers-reduced-motion: no-preference) {
  .bk-logo-strip--b .bk-ls-track { animation: lsDrift var(--ls-dur, 30s) linear infinite; }
  /* Pause on hover so a visitor can actually read a mark they noticed: and so
     the link under the cursor stays where the cursor is. Focus-within pauses it
     too: a keyboard user tabbing through moving links has the same problem. */
  .bk-logo-strip--b:hover .bk-ls-track,
  .bk-logo-strip--b:focus-within .bk-ls-track { animation-play-state: paused; }
  @keyframes lsDrift { from { transform: translateX(0); } to { transform: translateX(-50%); } }
}

/* ── c: grid: marks with their names ──────────────────────────────────────
   The credentials list. the punctuation strip: every mark is
   labelled, so this variant keeps the logos in FULL COLOUR: a named credential
   is being read, not glanced past. */
.bk-logo-strip--c .bk-ls-lane {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: clamp(1.25rem, 3vw, 2.25rem);
  align-items: stretch;
}
.bk-logo-strip--c .bk-ls-link {
  padding: 1rem .75rem;
  border: 1px solid var(--border);
  border-radius: var(--pf-radius-md);
  background: var(--card);
  height: 100%;
}
.bk-logo-strip--c .bk-ls-chip { border: 0; padding: 0; white-space: normal; }

@media (max-width: 640px) {
  /* Tighter gaps, smaller marks: at 390 the desktop gap leaves two logos on a
     line with a hole between them. */
  .bk-ls-lane { gap: 1rem 1.5rem; }
  .bk-ls-logo { max-width: 42vw; }
  /* TWO columns. 9rem plus the desktop gap does not fit twice in a
     390px gutter, so auto-fit drops the grid to a single column and six
     credentials become six full-width cards and a scroll: which reads as a
     list of offers. as a strip of marks. Measured at 390: 7rem
     leaves two ~137px cards with room for the gap. */
  .bk-logo-strip--c .bk-ls-lane {
    grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
    gap: .75rem;
  }
  .bk-logo-strip--c .bk-ls-link { padding: .85rem .5rem; }
}

/* ── cta_band: the closing band with a scarcity number (#719) ──────────────
   "3 · dates left in 2026", the line about what to do next, and the button that
   does it, edge to edge at the point where the visitor has stopped looking and
   has to decide. Entirely static: no browser JS, nothing animates, so a crawler,
   a failed bundle and prefers-reduced-motion all get the same picture. (There is
   deliberately no count-up animation: a figure that starts at 0 and climbs is
   briefly a lie about the photographer's availability.)

   THE GROUND IS A SET OF TOKENS, not a background declaration per variant. Each
   variant rebinds --cta-ground / --cta-ink / --cta-btn-*, and the block's own
   colour scheme (bk-cs-*, chosen by the photographer on the block) rebinds them
   back: that is what "honour the colour scheme like every other block" means
   here, and doing it with four tokens. ~20 overriding declarations is
   what keeps the two from fighting.

   The button inversion is the part that is easy to get silently wrong. Every
   theme's `.btn` is `background: var(--primary)`, so a primary button painted on
   an ACCENT ground is a primary rectangle on a primary band: present, clickable,
   and invisible. Wherever the ground is --primary (variant a by default, and any
   variant under the `accent` scheme) the button flips to --on-primary with
   --primary text: the pair the theme already guarantees AA for, because
   portfolio/render.go picks #111 or #fff by the accent's own luminance
   (on_primary_test.go).

   The variant class lives on the block's OWN element (.bk-cta-band--a) so LAYOUT
   rules survive anywhere it renders without the wrapper (#686). The GROUND rules
   do key on the
   wrapper, because they are exactly the rules that have to lose to the
   photographer's chosen scheme, and the scheme only exists on the wrapper.
   cta_band is not offered as a columns child: it is a bleed-tier band, and a
   third of a row is not one: so there is no live surface where that matters. */
.bk-cta-band {
  /* Defaults: no ground of its own (the wrapper's scheme shows through), ink
     inherited from that scheme, and the theme's ordinary primary button. */
  --cta-ground: transparent;
  --cta-ink: currentColor;
  --cta-ink-soft: color-mix(in srgb, currentColor 74%, transparent);
  --cta-btn-bg: var(--primary);
  --cta-btn-ink: var(--on-primary, #fff);

  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--cta-ground);
  color: var(--cta-ink);
  padding-block: clamp(2.5rem, 6vw, 5rem);
  padding-inline: clamp(1.5rem, 6vw, 6rem);
}
/* The band bleeds; its CONTENT does not. Without a cap, the heading of a 1440px
   band runs the full viewport and stops being a heading. */
.bk-cta-inner {
  max-width: 1180px;
  margin-inline: auto;
  display: grid;
  gap: clamp(1.25rem, 3vw, 2.5rem);
}

/* The figure. Tabular numerals so a two-digit count sits on the same optical
   centre as a one-digit one; the label is set small and tracked out so the
   NUMBER is what the eye lands on. */
.bk-cta-figure {
  margin: 0;
  display: grid;
  gap: .35rem;
  align-content: start;
}
.bk-cta-count {
  font-family: var(--font-display, Georgia, "Times New Roman", serif);
  font-size: clamp(3rem, 8vw, 5.5rem);
  line-height: .88;
  font-weight: 700;
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
}
.bk-cta-count-label {
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  line-height: 1.4;
}

.bk-cta-heading {
  margin: 0;
  font-size: clamp(1.5rem, 3.2vw, 2.5rem);
  line-height: 1.12;
  font-weight: 700;
  text-wrap: balance;
}
.bk-cta-subtext {
  margin: .75rem 0 0;
  font-size: 1rem;
  line-height: 1.65;
  max-width: 52ch;
}

/* THE INK RULES CARRY TWO CLASSES ON PURPOSE. Every theme styles headings and
   body copy inside the page (`.pf-custom-page h2`, `.bk-* p` and friends) at
   specificity (0,1,1), which out-ranks a single-class `.bk-cta-heading {color}`
  : measured: on the accent band the heading and the figure's caption rendered
   in the theme's dark body colour on a dark accent, while the numeral (a bare
   <span> nothing else targets) took the token and came out white. Two classes
   put every piece of ink in the band on the same footing, which is the same
   trick 06-schemes-variants.css uses for the colour schemes.

   THE FIGURE'S WRAPPER IS ON THE LIST TOO, and it is the one that is easy to
   leave off. `.bk-cta-figure` is a <p>, and `06-schemes-variants.css` dims every
   <p> inside the two dark bands to 74% of the band's foreground: a rule at
   (0,1,1), which a single-class rule loses to. The COUNT's own rule wins, but on
   every scheme that leaves --cta-ink as `currentColor` that token resolves
   against the figure's INHERITED colour, so the numeral came out at 74% beside a
   heading at 100%: the block's whole point, quieter than the line next to it.
   Putting the wrapper on the token fixes the numeral and the caption together
   (--cta-ink-soft is a mix of currentColor, so it re-derives from the corrected
   value). Measured on all five schemes × three variants. */
.bk-cta-band .bk-cta-figure,
.bk-cta-band .bk-cta-count,
.bk-cta-band .bk-cta-heading { color: var(--cta-ink); }
.bk-cta-band .bk-cta-count-label,
.bk-cta-band .bk-cta-subtext { color: var(--cta-ink-soft); }
/* …AND ONE MORE CLASS AGAIN, KEYED ON THE WRAPPER, because two classes are only
   enough against the THEME. The COLOUR-SCHEME rules in 06-schemes-variants.css
   are `.bk-cs-<x> :is(h1,…,.bk-statement-heading)`: (0,2,0), a tie with the
   rules above that source order decides, and 06 is concatenated AFTER 03: and
   `.bk-cs-<x> :is(p,…,.bk-callout p)`, which is (0,2,1) and simply wins. So on
   any band whose own ground differs from the scheme's (the photo band under the
   `accent` scheme; every band under the two dark schemes) the heading and the
   subtext took the SCHEME's ink while the numeral took the block's, and the two
   were never the same colour. Measured across all five schemes × three variants:
   #111 heading over the photo band's black scrim under a light accent, and a
   figure dimmed to 74% beside a heading at 100% on `dark`. Three classes clear
   both, and the wrapper is where the scheme class lives, so nothing is lost in
   the wrapper-less case. A columns child is not wrapper-less any more: its
   `.bk-col-child` box carries the scheme class (_blocks.tmpl, #1917), so the
   scheme rules reach it and it needs the same three classes. */
:is(.page-block--cta_band,.bk-col-child) .bk-cta-band .bk-cta-figure,
:is(.page-block--cta_band,.bk-col-child) .bk-cta-band .bk-cta-count,
:is(.page-block--cta_band,.bk-col-child) .bk-cta-band .bk-cta-heading { color: var(--cta-ink); }
:is(.page-block--cta_band,.bk-col-child) .bk-cta-band .bk-cta-count-label,
:is(.page-block--cta_band,.bk-col-child) .bk-cta-band .bk-cta-subtext { color: var(--cta-ink-soft); }
:is(.page-block--cta_band,.bk-col-child) .bk-cta-band .bk-cta-btn2 { color: var(--cta-ink); }
.bk-cta-body { display: grid; align-content: center; }

/* justify-self, not align-self: .bk-cta-body is a GRID, and on a grid item
   align-self governs the BLOCK axis: it stops the row growing taller and does
   nothing about its width, so the default `stretch` would spread two buttons
   across the whole band with a hole between them (the trap collage hit). */
.bk-cta-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .85rem 1.4rem;
  margin-top: 1.5rem;
  justify-self: start;
}
/* Two classes again: every theme defines `.btn { background: var(--primary);
   color: var(--on-primary, #fff) }`, which is (0,1,0) like a bare
   `.bk-cta-btn` would be, and the winner of a tie is whichever the
   concatenator emitted last. That is filename order across two directories,
   which is not a thing to bet a button's legibility on. */
.bk-cta-band .bk-cta-btn {
  background: var(--cta-btn-bg);
  color: var(--cta-btn-ink);
}
/* The second action is a LINK: two equal buttons make the
   visitor choose between them, and the point of the band is that there is one
   thing to do. Underlined. colour-only, so it is still identifiable
   as a link on a photo, where colour alone is unreliable. */
.bk-cta-btn2 {
  /* inline-flex + min-height, unlike a bare inline <a>. A text link is ~24px tall,
     and this one sits next to a 45px button in the band a visitor taps to book:
     the design-compare touch metric measured it at 103×24 against a 44px floor.
     The underline still carries the "this is a link" signal; the box around it
     is only there to be hit. */
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding-inline: .15rem;
  font-weight: 600;
  font-size: .95rem;
  text-decoration: underline;
  text-underline-offset: .25em;
  text-decoration-thickness: 1px;
}
.bk-cta-band .bk-cta-btn2 { color: var(--cta-ink); }
.bk-cta-btn2:hover { opacity: .82; text-decoration: underline; }

/* The background photo: emitted by renderCtaBand for the PHOTO BAND only. An
   element with `z-index: -1` sits behind the block's CONTENT but in FRONT of
   the block's own background, so a photo present on variant a would cover the
   accent ground. sit under it; the renderer leaves it out .
   this file hiding it, which also spares a/c the download.
   `isolation: isolate` on the band keeps the -1 from escaping to the page.
   Variant b paints #111 under the photo, so an image that fails to load leaves
   white type on dark. white on white: which is also why this is a
   real <img> in a positioned div. a background-image: an element is
   what the visual gate can measure. */
.bk-cta-bg { position: absolute; inset: 0; z-index: -1; }
.bk-cta-bg-img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ── a: accent band: the whole band in the brand colour ───────────────────
   The default, and the reason the block exists as its own type: the figure is
   set at display size on the accent, which no other block can do. */
.bk-cta-band--a { text-align: center; }
.bk-cta-band--a .bk-cta-inner { justify-items: center; }
.bk-cta-band--a .bk-cta-subtext { margin-inline: auto; }
.bk-cta-band--a .bk-cta-actions { justify-self: center; justify-content: center; }
/* The accent ground is keyed on the wrapper's DEFAULT scheme, so a photographer
   who deliberately picks `dark` or `light-accent` gets what they picked. The
   `accent` scheme shares the rule. being excluded from it: that
   scheme already paints this exact ground for EVERY variant, and it needs the
   inverted button just as much.

   THE PHOTO BAND IS EXCLUDED, and that exclusion is load-bearing. This selector
   is (0,3,0) and `.bk-cta-band--b` is (0,1,0), so without the :not() the accent
   scheme rebinds the PHOTO band's tokens too: while renderCtaBand still emits
   the photo and `.bk-cta-bg::after` still paints its 45-68% black scrim over it.
   Measured: on a light accent that put #111 heading and numeral on a dark scrim
   over a photograph, about 1.5:1. Both gates were green through it, because the
   fixture page carries only the default scheme and fidelity compares a canvas
   and a site that are wrong identically. Variant b's contract is its own: what
   is underneath is a photograph whose luminance nothing here knows, so the ink
   stays white no matter which scheme the photographer picked. */
:is(.page-block--cta_band,.bk-col-child).bk-cs-light .bk-cta-band--a,
:is(.page-block--cta_band,.bk-col-child).bk-cs-accent .bk-cta-band:not(.bk-cta-band--b) {
  --cta-ground: var(--primary);
  --cta-ink: var(--on-primary, #fff);
  --cta-ink-soft: var(--on-primary-muted, color-mix(in srgb, var(--on-primary, #fff) 76%, transparent));
  --cta-btn-bg: var(--on-primary, #fff);
  --cta-btn-ink: var(--primary);
}

/* ── b: photo band: the copy over a photo, under a dark scrim ─────────────
   The scrim is unconditional and the ink is white regardless of scheme: what is
   underneath is the photographer's own photo, whose luminance nothing here
   knows, and "readable most of the time" is not a contract. #111 under the photo
   means a failed image is a dark band with white type, not white on white. */
.bk-cta-band--b {
  --cta-ground: var(--pf-photo-ground);
  --cta-ink: var(--pf-white);
  --cta-ink-soft: var(--pf-white-84);
  text-align: center;
  min-height: clamp(18rem, 34vw, 26rem);
  display: grid;
  align-items: center;
}
.bk-cta-band--b .bk-cta-inner { justify-items: center; }
.bk-cta-band--b .bk-cta-subtext { margin-inline: auto; }
.bk-cta-band--b .bk-cta-actions { justify-self: center; justify-content: center; }
.bk-cta-band--b .bk-cta-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, var(--pf-black-45), var(--pf-black-68));
}

/* ── c: split: the figure on the left, the copy and buttons on the right ──
   A fixed two-column track would keep the figure's column when the block has no
   figure (the count defaults to 0 and the label is optional), leaving a third of
   a full-bleed band blank. :has() gives the single-child case its own track.
   The same escape collage needed for its empty text panel. */
/* `auto`. A fractional figure track is sized without regard to
   what is in it: at 810 a .75fr track came out ~216px while "DATES LEFT IN 2026"
   needs ~285px at this tracking, so the caption quietly overflowed its own
   column into the gutter: invisible to every gate, because the escape lands
   inside the block and the document's scroll width never moves: and the number
   sat marooned ~90px from the copy it belongs to. Sizing the track to its
   content puts the figure exactly as far from the heading as the gap says, at
   every width. The max-width is the other half: without it a photographer who
   writes a sentence where a caption goes takes the whole row. */
.bk-cta-band--c .bk-cta-inner {
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(1.5rem, 5vw, 4rem);
}
.bk-cta-band--c .bk-cta-inner:has(> .bk-cta-body:only-child) { grid-template-columns: minmax(0, 1fr); }
.bk-cta-band--c .bk-cta-figure { text-align: left; max-width: 22rem; }
.bk-cta-band--c .bk-cta-count { font-size: clamp(3.5rem, 9vw, 6.5rem); }
/* A tinted ground, so the split still reads as a BAND. as two
   paragraphs adrift in the page. Only on the default scheme: a photographer who
   picked `dark` gets dark, not a tint of primary over dark. */
:is(.page-block--cta_band,.bk-col-child).bk-cs-light .bk-cta-band--c {
  --cta-ground: color-mix(in srgb, var(--primary) 7%, var(--bg));
}

@media (max-width: 760px) {
  /* One column on a phone: two ~137px tracks are too narrow for either the
     figure or the heading. */
  .bk-cta-band--c .bk-cta-inner { grid-template-columns: minmax(0, 1fr); }
  .bk-cta-band--c .bk-cta-count { font-size: clamp(3rem, 16vw, 4.5rem); }
  .bk-cta-actions { gap: .85rem 1rem; }
  .bk-cta-band--b { min-height: 20rem; }
}

/* ── blog_posts: the "recent posts" band (#1648) ──────────────────────────
   Every post is resolved server-side by SiteBridge.resolveBlogPosts before
   this ever reaches the browser (blocktype_blog_posts.go): this file only
   lays the cards out, same split as vendor_credits above. The variant class
   rides on the block's own element, `.bk-blog-posts--<v>`, not a
   `.page-block--blog_posts` ancestor: a `columns` child renders with no
   `.page-block--<type>` wrapper, same reason renderStats gives. */
.bk-blog-posts        { display: flex; flex-direction: column; gap: 1.5rem; }
.bk-bp-heading         { margin: 0; font-family: var(--font-display, inherit);
                         font-size: clamp(1.25rem, 2.6vw, 1.9rem); font-weight: 600; letter-spacing: -.01em; }

.bk-bp-grid            { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1.75rem; }

.bk-bp-card            { display: flex; flex-direction: column; gap: .6rem; color: inherit; text-decoration: none; }
.bk-bp-cover           { display: block; aspect-ratio: 4 / 3; overflow: hidden; border-radius: calc(.5rem * var(--pf-radius-k,1)); background: var(--border); }
.bk-bp-cover img       { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .35s ease; }
.bk-bp-card:hover .bk-bp-cover img { transform: scale(1.04); }

.bk-bp-title           { font-weight: 600; line-height: 1.3; }
.bk-bp-date            { color: var(--muted); font-size: .8rem; }
.bk-bp-excerpt         { color: var(--muted); font-size: .92rem; line-height: 1.6;
                         display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* Owner-only tag on a draft/scheduled-future card (canvas/preview only: the
   port never returns one to a visitor-scoped render, see BlogPostsPort). */
.bk-bp-card--draft     { position: relative; }
.bk-bp-draft-tag       { align-self: flex-start; padding: .15rem .55rem; border-radius: 999px;
                         background: var(--border); color: var(--muted); font-size: .7rem;
                         font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }

/* Owner-only hint; never rendered for a visitor (renderBlogPosts gates it on
   edit mode). */
.bk-bp-empty           { margin: 0; padding: 1rem 1.25rem; border: 1px dashed var(--border);
                         border-radius: var(--pf-radius-md); color: var(--muted); font-size: .9rem; }

/* "Load more": a hidden checkbox + label toggling a sibling grid, no browser
   JS (renderBlogPostsLoadMore). Collapsed by default; the label doubles as
   the show/hide control and disappears once the extra grid is revealed. */
.bk-bp-grid--extra     { display: none; margin-top: 1.75rem; }
.bk-bp-more-toggle:checked ~ .bk-bp-grid--extra { display: grid; }
.bk-bp-more-label      { display: inline-flex; align-self: flex-start; margin-top: 1rem;
                         padding: .55rem 1.1rem; border: 1px solid var(--border); border-radius: 999px;
                         font-size: .85rem; font-weight: 600; cursor: pointer; }
.bk-bp-more-label:hover { border-color: var(--text); }
.bk-bp-more-toggle:checked ~ .bk-bp-more-label { display: none; }

/* Variants: CSS only, same HTML.
   a = grid (default) · b = list · c = cards */
.bk-blog-posts--b .bk-bp-grid,
.bk-blog-posts--b .bk-bp-grid--extra {
  grid-template-columns: 1fr; gap: 1.1rem;
}
.bk-blog-posts--b .bk-bp-card {
  flex-direction: row; align-items: center; gap: 1.25rem;
}
.bk-blog-posts--b .bk-bp-cover {
  flex: 0 0 8rem; aspect-ratio: 1 / 1;
}

.bk-blog-posts--c .bk-bp-card {
  padding: 1rem; border: 1px solid var(--border); border-radius: calc(.8rem * var(--pf-radius-k,1)); background: var(--card);
}

@media (max-width: 640px) {
  .bk-blog-posts--b .bk-bp-card { flex-direction: column; align-items: stretch; }
  .bk-blog-posts--b .bk-bp-cover { flex: none; aspect-ratio: 4 / 3; }
}

/* ── child_pages: a page's own subpages (#1728) ───────────────────────────
   Resolved server-side by SiteBridge.resolveChildPages; this file only lays
   the cards out. The variant class rides on the block's own element
   (`.bk-child-pages--<v>`) because a `columns` child has no
   `.page-block--<type>` wrapper. a = grid · b = list. */
.bk-child-pages       { display: flex; flex-direction: column; gap: 1.5rem; }
.bk-cp-heading        { margin: 0; font-family: var(--font-display, inherit);
                        font-size: clamp(1.25rem, 2.6vw, 1.9rem); font-weight: 600; letter-spacing: -.01em; }
.bk-cp-grid           { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1.75rem; }
.bk-cp-card           { display: flex; flex-direction: column; gap: .6rem; color: inherit; text-decoration: none; }
.bk-cp-cover          { display: block; aspect-ratio: 4 / 3; overflow: hidden; border-radius: calc(.5rem * var(--pf-radius-k,1)); background: var(--border); }
.bk-cp-cover img      { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .35s ease; }
.bk-cp-card:hover .bk-cp-cover img { transform: scale(1.04); }
.bk-cp-text           { display: flex; flex-direction: column; gap: .35rem; min-width: 0; }
.bk-cp-title          { font-family: var(--font-display, inherit); font-weight: 600; line-height: 1.3; }
.bk-cp-excerpt        { color: var(--muted); font-size: .92rem; line-height: 1.6;
                        display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }

/* Owner-only tag on a draft child (canvas/preview; a visitor never gets one). */
.bk-cp-draft-tag      { align-self: flex-start; padding: .15rem .55rem; border-radius: 999px;
                        background: var(--border); color: var(--muted); font-size: .7rem;
                        font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }

.bk-child-pages--b .bk-cp-grid { grid-template-columns: 1fr; gap: 1.1rem; }
.bk-child-pages--b .bk-cp-card { flex-direction: row; align-items: center; gap: 1.25rem; }
.bk-child-pages--b .bk-cp-cover { flex: 0 0 9rem; aspect-ratio: 4 / 3; }
.bk-child-pages--b .bk-cp-text  { flex: 1 1 auto; }

@media (max-width: 640px) {
  .bk-child-pages--b .bk-cp-card { flex-direction: column; align-items: stretch; }
  .bk-child-pages--b .bk-cp-cover { flex: none; }
}

/* Hard crop edge on page-block photos (#1785). The server puts the crop in
   --bk-vb (an inset() over the original) and the crop-relative focal position
   in --bk-op. Browsers without object-view-box keep the inline object-position
   and only get the focal anchor. */
@supports (object-view-box: inset(0)) {
  img[style*="--bk-vb"] {
    object-view-box: var(--bk-vb);
    object-position: var(--bk-op) !important;
  }
}

/* ── Specifications: rows of label and value (#2432) ──────────────────────── */
/* The variant rides on .bk-specs itself (bk-specs--<v>), clear of the
   .page-block--specifications wrapper: a columns child never gets that wrapper.
   a = table (label column, value column), b = stacked (label above value). */
.bk-specs               { margin:1rem 0 2.5rem; }
.bk-specs-heading       { font-family:var(--font-display); font-size:clamp(1.4rem,3vw,2rem); font-weight:700; margin:0 0 1rem; }
.bk-specs-list          { margin:0; }
.bk-specs-row           { padding:.75rem 0; border-bottom:1px solid var(--border); }
.bk-specs-row dt        { color:var(--muted); }
.bk-specs-row dd        { margin:0; overflow-wrap:anywhere; }
.bk-specs--a .bk-specs-row { display:grid; grid-template-columns:minmax(7rem,1fr) 2fr; gap:1rem; }
.bk-specs--b .bk-specs-row dt { font-size:.8rem; letter-spacing:.1em; text-transform:uppercase; margin-bottom:.2rem; }
@media (max-width:480px) {
  .bk-specs--a .bk-specs-row { grid-template-columns:1fr; gap:.2rem; }
}
/* Photo frame: the shared frame-and-tilt treatment for one photo (#2229).
   Markup comes from pages.renderTreatedPhoto (photo_treatment.go):

     figure.photo-frame.photo-frame--<style>   tilt, shadow, motion
       div.photo-frame__paper                  the frame itself
         div.photo-frame__img > img            the photo
         figcaption.photo-frame__caption       polaroid only

   split variant e is the first caller; image and collage can use the same
   markup without new CSS. The tilt arrives as --photo-tilt on the figure, the
   one value that cannot be a class. Colours are theme tokens, so the frame
   follows the site palette. */

:root { --photo-overhang: clamp(3rem, 7vw, 6rem); }

.photo-frame {
  --photo-tilt: 0deg;
  margin:0; width:min(100%, 28rem); justify-self:center;
  rotate:var(--photo-tilt);
  translate:0 var(--photo-shift, 0px);
  filter:drop-shadow(0 14px 22px var(--pf-black-25));
}
/* --card is a translucent tint of the text colour in the generated theme, so the
   paper is laid over the page background to stay opaque where the frame
   overhangs an image-led block. */
.photo-frame__paper     { background:linear-gradient(var(--card, var(--pf-white)), var(--card, var(--pf-white))), var(--bg, var(--pf-white)); color:var(--text); padding:clamp(.6rem, 1.4vw, 1rem); }
.photo-frame__img       { aspect-ratio:4/5; overflow:hidden; background:var(--border); }
.photo-frame__img img   { width:100%; height:100%; object-fit:cover; display:block; }
.photo-frame__empty     { display:block; width:100%; height:100%; background:var(--card, var(--border)); }
.photo-frame__caption   { margin:0; padding-top:.7rem; text-align:center; font-size:.95rem; line-height:1.3; overflow-wrap:anywhere; }

/* paper: a deckled edge, cut by an inline SVG mask (no extra request). */
.photo-frame--paper .photo-frame__paper {
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0.0 0.0L3.6 0.8L7.1 0.3L10.7 0.2L14.3 0.7L17.9 0.5L21.4 0.3L25.0 0.7L28.6 0.7L32.1 0.5L35.7 0.6L39.3 0.6L42.9 0.7L46.4 0.2L50.0 0.3L53.6 0.6L57.1 0.6L60.7 0.0L64.3 0.6L67.9 0.4L71.4 0.8L75.0 0.7L78.6 0.1L82.1 0.5L85.7 0.8L89.3 0.6L92.9 0.6L96.4 0.5L99.2 0.0L99.2 3.6L99.7 7.1L99.7 10.7L99.2 14.3L99.7 17.9L99.9 21.4L99.7 25.0L99.6 28.6L99.7 32.1L99.2 35.7L99.5 39.3L99.3 42.9L99.2 46.4L99.9 50.0L99.8 53.6L99.5 57.1L99.5 60.7L99.6 64.3L99.8 67.9L99.2 71.4L100.0 75.0L99.7 78.6L99.7 82.1L99.4 85.7L99.2 89.3L99.5 92.9L99.9 96.4L100.0 99.6L96.4 99.7L92.9 99.7L89.3 99.6L85.7 99.7L82.1 99.5L78.6 99.4L75.0 99.8L71.4 99.2L67.9 99.6L64.3 99.2L60.7 99.4L57.1 99.4L53.6 99.5L50.0 99.5L46.4 99.4L42.9 99.2L39.3 99.6L35.7 99.4L32.1 99.8L28.6 99.5L25.0 99.7L21.4 99.7L17.9 99.6L14.3 99.3L10.7 99.2L7.1 99.9L3.6 99.3L0.1 100.0L0.4 96.4L0.1 92.9L0.1 89.3L0.9 85.7L0.2 82.1L0.6 78.6L0.1 75.0L0.4 71.4L0.5 67.9L0.1 64.3L0.8 60.7L0.8 57.1L0.8 53.6L0.8 50.0L0.8 46.4L0.7 42.9L0.2 39.3L0.0 35.7L0.2 32.1L0.1 28.6L0.0 25.0L0.6 21.4L0.2 17.9L0.7 14.3L0.4 10.7L0.6 7.1L0.2 3.6Z'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
          mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0.0 0.0L3.6 0.8L7.1 0.3L10.7 0.2L14.3 0.7L17.9 0.5L21.4 0.3L25.0 0.7L28.6 0.7L32.1 0.5L35.7 0.6L39.3 0.6L42.9 0.7L46.4 0.2L50.0 0.3L53.6 0.6L57.1 0.6L60.7 0.0L64.3 0.6L67.9 0.4L71.4 0.8L75.0 0.7L78.6 0.1L82.1 0.5L85.7 0.8L89.3 0.6L92.9 0.6L96.4 0.5L99.2 0.0L99.2 3.6L99.7 7.1L99.7 10.7L99.2 14.3L99.7 17.9L99.9 21.4L99.7 25.0L99.6 28.6L99.7 32.1L99.2 35.7L99.5 39.3L99.3 42.9L99.2 46.4L99.9 50.0L99.8 53.6L99.5 57.1L99.5 60.7L99.6 64.3L99.8 67.9L99.2 71.4L100.0 75.0L99.7 78.6L99.7 82.1L99.4 85.7L99.2 89.3L99.5 92.9L99.9 96.4L100.0 99.6L96.4 99.7L92.9 99.7L89.3 99.6L85.7 99.7L82.1 99.5L78.6 99.4L75.0 99.8L71.4 99.2L67.9 99.6L64.3 99.2L60.7 99.4L57.1 99.4L53.6 99.5L50.0 99.5L46.4 99.4L42.9 99.2L39.3 99.6L35.7 99.4L32.1 99.8L28.6 99.5L25.0 99.7L21.4 99.7L17.9 99.6L14.3 99.3L10.7 99.2L7.1 99.9L3.6 99.3L0.1 100.0L0.4 96.4L0.1 92.9L0.1 89.3L0.9 85.7L0.2 82.1L0.6 78.6L0.1 75.0L0.4 71.4L0.5 67.9L0.1 64.3L0.8 60.7L0.8 57.1L0.8 53.6L0.8 50.0L0.8 46.4L0.7 42.9L0.2 39.3L0.0 35.7L0.2 32.1L0.1 28.6L0.0 25.0L0.6 21.4L0.2 17.9L0.7 14.3L0.4 10.7L0.6 7.1L0.2 3.6Z'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
}

/* polaroid: wide bottom margin that holds the caption. */
.photo-frame--polaroid .photo-frame__paper { padding:.8rem .8rem 1rem; border-radius:2px; }
.photo-frame--polaroid .photo-frame__img   { aspect-ratio:1/1; }
.photo-frame--polaroid .photo-frame__img:last-child { margin-bottom:2.4rem; }
.photo-frame--polaroid .photo-frame__caption { min-height:2.4rem; }

/* thin-border: a hairline with a small gap around the photo. */
.photo-frame--thin-border .photo-frame__paper { background:var(--bg); padding:.5rem; border:1px solid var(--text); }

/* none: tilt and shadow only. */
.photo-frame--none .photo-frame__paper { background:none; padding:0; }

/* Entrance: JS arms the frame (a larger tilt, faded out) and settles it once on
   first view. Nothing starts hidden unless JS added --armed, and reduced motion
   never arms it. */
@media (prefers-reduced-motion: no-preference) {
  .photo-frame--armed { opacity:0; rotate:calc(var(--photo-tilt) * 3);
    transition:opacity .8s ease, rotate .9s cubic-bezier(.2,.7,.2,1); }
  .photo-frame--armed.is-settled { opacity:1; rotate:var(--photo-tilt); }
}

/* split variant e: text left, framed photo right. The tilted frame's corners
   swing past the block's side edge at tablet width (7px at 810, measured by the
   compare gate); clipping the x axis only keeps the vertical overhang. */
.bk-split--e { overflow-x:clip; }
@media (min-width: 641px) {
  .bk-split--e { grid-template-columns:minmax(0, 1.1fr) minmax(0, 1fr); }
  .bk-split--e .photo-frame { order:2; }

  /* Overhang: the frame hangs out of the split into the image-led block below.
     The render pass sets .is-overhanging on the split's wrapper and .is-overhung
     on the block below only when that block is a full_bleed, banner or image
     that renders; otherwise none of these rules apply. The wrapper gives up its
     bottom margin and rises above the next block; the text inside the next block
     is pushed down by the same amount so the frame never covers it. */
  .bk-split--overhang .photo-frame { margin-bottom:calc(var(--photo-overhang) * -1); }
  .page-block.is-overhanging { position:relative; z-index:2; margin-bottom:0; }
  .page-block.is-overhung :is(.bk-full-bleed-content, .bk-banner-slide) { padding-top:var(--photo-overhang); }
}

/* Phone: the photo stacks above the text, the tilt is held to 2 degrees and
   nothing overhangs (the photo is not at the bottom of the block here). */
@media (max-width: 640px) {
  .bk-split--e .photo-frame { rotate:clamp(-2deg, var(--photo-tilt), 2deg); width:min(100%, 22rem); }
  .photo-frame--armed.is-settled { rotate:clamp(-2deg, var(--photo-tilt), 2deg); }
}
/* ── Portfolio hero block (replaces home.tmpl hero) ──────────────────────── */
.bk-portfolio-hero--cover    { position:relative; height:100vh; min-height:500px;
                               display:flex; align-items:center; justify-content:center;
                               overflow:hidden; background-size:cover; background-position:center;
                               background-color:var(--text); margin:0 0 2.5rem; }
.page-block--portfolio_hero  { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }
.bph-overlay                 { position:absolute; inset:0; background:var(--pf-black-42); }
.bph-content                 { position:relative; z-index:1; text-align:center; color:var(--pf-white);
                               padding:2rem; max-width:700px; }
.bk-portfolio-hero--cover {
  & .bph-name    { font-family:var(--font-display); font-size:clamp(2.5rem,8vw,5rem); font-weight:700;
                   letter-spacing:-.02em; line-height:1.05; margin:0 0 .75rem; }
  & .bph-tagline { font-size:clamp(1rem,2.5vw,1.4rem); font-weight:300;
                   opacity:.9; margin:0 0 2rem; }
}
.bph-cta                     { display:inline-block; padding:.85rem 2.25rem;
                               background:var(--primary); color:var(--on-primary,#fff); border-radius:calc(.4rem * var(--pf-radius-k,1));
                               font-weight:600; text-decoration:none;
  &:hover { opacity:.88; }
}
.bk-portfolio-hero--text     { padding:3rem 0 2rem; text-align:center; margin-bottom:2.5rem;
  & .bph-name    { font-family:var(--font-display); font-size:clamp(2rem,6vw,4rem); font-weight:700; margin:0 0 .5rem; }
  & .bph-tagline { font-size:1.15rem; color:var(--muted); margin:0 0 1.5rem; }
}
.bph-headline                { max-width:560px; margin:0 auto 1.5rem; color:var(--muted); }
@media(max-width:640px)      { .bk-portfolio-hero--cover { margin:0 0 2rem; height:90vh; } }

/* ── Portfolio bio block (replaces about.tmpl) ───────────────────────────── */
/* margin-inline:auto (#1472) centres the 46rem-capped box inside the
   full-band-width wrapper a tinted scheme paints: without it, variants b, c
   and d sat flush-left in a band whose colour spans the whole width. Variants
   a and e set their own grid placement (`max-width:none`) at
   min-width:760px, so this has no visual effect there once that override
   applies; below that breakpoint they fell back to this same base rule and
   had the same left-flush bug, so they gain the fix too. */
.bk-portfolio-bio            { padding:1rem 0; max-width:46rem; margin-inline:auto; margin-bottom:2.5rem; }
/* A scheme class paints the BAND and nothing else: 06-schemes-variants.css
   sets background and colour, never an inset. Blocks at the measure tier get
   their inset for free, because 02-layout.css centres their self-capped copy
   inside a wider band. This one does not: the two-column rule below releases
   `.bk-portfolio-bio`'s own 46rem cap so the content fills the band. Measured
   on /over-mij at 1440px, the terracotta band ran 1023x284 at x 208..1231
   with the heading starting at x=208 and the intro ending at x=1224: 0px of
   left inset, 7px of right, the copy flush against the band's own edges on
   both sides. One padding shorthand insets all four, replacing the base
   rule's vertical-only pair so the band reads as one padded box.

   The selector matches on the section's own scheme class, not an ancestor's
   (#1411): a columns child carries its variant/scheme directly on
   `.bk-portfolio-bio` (_blocks.tmpl) because it never gets the
   `.page-block--portfolio_bio` wrapper a top-level block does, so an
   ancestor-combinator version of this rule silently never matched it. */
.bk-portfolio-bio:not(.bk-cs-light) {
  padding:clamp(1.5rem,3vw,2.5rem);
}
.bpb-header                  { display:flex; gap:1.5rem; align-items:flex-start;
                               flex-wrap:wrap; margin-bottom:2rem; }
.bpb-avatar                  { width:120px; height:120px; border-radius:50%; object-fit:cover; flex-shrink:0; }
.bpb-info                    { flex:1; min-width:200px; }
.bpb-name                    { font-size:1.85rem; margin:0 0 .5rem; }
.bpb-location                { margin:0; color:var(--muted); }
.bpb-social                  { display:flex; flex-wrap:wrap; gap:.5rem; margin-top:.75rem;
  & a { display:inline-flex; align-items:center; gap:.35rem;
        padding:.3rem .6rem; border:1px solid var(--border);
        border-radius:calc(.4rem * var(--pf-radius-k,1)); color:inherit; text-decoration:none; font-size:.875rem; }
}
.bpb-bio                     { font-size:1.05rem;
  & p { margin:0 0 .9em 0; }
  & p:last-child { margin-bottom:0; }
  & h2 { font-size:1.4rem; margin:1.2em 0 .5em; }
  & h3 { font-size:1.15rem; margin:1em 0 .4em; }
  & ul,
  & ol { padding-left:1.3em; margin:.4em 0 .9em; }
  & blockquote { margin:.6em 0; padding:.1em 1em; border-left:3px solid var(--border); color:var(--muted); }
  & a { color:var(--accent); text-decoration:underline; }
}
.bpb-bio-empty               { color:var(--muted); font-style:italic; }
.bpb-cta                     { margin-top:2rem; padding-top:1.5rem; border-top:1px solid var(--border); }

/* ── Portfolio hero layout variants (a = base above) ──────────────────────────
   The variant class rides the .page-block--portfolio_hero wrapper (edit + live).
   Each variant restyles WHICHEVER of the two structures the template renders —
   `.bk-portfolio-hero--cover` (a cover photo is set) or `.bk-portfolio-hero--text`
   (none is) — so every variant is a real, distinct layout regardless of cover
   presence (#1501 A2: b/c used to do nothing without a cover, e did nothing
   with one, and d carried no rules at all, rendering identically to a). */
:is(.page-block--portfolio_hero,.bk-col-child--portfolio_hero) {
  /* b: Cover, caption bottom-left over a bottom-up scrim. Without a cover,
     the fallback is an explicit bordered panel: a frame standing in for the
     missing photo, not a silent drop to `a`'s plain centred look. */
  &.bk-variant-b {
    & .bph-overlay   { background:linear-gradient(to top, var(--pf-black-74) 0%, var(--pf-black-18) 55%, var(--pf-black-00) 100%); }
    & .bk-portfolio-hero--cover { align-items:flex-end; justify-content:flex-start; }
    & .bph-content   { text-align:left; max-width:960px; width:100%;
                       padding:0 clamp(1.5rem,6vw,5rem) clamp(2rem,7vw,4.5rem); }
    & .bph-cta       { margin-top:.5rem; }
    & .bk-portfolio-hero--text { text-align:left; max-width:960px; margin:0 auto 2.5rem;
                       padding:2.5rem clamp(1.5rem,6vw,4rem); border:1px solid var(--border);
                       border-radius:calc(1rem * var(--pf-radius-k,1)); background:var(--card); }
  }
  /* c: Full-bleed cover, overlay title: display name only, hairline scrim,
     tracked-out caps (the caps/hidden-tagline rules below already apply to
     both structures, since .bph-name/.bph-tagline exist in either one — only
     the BAND itself was missing without a cover). Without a cover, the same
     caps treatment sits on a tinted full-bleed band instead of on nothing,
     so the block still reads as a statement band either way. */
  &.bk-variant-c {
    & .bph-overlay   { background:var(--pf-black-30); }
    & .bph-tagline,
    & .bph-cta       { display:none; }
    & .bph-name      { text-transform:uppercase; letter-spacing:.14em;
                       font-weight:600; font-size:clamp(1.8rem,5.5vw,3.6rem); margin:0; }
    /* Text branch: same "display name only" reduction, on an inverted band.
       bph-headline uses --muted, calibrated for the scheme's normal
       background, not this flipped one, so it's hidden here rather than
       carried over at the wrong contrast (caught by the contrast gate). */
    & .bk-portfolio-hero--text {
                       background:var(--text); color:var(--bg);
                       padding:clamp(3rem,8vw,6rem) 1.5rem; margin-bottom:2.5rem; }
    & .bk-portfolio-hero--text .bph-headline { display:none; }
  }
  /* d: Centred, framed: the picker's "text_centered" label (blockDefs.ts) —
     `a` IS the plain centred cover overlay, so `d` earns its own identity by
     framing rather than re-centring: an inset card with room around it,
     instead of `a`'s edge-to-edge bleed. This is also the exact bug in
     #1501 — d carried no rules at all, so a cover rendered pixel-identical
     to `a`. Without a cover, the same framed-card idea carries over as a
     bordered box around the centred text. */
  &.bk-variant-d {
    & .bk-portfolio-hero--cover { height:64vh; min-height:24rem; max-width:var(--w-wide,1600px);
                       margin-inline:auto; margin-bottom:2.5rem;
                       border-radius:1.25rem; box-shadow:0 30px 70px -32px var(--pf-black-45); }
    & .bk-portfolio-hero--cover .bph-content { max-width:min(var(--w-measure,820px), 100% - var(--w-gutter,3rem)); }
    & .bk-portfolio-hero--text {
                       max-width:38rem; margin:0 auto 2.5rem; text-align:center;
                       padding:3rem clamp(1.5rem,5vw,3rem); border:1px solid var(--border);
                       border-radius:1.25rem; background:var(--card); }
  }
  /* e: Left-aligned editorial: the picker's "text_editorial" label — text-left
     within the page measure, an accent rule beside the headline. With a
     cover, the photo becomes a lower, left-anchored band the copy sits over
     instead of a full-height centred hero, so the editorial-caption identity
     holds with or without one. */
  &.bk-variant-e {
    & .bk-portfolio-hero--text {
                       text-align:left; max-width:960px; margin:0 auto;
                       padding:3.5rem clamp(1rem,4vw,1.5rem) 2.5rem; }
    & .bph-name      { font-size:clamp(2.4rem,7vw,4.5rem); }
    & .bph-tagline   { color:var(--muted); }
    & .bph-headline  { margin:0 0 1.75rem; padding-left:1.25rem;
                       border-left:3px solid var(--primary); max-width:34rem; }
    & .bk-portfolio-hero--cover { height:56vh; min-height:22rem;
                       align-items:center; justify-content:flex-start; }
    & .bk-portfolio-hero--cover .bph-overlay {
                       background:linear-gradient(to right, var(--pf-black-62) 0%, var(--pf-black-15) 55%, var(--pf-black-00) 85%); }
    & .bk-portfolio-hero--cover .bph-content { text-align:left; max-width:640px;
                       padding:0 clamp(1.5rem,6vw,5rem); }
  }
}

/* ── Portfolio bio layout variants (a = base above) ───────────────────────── */
:is(.page-block--portfolio_bio,.bk-col-child--portfolio_bio) {
  /* b: Centered: avatar on top, header + social centered, bio AND cta centred
     too (#1451: this used to centre only .bpb-header, leaving the bio and the
     CTA button flush against the block's left edge under a centred name). */
  &.bk-variant-b            { margin-left:auto; margin-right:auto;
    & .bpb-header { flex-direction:column; align-items:center; text-align:center; }
    & .bpb-avatar { width:150px; height:150px; }
    & .bpb-info   { flex:0 1 auto; min-width:0; }
    & .bpb-social { justify-content:center; }
    & .bpb-bio    { max-width:34rem; margin-inline:auto; text-align:center; }
    & .bpb-cta    { text-align:center; }
  }
  /* c: Editorial: display-font name, ruled header, drop cap. The avatar is
     gated by the block's own show_avatar field (#1452), template-side
     (_blocks.tmpl), not hidden unconditionally here: a fixed `display:none`
     left no way for a photographer to turn it back on. */
  &.bk-variant-c            { max-width:44rem;
    & .bpb-header { border-bottom:1px solid var(--border);
                    padding-bottom:1.25rem; margin-bottom:1.75rem; }
    & .bpb-name   { font-family:var(--font-display); font-size:2.4rem; letter-spacing:-.01em; }
    & .bpb-bio    { font-size:1.15rem; line-height:1.75; }
    & .bpb-bio p:first-child::first-letter {
                    float:left; font-family:var(--font-display); font-weight:700;
                    font-size:3.4rem; line-height:.72; padding:.06em .1em 0 0; color:var(--primary); }
  }
  /* d: Card / boxed: the whole bio sits on a bordered card. The fixed
     background/border below is the LIGHT-scheme default; 06-schemes-variants
     .css overrides both per bk-cs-* so the card derives from the active
     scheme's own ground instead of a tint of the light palette's --text
     (#1453). */
  &.bk-variant-d            { margin-left:auto; margin-right:auto;
                    background:var(--card); border:1px solid var(--border);
                    border-radius:calc(1rem * var(--pf-radius-k,1)); padding:2.25rem clamp(1.5rem,4vw,2.5rem);
                    box-shadow:0 1px 3px var(--pf-black-06);
    & .bpb-cta { border-top-color:var(--border); }
  }
}

/* a: Avatar left, media-object: the avatar sits in its own narrow left
   column; heading, location, bio and CTA stack together in the wider column
   beside it (#1450). This used to be grouped into variant e's two-column
   grid rule (#1352), which made the two variants render identically. They
   are now two separate rules with two different shapes: here the avatar is
   the only thing in its own column and everything else reads as one stacked
   block; in e (below) the whole header stays together in the left column and
   only the bio moves to the right.

   `.bpb-header` is `display:contents` in this variant only: the header's own
   box (and its flex layout) exists purely to lay the avatar and the name out
   side-by-side in the DEFAULT single-column stack. Removing that box here
   lets its two children (.bpb-avatar, .bpb-info) become direct items of the
   section's own grid, alongside .bpb-bio and .bpb-cta, which is what puts the
   avatar in column 1 while info/bio/cta occupy three separate rows of
   column 2.

   This block doesn't carry its own width tier (02-layout.css leaves it at
   the default 1360px band in every scheme), so a narrow avatar column beside
   a 46rem measure would still read as a left-biased object with dead ground
   to its right (#1352 section B); widening the wrapper to 64rem here keeps
   the two-column shape filling the band the way variant e's does. */
@media(min-width:760px) {
  .page-block--portfolio_bio.bk-variant-a {
    max-width:64rem; margin-inline:auto;
  }
  .bk-portfolio-bio.bk-variant-a {
    max-width:none; display:grid;
    grid-template-columns:minmax(96px,140px) 1fr;
    column-gap:1.75rem; row-gap:1.5rem; align-items:start;
    & .bpb-header { display:contents; }
    & .bpb-avatar { grid-column:1; grid-row:1 / span 3; width:100%; height:auto;
                    aspect-ratio:1; }
    & .bpb-info   { grid-column:2; grid-row:1; }
    & .bpb-bio,
    & .bpb-bio-empty { grid-column:2; grid-row:2; margin:0; }
    & .bpb-cta { grid-column:2; grid-row:3; margin-top:0; border-top:none; }
  }
}

/* e: Two column: header (left, sticky, avatar + name + location together)
   beside the bio (right); CTA follows the bio in the same column.

   The grid goes on `.bk-portfolio-bio`, the <section> that actually holds
   .bpb-header / .bpb-bio / .bpb-cta. It used to go on the `.page-block--…`
   wrapper, whose only child IS that section: one grid item, auto-placed into
   track 1, so `grid-column` on the three inner elements matched nothing and
   the whole bio rendered inside a 325px column with an empty second track
   beside it (#1312). The wrapper keeps the width cap; the section drops its
   own 46rem measure, which the two tracks now set instead.

   The grid selector below matches `.bk-portfolio-bio.bk-variant-e` directly,
   not through the `.page-block--portfolio_bio` ancestor (#1411): a columns
   child carries its variant on that section itself (_blocks.tmpl) because it
   never gets that wrapper, so the ancestor-combinator form silently fell
   back to the single-column stack (with its border-top still on) for a
   scheme'd about block nested in a column. */
@media(min-width:760px) {
  .page-block--portfolio_bio.bk-variant-e {
    max-width:64rem; margin-inline:auto;
  }
  .bk-portfolio-bio.bk-variant-e {
    max-width:none; display:grid;
    grid-template-columns:minmax(220px,1fr) 2fr;
    grid-template-rows:auto 1fr; gap:0 3rem; align-items:start;
    /* The header spans both rows, so its own height (a 120px avatar over a
       two-line name) sets the band's height without pushing the bio's row
       down and leaving the second column standing in empty ground.

       The name comes FIRST inside that column. Stacked avatar-then-name,
       it landed 156px below the top of the paragraph it introduces
       (measured on /over-mij at 1440px), so the reading order across the
       band was portrait, body, heading. `order` rather than
       `column-reverse`: the DOM keeps avatar-then-info, which is what a
       screen reader should hear. */
    & .bpb-header { grid-column:1; grid-row:1 / span 2; flex-direction:column;
                    margin-bottom:0; position:sticky; top:2rem; }
    & .bpb-avatar { order:2; }
    & .bpb-info   { order:1; }
    & .bpb-bio,
    & .bpb-bio-empty { grid-column:2; grid-row:1; margin:0; }
    /* The CTA follows the bio in the same column, close under the text it
       belongs to. Spanning both columns drew its 1px top border the full
       width of the tinted band, where it read as a seam across the colour;
       the rule earns its keep in the stacked layout, which still has it. */
    & .bpb-cta { grid-column:2; grid-row:2; align-self:start;
                 margin-top:1.75rem; padding-top:0; border-top:none; }
  }
}
/* ── gallery_slideshow variant d: peek carousel (#2230) ───────────────────────
   Tall 3:4 covers, about 4.5 visible on desktop so the cards at both edges are
   cut off by the viewport: the cut-off IS the "this scrolls" cue. The hovered
   card washes its photo out and shows the title and an outlined pill; on a
   device with no hover those are always on, over a bottom gradient.

   Everything is scoped under .cp-peek, so nothing here can reach variants a-c.
   Colours come from the site tokens (--bg, --text, --border) and the --pf-*
   scrims; no literals. */

/* Full-bleed band like variant c; in a columns cell it fills the cell instead
   (#1976), so only the page-level wrapper breaks out. */
.page-block--gallery_slideshow.bk-variant-d { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }

:is(.page-block--gallery_slideshow,.bk-col-child--gallery_slideshow).bk-variant-d .cp-slideshow-heading { padding-inline:clamp(1rem,4vw,3rem); }

.cp-peek {
  --peek-gap:.75rem; --peek-pad:clamp(1rem,4vw,3rem);
  /* Cards per row and the gaps between them: the row is n cards + g gaps wide. */
  --peek-n:1.35; --peek-g:1;
  position:relative;
}
@media (min-width:640px)  { .cp-peek { --peek-n:2.5; --peek-g:2; } }
@media (min-width:1024px) { .cp-peek { --peek-n:4.5; --peek-g:4; } }

.cp-peek-track {
  position:relative; display:flex; gap:var(--peek-gap); overflow-x:auto;
  padding-inline:var(--peek-pad); scroll-padding-inline:var(--peek-pad);
  scroll-snap-type:x mandatory; overscroll-behavior-x:contain;
  scrollbar-width:none; -webkit-overflow-scrolling:touch; cursor:grab;
  &::-webkit-scrollbar { display:none; }
  &.is-dragging { scroll-snap-type:none; cursor:grabbing; user-select:none; }
}
.cp-peek-slide { flex:0 0 calc((100% - var(--peek-g) * var(--peek-gap)) / var(--peek-n)); scroll-snap-align:start; min-width:0; }

.cp-peek-card { position:relative; display:block; color:var(--text); text-decoration:none; border-radius:calc(.35rem * var(--pf-radius-k,1)); overflow:hidden; -webkit-user-drag:none; }
.cp-peek-media {
  position:relative; overflow:hidden; aspect-ratio:3/4; background:var(--border);
  & img { width:100%; height:100%; object-fit:cover; display:block; -webkit-user-drag:none; user-select:none; }
  & .cp-gal-ph { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; padding:1rem; text-align:center; }
}

/* The reveal layer. Default (no hover available): title and pill at the bottom
   of the card over a gradient, always visible. */
.cp-peek-veil {
  position:absolute; inset:0; display:flex; flex-direction:column; justify-content:flex-end; align-items:flex-start; gap:.6rem; padding:1.1rem;
  color:var(--pf-white); background:linear-gradient(to top, var(--pf-black-65), var(--pf-black-00) 55%);
  pointer-events:none;
}
.cp-peek-title { font-weight:600; font-size:1.05rem; line-height:1.25; }
.cp-peek-pill {
  display:inline-block; padding:.5rem 1.05rem; border:1.5px solid currentColor; border-radius:999px;
  font-size:.82rem; font-weight:500; line-height:1.2; letter-spacing:.01em;
}

/* Hover-capable pointer: the veil appears on the hovered or keyboard-focused
   card only. A light wash (the site background at 72%) over the photo, with
   centred text in the site's text colour. */
@media (hover:hover) and (pointer:fine) {
  .cp-peek-veil {
    justify-content:center; align-items:center; text-align:center; padding:1.25rem;
    color:var(--text); background:color-mix(in srgb, var(--bg) 72%, transparent);
    opacity:0;
  }
  .cp-peek-card:hover .cp-peek-veil,
  .cp-peek-card:focus-visible .cp-peek-veil { opacity:1; }
  .cp-peek-card:focus-visible { outline:2px solid var(--primary); outline-offset:-2px; }
  @media (prefers-reduced-motion:no-preference) {
    .cp-peek-veil { transition:opacity .25s ease; }
  }
}

/* peek.ts sets hidden on the arrows and dots when every card already fits; the
   display values below would otherwise win over the attribute. */
.cp-peek [hidden] { display:none; }

/* Arrows overlay the edge cards. */
.cp-peek-nav {
  position:absolute; top:calc(50% - 22px); z-index:2; width:44px; height:44px; border-radius:50%; border:none; cursor:pointer;
  background:var(--pf-slate-60); color:var(--pf-white); font-size:1.6rem; line-height:1; padding:0;
  display:flex; align-items:center; justify-content:center;
  &:hover { background:var(--pf-slate-85); }
  &:focus-visible { outline:2px solid var(--primary); outline-offset:2px; }
}
.cp-peek-prev { left:calc(var(--peek-pad) + .5rem); }
.cp-peek-next { right:calc(var(--peek-pad) + .5rem); }

/* One dot per gallery. The button is the 44px target; the visible dot is its child. */
.cp-peek-dots { display:flex; flex-wrap:wrap; justify-content:center; margin-top:.5rem; padding-inline:var(--peek-pad); }
.cp-peek-dot {
  width:44px; height:44px; padding:0; border:none; background:none; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  & span { width:.5rem; height:.5rem; border-radius:50%; background:var(--border); }
  &[aria-current="true"] span { background:var(--text); }
  &:focus-visible { outline:2px solid var(--primary); outline-offset:-4px; border-radius:50%; }
}

/* Optional footer pill under the track. */
.cp-peek-footer { display:flex; justify-content:center; margin-top:clamp(1rem,2.5vw,1.75rem); padding-inline:var(--peek-pad); }
.cp-peek-footer-pill {
  display:inline-flex; align-items:center; justify-content:center; min-height:44px; padding:.55rem 1.4rem;
  border:1.5px solid var(--text); border-radius:999px; color:var(--text); text-decoration:none; font-weight:500; font-size:.95rem;
  &:hover { background:var(--text); color:var(--bg); }
  &:focus-visible { outline:2px solid var(--primary); outline-offset:3px; }
}
/* ── Gallery list block ───────────────────────────────────────────────────── */
/* Sized for the wide tier:
     · auto-FIT, not auto-fill: auto-fill keeps empty trailing tracks, so three
       galleries in a 1600px row left a dead column on the right.
     · a 320px floor stops the row packing into cramped cards as it widens.
     · the max MUST stay 1fr. With a fixed max (e.g. 520px) the track count is
       computed from that max, so a 1600px row fits only two: the cards get a
       ceiling via .cp-gal-card instead, which also stops a lone gallery
       ballooning to 1600px (its 4/3 cover would become a ~1200px-tall slab). */
.cp-gal-grid      { display:grid; grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
                    justify-items:center; gap:1.5rem; margin:1rem 0 0; }
.cp-gal-card      { border:1px solid var(--border); border-radius:calc(.5rem * var(--pf-radius-k,1)); overflow:hidden;
                    background:var(--card); text-decoration:none; color:inherit;
                    width:100%; max-width:520px;
                    display:flex; flex-direction:column; transition:box-shadow .2s,transform .2s;
  &:hover { box-shadow:0 6px 24px var(--pf-black-12); transform:translateY(-2px); }
}
.cp-gal-cover     { position:relative; aspect-ratio:4/3; overflow:hidden; background:var(--border);
  & img { width:100%; height:100%; object-fit:cover; display:block; transition:transform .35s ease; }
}
.cp-gal-card:hover .cp-gal-cover img { transform:scale(1.04); }
.cp-gal-body      { padding:.75rem 1rem 1rem; display:flex; flex-direction:column; gap:.2rem; }
.cp-gal-title     { font-weight:700; font-size:1rem; color:var(--text); }
.cp-gal-meta      { font-size:.8rem; color:var(--muted); margin:0; }
.cp-gal-desc      { font-size:.875rem; color:var(--muted); margin:.25rem 0 0; }
/* body[class] outranks a theme's `.tpl-<name> .cp-gal-grid` override (both are
   0,2,0 otherwise), so this floor always wins below 480px regardless of which
   theme's minmax() would otherwise force a track wider than the viewport. */
@media(max-width:480px) { body[class] .cp-gal-grid { grid-template-columns:1fr; } }
.cp-gal-empty { grid-column:1/-1; padding:3rem 1rem; text-align:center; color:var(--muted); font-size:.9rem; }
.cp-gal-badge { position:absolute; top:.5rem; left:.5rem; padding:.15rem .5rem; border-radius:calc(.3rem * var(--pf-radius-k,1));
                background:var(--pf-slate-82); color:var(--pf-white); font-size:.68rem; font-weight:700;
                letter-spacing:.04em; text-transform:uppercase; z-index:2; }

/* ── gallery_cover (one featured gallery, format = variant) ───────────────── */
.cp-cover-card    { display:block; text-decoration:none; color:var(--text); border-radius:var(--pf-radius-md); overflow:hidden; }
.cp-cover-media   { position:relative; overflow:hidden; background:var(--border);
  & img { width:100%; height:100%; object-fit:cover; display:block; transition:transform .4s ease; }
}
.cp-cover-card:hover .cp-cover-media img { transform:scale(1.03); }
.cp-cover-title   { font-weight:700; font-size:clamp(1.25rem,2.5vw,1.9rem); line-height:1.1; }
.cp-cover-meta    { font-size:.85rem; color:var(--muted); margin:.2rem 0 0; }
.cp-cover-desc    { color:var(--muted); margin:.6rem 0 0; }
.cp-cover-cta     { display:inline-block; margin-top:.9rem; font-size:.78rem; letter-spacing:.1em; text-transform:uppercase; color:var(--primary); }
/* variant a (default): full-bleed hero with overlaid info */
.page-block--gallery_cover.bk-variant-a {
  & .cp-cover-card  { position:relative; }
  & .cp-cover-media { aspect-ratio:16/9; }
  & .cp-cover-media::after { content:""; position:absolute; inset:0; background:linear-gradient(to top, var(--pf-black-62), var(--pf-black-05)); }
  & .cp-cover-info  { position:absolute; left:0; right:0; bottom:0; z-index:1; padding:clamp(1.25rem,3vw,2.5rem); color:var(--pf-white); }
  & .cp-cover-meta,
  & .cp-cover-desc  { color:var(--pf-white-85); }
  & .cp-cover-cta   { color:var(--pf-white); }
}
/* variant b: framed card: image on top, info below */
.page-block--gallery_cover.bk-variant-b {
  & .cp-cover-card  { border:1px solid var(--border); }
  & .cp-cover-media { aspect-ratio:3/2; }
  & .cp-cover-info  { padding:1.25rem 1.5rem 1.5rem; }
}
/* variant c: split: image beside info */
.page-block--gallery_cover.bk-variant-c {
  & .cp-cover-card  { display:grid; grid-template-columns:1fr 1fr; align-items:stretch; border:1px solid var(--border); }
  & .cp-cover-media { min-height:300px; }
  & .cp-cover-info  { display:flex; flex-direction:column; justify-content:center; padding:clamp(1.5rem,3vw,3rem); }
}
@media(max-width:640px){ .page-block--gallery_cover.bk-variant-c .cp-cover-card { grid-template-columns:1fr; } .page-block--gallery_cover.bk-variant-c .cp-cover-media { min-height:220px; } }
/* variant d: full-bleed cinematic 21:9 with overlaid info, edge-to-edge */
.page-block--gallery_cover.bk-variant-d {
  & .cp-cover-card  { position:relative; border-radius:0; }
  & .cp-cover-media { aspect-ratio:21/9; }
  & .cp-cover-media::after { content:""; position:absolute; inset:0; background:linear-gradient(105deg, var(--pf-black-70) 0%, var(--pf-black-25) 45%, var(--pf-black-00) 75%); }
  & .cp-cover-info  { position:absolute; left:0; top:50%; transform:translateY(-50%); z-index:1; max-width:min(90%,640px); padding:clamp(1.5rem,5vw,4rem); color:var(--pf-white); }
  & .cp-cover-title { font-size:clamp(1.6rem,4vw,3rem); }
  & .cp-cover-meta,
  & .cp-cover-desc  { color:var(--pf-white-85); }
  & .cp-cover-cta   { color:var(--pf-white); }
}
@media(max-width:640px){ .page-block--gallery_cover.bk-variant-d .cp-cover-media { aspect-ratio:4/3; } }
/* variant e: portrait 3:4 image beside vertically-centered text */
.page-block--gallery_cover.bk-variant-e {
  & .cp-cover-card  { display:grid; grid-template-columns:minmax(0,3fr) minmax(0,4fr); gap:clamp(1.5rem,4vw,3.5rem); align-items:center; }
  & .cp-cover-media { aspect-ratio:3/4; border-radius:var(--pf-radius-md); }
  & .cp-cover-info  { display:flex; flex-direction:column; justify-content:center; padding:.25rem; }
  & .cp-cover-title { font-size:clamp(1.5rem,3.2vw,2.4rem); }
}
@media(max-width:640px){ .page-block--gallery_cover.bk-variant-e .cp-cover-card { grid-template-columns:1fr; gap:1.25rem; } .page-block--gallery_cover.bk-variant-e .cp-cover-media { aspect-ratio:4/3; } }
/* variant f: overlay minimal: big image, title only, no meta/desc/cta chrome */
.page-block--gallery_cover.bk-variant-f {
  & .cp-cover-card  { position:relative; border-radius:var(--pf-radius-md); }
  & .cp-cover-media { aspect-ratio:16/9; }
  & .cp-cover-media::after { content:""; position:absolute; inset:0; background:linear-gradient(to top, var(--pf-black-50), var(--pf-black-00) 55%); }
  & .cp-cover-info  { position:absolute; left:0; right:0; bottom:0; z-index:1; padding:clamp(1.25rem,3vw,2.25rem); color:var(--pf-white); text-align:center; }
  & .cp-cover-title { font-size:clamp(1.3rem,3vw,2.2rem); letter-spacing:.02em; }
  & .cp-cover-meta,
  & .cp-cover-desc,
  & .cp-cover-cta   { display:none; }
}
/* variant g: postcard: bordered card with generous padding + inset image */
:is(.page-block--gallery_cover,.bk-col-child--gallery_cover).bk-variant-g {
  & .cp-cover-card  { border:1px solid var(--border); border-radius:calc(.75rem * var(--pf-radius-k,1)); background:var(--card); padding:clamp(1.25rem,3vw,2.5rem); }
  & .cp-cover-media { aspect-ratio:3/2; border-radius:calc(.5rem * var(--pf-radius-k,1)); }
  & .cp-cover-info  { padding:1.4rem .25rem 0; text-align:center; }
  & .cp-cover-cta   { margin-top:1.1rem; }
}

/* ── gallery_slideshow (cover carousel over selected galleries) ───────────── */
.cp-slideshow         { position:relative; }
.cp-slideshow-track   { display:flex; gap:1rem; overflow-x:auto; scroll-snap-type:x mandatory; scroll-behavior:smooth; -webkit-overflow-scrolling:touch; padding-bottom:.5rem;
  &::-webkit-scrollbar { height:0; }
}
.cp-slide             { position:relative; flex:0 0 clamp(260px,60%,520px); scroll-snap-align:center; text-decoration:none; color:var(--pf-white); border-radius:var(--pf-radius-md); overflow:hidden; background:var(--border); }
.cp-slide-media       { aspect-ratio:16/10;
  & img { width:100%; height:100%; object-fit:cover; display:block; }
}
.cp-slide-caption     { position:absolute; left:0; right:0; bottom:0; display:flex; flex-direction:column; gap:.1rem; padding:1rem 1.25rem; background:linear-gradient(to top, var(--pf-black-65), transparent); }
.cp-slide-title       { font-weight:700; font-size:1.1rem; }
.cp-slide-meta        { font-size:.8rem; opacity:.85; }
.cp-slideshow-nav     { position:absolute; top:50%; transform:translateY(-50%); z-index:2; width:44px; height:44px; border-radius:50%; border:none; cursor:pointer; background:var(--pf-slate-60); color:var(--pf-white); font-size:1.5rem; line-height:1; display:flex; align-items:center; justify-content:center;
  &:hover { background:var(--pf-slate-85); }
}
.cp-slideshow-prev    { left:.5rem; } .cp-slideshow-next { right:.5rem; }
/* variant b: full width */
.page-block--gallery_slideshow.bk-variant-b { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .cp-slideshow-track { padding-inline:clamp(1rem,4vw,3rem); }
}

/* Optional band heading above the slideshow track. */
.cp-slideshow-heading { text-align:center; margin:0 0 clamp(1.5rem,4vw,3rem);
                        font-size:clamp(1.8rem,3.6vw,3rem); line-height:1.1; font-weight:600; }

/* variant c: full-bleed strip, captions BELOW the image. overlaid,
   with neighbouring slides peeking in at both edges. The overlay caption in the
   default variant fights busy photography; below the image it always reads. */
.page-block--gallery_slideshow.bk-variant-c { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }
/* In a columns cell the strip fills the cell instead of breaking out to the
   viewport (#1976): the box carries no bleed, the rest of the variant applies. */
:is(.page-block--gallery_slideshow,.bk-col-child--gallery_slideshow).bk-variant-c {
  & .cp-slideshow-track { padding-inline:clamp(1rem,4vw,3rem); gap:1.25rem; scroll-snap-type:x proximity; }
  /* <100% keeps the next slide visible at the edge: the peek IS the affordance
     that says "this scrolls", which matters most without visible scrollbars. */
  & .cp-slide { flex:0 0 clamp(280px,30%,560px); border-radius:0; background:none; color:inherit; }
  & .cp-slide-media { aspect-ratio:4/5; }
  & .cp-slide-caption { position:static; background:none; padding:.75rem 0 0; gap:.15rem; }
  & .cp-slide-title { font-weight:400; font-size:1rem; color:var(--text); }
  & .cp-slide-meta { color:var(--muted); }
  & .cp-slideshow-nav { background:var(--pf-slate-55); }
  & .cp-slideshow-prev { left:clamp(1rem,4vw,3rem); }
  & .cp-slideshow-next { right:clamp(1rem,4vw,3rem); }
}
/* ── Block colour schemes ─────────────────────────────────────────────────── */
/* Applied to the block wrapper; sets local bg/fg CSS vars consumed by inner elements */
.bk-cs-light        { background:var(--bg);         color:var(--text); }
.bk-cs-light-accent { background:var(--bg-accent,color-mix(in srgb,var(--primary) 10%,var(--bg)));
                      color:var(--text); }
.bk-cs-accent       { background:var(--primary);    color:var(--on-primary,#fff); }
/* Body/secondary copy on the accent (primary) background must derive from the
   luminance-aware on-primary tone, not the theme's --muted (which is keyed to
   --text and goes unreadable on a light accent). Higher specificity than the
   block's own p rules so it wins on light OR dark accents. */
.bk-cs-accent :is(h1,h2,h3,h4,h5,strong,dt,.bk-statement-heading) { color:var(--on-primary,#fff); }
.bk-cs-accent :is(p,li,dd,figcaption,footer,summary,.bk-callout p) { color:var(--on-primary-muted,color-mix(in srgb,var(--on-primary,#fff) 72%,transparent)); }
.bk-cs-dark-accent  { background:var(--dark-bg,#1e293b); color:var(--dark-text,#f1f5f9); }
.bk-cs-dark         { background:var(--dark-bg,#0b0b0b); color:var(--dark-text,#f1f5f9); }
/* The two dark bands need the same treatment as the accent above, for the same
   reason: the wrapper's `color` only reaches text that INHERITS it, and a good
   deal of secondary copy sets its own: a FAQ answer's <dd>, a quote's <footer>,
   .bk-services-desc, .bk-stat-label all resolve to var(--muted), which is keyed
   to the LIGHT palette's --text. On a dark band that is near-black on
   near-black: a quote's attribution or a whole FAQ answer simply disappears.
   Same specificity trick as the accent rules (a class inside the :is() list) so
   these out-rank the blocks' own colour declarations. */
.bk-cs-dark-accent :is(h1,h2,h3,h4,h5,strong,dt,.bk-statement-heading) { color:var(--dark-text,#f1f5f9); }
.bk-cs-dark-accent :is(p,li,dd,figcaption,footer,summary,.bk-callout p) {
  color:color-mix(in srgb,var(--dark-text,#f1f5f9) 74%,transparent); }
.bk-cs-dark :is(h1,h2,h3,h4,h5,strong,dt,.bk-statement-heading) { color:var(--dark-text,#f1f5f9); }
.bk-cs-dark :is(p,li,dd,figcaption,footer,summary,.bk-callout p) {
  color:color-mix(in srgb,var(--dark-text,#f1f5f9) 74%,transparent); }

/* ── Light block nested inside a coloured band (#1485) ─────────────────────
   A `columns`/`section` child that keeps its own `light`/`light-accent`
   scheme renders `.bk-text.bk-cs-light` etc. directly (no `.page-block`
   wrapper: render_block skips it for a column child), so it already carries
   the RIGHT class. What went wrong is the three rules above: they're plain
   descendant selectors, `.bk-cs-dark :is(h1,…)`, which match every h1 under
   a dark ancestor regardless of how many scheme boundaries sit in between,
   and at equal (0,2,0) specificity the later one in this file wins
   the tie — always the dark/accent rule, never the light child's own. The
   light child's ink then came out `#f6f6f3` on its own `#fafaf9` ground
   (1.04:1), on every columns variant, on any of the three non-light schemes.

   Fixed the same way #1460 fixed --muted for this exact shape: a rule that
   is more specific than the ink rules above by naming BOTH the coloured
   ancestor and the light scheme it's nesting, so it only ever matches the
   broken case and leaves an un-nested light block (the overwhelming common
   case) untouched. */
:is(.bk-cs-accent,.bk-cs-dark-accent,.bk-cs-dark) :is(.bk-cs-light,.bk-cs-light-accent) :is(h1,h2,h3,h4,h5,strong,dt,.bk-statement-heading) {
  color:var(--text);
}
:is(.bk-cs-accent,.bk-cs-dark-accent,.bk-cs-dark) :is(.bk-cs-light,.bk-cs-light-accent) :is(p,li,dd,figcaption,footer,summary,.bk-callout p) {
  color:var(--muted);
}
/* Scheme-local --muted (#1460). The :is() lists above only reach elements by
   tag, and a lot of secondary copy sets `color:var(--muted)` on a class of its
   own (.bk-lc-sub, .bk-process-step-body, .bk-services-desc, .bk-stat-label,
   .bk-testimonial-role, .bk-faq-body), which beats an inherited colour and
   painted the light palette's grey on the accent and dark bands at 1.0 to
   3.7:1. --muted is only ever a text colour, so each scheme redefines it for
   its own ground. The light schemes reset it, so a light block nested in a
   dark column gets the page value back; light-accent takes the tone the theme
   floored against its tinted ground (theme_vars.go). */
:root { --pf-muted-root:var(--muted); }
.bk-cs-light        { --muted:var(--pf-muted-root); }
.bk-cs-light-accent { --muted:var(--muted-light-accent,var(--pf-muted-root)); }
.bk-cs-accent       { --muted:var(--on-primary-muted,color-mix(in srgb,var(--on-primary,#fff) 72%,transparent)); }
.bk-cs-dark-accent,
.bk-cs-dark         { --muted:color-mix(in srgb,var(--dark-text,#f1f5f9) 74%,transparent); }

/* Scheme-local --primary-ink (#1483). A handful of elements paint the brand
   colour itself as TEXT (a label, a stat value, an outline number) rather
   than inheriting the wrapper's `color`, so the same :is() rules above never
   reach them. `--primary-ink` is already floored for the page's own ground
   (theme_vars.go); each scheme redefines it for its OWN ground the same way
   --muted is redefined just above: on-primary on the accent band (the ground
   IS the brand colour there, so the ink can only ever be on-primary), and a
   value theme_vars.go floors against --dark-bg / the light-accent tint on the
   other two. The light schemes reset it, so a light block nested in a
   coloured column gets the page value back. */
:root { --pf-primary-ink-root:var(--primary-ink); }
.bk-cs-light        { --primary-ink:var(--pf-primary-ink-root); }
.bk-cs-light-accent { --primary-ink:var(--primary-ink-light-accent,var(--pf-primary-ink-root)); }
.bk-cs-accent       { --primary-ink:var(--on-primary,#fff); }
.bk-cs-dark-accent,
.bk-cs-dark         { --primary-ink:var(--primary-ink-dark,var(--pf-primary-ink-root)); }

/* --primary-ink-lg (#1483 follow-up): the same per-scheme resolution as
   --primary-ink above, just floored at the WCAG large-text 3:1 ratio
   (theme_vars.go's primaryInkLgVars) instead of 4.5:1. Only .bk-stat-value
   and .bk-services-price use it (below): both are bold display numerals well
   past the large-text threshold. light/light-accent reuse the plain root
   value (already 4.5:1-floored, a strictly higher bar) rather than a
   dedicated light-accent-lg token nothing needs; accent's ground IS the
   brand colour, so its ink can only ever be on-primary regardless of size. */
:root { --pf-primary-ink-lg-root:var(--primary-ink-lg); }
.bk-cs-light        { --primary-ink-lg:var(--pf-primary-ink-lg-root); }
.bk-cs-light-accent { --primary-ink-lg:var(--pf-primary-ink-lg-root); }
.bk-cs-accent       { --primary-ink-lg:var(--on-primary,#fff); }
.bk-cs-dark-accent,
.bk-cs-dark         { --primary-ink-lg:var(--primary-ink-dark-lg,var(--pf-primary-ink-lg-root)); }
/* Hairlines keyed to the light palette vanish on a dark band too: lift them to
   a tint of the band's own foreground so cards and rules keep their edges. */
.bk-cs-dark-accent :is(.bk-faq-item,.bk-divider),
.bk-cs-dark :is(.bk-faq-item,.bk-divider) {
  border-color:var(--pf-hairline-dark);
}

/* ── Gallery card/title ink on a coloured ground (#1511) ────────────────────
   05-galleries.css sets `.cp-gal-title`/`.cp-cover-card`/variant c's
   `.cp-slide-title` to `color:var(--text)` directly, not `--muted` (which IS
   already redefined per scheme above), so their ink is stuck at the light
   theme's value under every scheme: near-black on the accent ground (a
   headline-weight title still needs the small-text 4.5:1 floor at this font
   size) and the same near-black-on-near-black loss the two dark bands
   already needed --muted fixed for. `.cp-cover-card` covers `.cp-cover-title`
   too: neither sets its own colour, so both inherit from whichever ancestor
   last set one, and the a/d/f overlay variants already give `.cp-cover-info`
   its own `#fff`, closer in the tree, so this leaves them untouched.
   Chained onto `.page-block` too, at (0,3,0): variant c's own `.cp-slide-title`/
   `.cp-slide-meta` (05-galleries.css, one selector down) already need a
   variant class to strip the default overlay treatment, which puts THAT rule
   at (0,3,0) too - a tie this file's later load order (06 > 05) still wins,
   but only once the specificity actually matches. `.bk-col-child` is the box a
   columns child gets in place of `.page-block` (_blocks.tmpl, #1917); `:is()`
   takes the larger of its arguments' specificity, so both stay at (0,3,0). */
:is(.page-block,.bk-col-child).bk-cs-accent :is(.cp-gal-title,.cp-cover-card,.cp-slide-title) { color:var(--on-primary,#fff); }
:is(.page-block,.bk-col-child).bk-cs-dark-accent :is(.cp-gal-title,.cp-cover-card,.cp-slide-title),
:is(.page-block,.bk-col-child).bk-cs-dark :is(.cp-gal-title,.cp-cover-card,.cp-slide-title) { color:var(--dark-text,#f1f5f9); }
/* variant c's own `.cp-slide-meta` (05-galleries.css) reads `var(--muted)`,
   which IS already scheme-scoped, but the base `.cp-slide-meta` rule (also
   05-galleries.css) additionally sets `opacity:.85` for its default
   overlay-on-photo use, and variant c never clears that: it compounds with
   `--muted`'s own alpha, weakening BOTH this fix's mix and, on `accent`,
   the theme's own on-primary-muted value below the small-text 4.5:1 floor.
   `opacity:1` here, not a stronger colour alone, is what actually fixes it. */
:is(.page-block,.bk-col-child).bk-cs-light .cp-slide-meta,
:is(.page-block,.bk-col-child).bk-cs-light-accent .cp-slide-meta { color:color-mix(in srgb, var(--text) 64%, transparent); opacity:1; }
:is(.page-block,.bk-col-child).bk-cs-accent .cp-slide-meta { color:var(--on-primary,#fff); opacity:1; }
/* The light rule above also reaches a dark box nested in a light columns block
   (its ancestor is `.page-block.bk-cs-light`), so a dark child names its own
   ink: the scheme's `--muted` at the base rule's `opacity:.85`, which is what
   variant c already resolves to. */
:is(.page-block,.bk-col-child).bk-cs-dark .cp-slide-meta,
:is(.page-block,.bk-col-child).bk-cs-dark-accent .cp-slide-meta { color:var(--muted); opacity:.85; }

/* ── Gallery CTA ink on a coloured ground (#1511) ────────────────────────────
   `.cp-cover-cta` and the editorial row's `.cp-gal-cta` (08-fullwidth.css)
   paint `color:var(--primary)` directly: the same brand-colour-as-ink bug
   #1316 fixed for `.btn`. Invisible on `accent` (primary on primary), and
   short of the small-text 4.5:1 floor on the dark bands and on light-accent's
   tinted ground (3.9-4.0:1 measured). Variants a/d/f already give their
   overlay-on-photo CTA its own `#fff` at (0,3,0) - `.bk-cs-accent
   .cp-cover-cta` at (0,2,0) can't touch those, which is what keeps them
   untouched here too. */
.bk-cs-accent .cp-cover-cta { color:var(--on-primary,#fff); }
.bk-cs-dark-accent .cp-cover-cta,
.bk-cs-dark .cp-cover-cta { color:var(--dark-text,#f1f5f9); }
.bk-cs-light-accent .cp-cover-cta { color:var(--text); }
/* variant g's card sits on `var(--card)`, not the plain light ground, and
   the primary orange only clears 4.17:1 there (needs 4.5:1) - light-accent
   and the dark/accent bands above already route this CTA off var(--primary)
   entirely, so only plain light needs its own scoped exception. */
:is(.page-block--gallery_cover,.bk-col-child--gallery_cover).bk-variant-g.bk-cs-light .cp-cover-cta { color:var(--text); }

/* ── Statement/stats ink on a coloured ground (#1507) ────────────────────────
   `.bk-statement-label` and `.bk-stat-value` (03-blocks-content.css) both
   paint `color:var(--primary)` directly, with no scheme override at all: the
   same brand-colour-as-ink bug #1316 fixed for `.btn` and #1511 fixed for the
   gallery CTA, just never applied here. #1507's own starter sites surfaced it
   for real: three of the twelve presets (Voltage, Terracotta, Porcelain) pick
   an accent that clears AA against nothing painted with it as text, only as a
   fill. `--primary-ink` is the same readability-adjusted accent nav.css
   already uses for exactly this shape (theme_vars.go's readableAccentInk,
   floored against the page's own light bg), which is also what `light` and
   `light-accent` render these two elements on; the accent/dark bands get the
   same treatment as the heading rules above rather than an ink calculated for
   a different ground entirely. */
.bk-cs-light .bk-statement-label,
.bk-cs-light-accent .bk-statement-label { color:var(--primary-ink,var(--primary)); }
.bk-cs-accent .bk-statement-label { color:var(--on-primary,#fff); }
.bk-cs-dark-accent .bk-statement-label,
.bk-cs-dark .bk-statement-label { color:var(--dark-text,#f1f5f9); }

/* ── Large-text ink for display numerals (#1483 follow-up) ───────────────────
   `.bk-stat-value` and `.bk-services-price` get their own rule rather than
   sharing the `.bk-statement-label` selector list above: both are WCAG large
   text (bold, well past 18.66px), floored at 3:1 via `--primary-ink-lg`
   instead of the label's 4.5:1 `--primary-ink`. On the dark bands this
   matters for real: the brand accent regularly clears 3:1 against a
   near-black ground while missing 4.5:1, and sharing the label's rule would
   silently fall the numeral back to `--dark-text` even then. light/
   light-accent/accent resolve to the same value either token would give
   (light-accent-lg/on-primary need no size distinction, see above), so this
   is a behaviour change only on the two dark bands. */
.bk-cs-light .bk-stat-value, .bk-cs-light .bk-services-price,
.bk-cs-light-accent .bk-stat-value, .bk-cs-light-accent .bk-services-price {
  color:var(--primary-ink-lg,var(--primary));
}
.bk-cs-accent .bk-stat-value, .bk-cs-accent .bk-services-price { color:var(--primary-ink-lg,var(--on-primary,#fff)); }
.bk-cs-dark-accent .bk-stat-value, .bk-cs-dark-accent .bk-services-price,
.bk-cs-dark .bk-stat-value, .bk-cs-dark .bk-services-price {
  color:var(--primary-ink-lg,var(--dark-text,#f1f5f9));
}

/* ── Buttons on a coloured ground (#1316) ──────────────────────────────────
   Every theme's `.btn` is `background:var(--primary); color:var(--on-primary)`
   with no scheme awareness. That is invisible on the `accent` ground (primary
   painted on primary) and low-contrast on the two dark grounds (the theme's
   accent colour was never chosen against a dark background). The fix mirrors
   cta_band's own ground-aware button (03-blocks-content.css:975-1105): invert
   to a fill/ink pair guaranteed to contrast with the ground it sits on, rather
   than trying to guess whether the THEME's own accent happens to work there.
   `.bk-cs-accent .btn` is (0,2,0), which out-ranks every theme's bare `.btn`
   at (0,1,0), so this wins without `!important`. Covers every block that
   renders a `.btn`, not only the hero, because it keys on the scheme wrapper.

   That wrapper only exists on a TOP-LEVEL block: a block nested inside a
   `section` or `columns` renders its bare HTML with no `bk-cs-*` ancestor at
   all (site_bridge_section.go's renderSectionChild, and the "columns" case in
   _blocks.tmpl, both skip it on the published page), so a hero dropped into a
   section had its own colour scheme silently ignored. `.btn.bk-btn-accent`
   etc. are the same declarations, matched a second way: block_render_registry
   .go's renderHero puts the class directly on the anchor from the block's own
   ColorScheme field, so the button no longer depends on an ancestor existing
   at all. Same (0,2,0) specificity as the wrapper form, so neither rule set
   "wins" over the other — whichever is present applies the same fill. */
.bk-cs-accent .btn,
.btn.bk-btn-accent {
  background:var(--on-primary,#fff);
  color:var(--primary);
}
.bk-cs-dark-accent .btn,
.bk-cs-dark .btn,
.btn.bk-btn-dark-accent,
.btn.bk-btn-dark {
  background:var(--dark-text,#f1f5f9);
  color:var(--dark-bg,#0b0b0b);
}

/* ── portfolio_bio variant c's drop cap (#1312) ────────────────────────────
   04-portfolio.css paints the drop cap with `color:var(--primary)`, which is
   also the ground on the `accent` scheme: an accent-coloured "I" on an
   accent-coloured band disappears. Same specificity trick as the ink rules
   above; loads after 04-portfolio.css (06 > 04 in filename order) so it wins
   the tie. */
:is(.page-block--portfolio_bio,.bk-portfolio-bio).bk-cs-accent .bpb-bio p:first-child::first-letter {
  color:var(--on-primary,#fff);
}
:is(.page-block--portfolio_bio,.bk-portfolio-bio).bk-cs-dark-accent .bpb-bio p:first-child::first-letter,
:is(.page-block--portfolio_bio,.bk-portfolio-bio).bk-cs-dark .bpb-bio p:first-child::first-letter {
  color:var(--dark-text,#f1f5f9);
}

/* ── portfolio_bio variant d's card frame (#1453) ──────────────────────────
   04-portfolio.css paints the card with a fixed `--card`/`--border`, both
   tints of the LIGHT palette's `--text` (theme_vars.go): on the accent and
   dark schemes the card comes out dark-on-dark or near-invisible, the same
   class of bug the drop cap above already had. The fix derives the card's
   ground from THIS scheme's own bg/fg via `color-mix`, at the same small
   percentage light-accent already uses to lift its own band off `--bg`
   (line 4 above), so the card reads as "this scheme's surface, one step up"
   in every scheme rather than a foreign colour dropped on top of it.

   Heading and body text need no override here: they already inherit the
   wrapper's own `color` (`.bk-cs-*` above), and the accent/dark :is() rules
   above already repaint headings and body copy for those three schemes, so
   text over the card gets the same treatment as text over the flat band.
   Same (0,3,0) specificity trick as the drop cap: two classes plus the
   variant beats 04-portfolio.css's (0,2,0) card rule with no !important. */
:is(.page-block--portfolio_bio,.bk-col-child--portfolio_bio).bk-variant-d {
  &.bk-cs-light {
    background:color-mix(in srgb,var(--text) 5%,var(--bg));
    border-color:color-mix(in srgb,var(--text) 14%,var(--bg));
  }
  &.bk-cs-light-accent {
    background:color-mix(in srgb,var(--text) 5%,var(--bg-accent,color-mix(in srgb,var(--primary) 10%,var(--bg))));
    border-color:color-mix(in srgb,var(--text) 14%,var(--bg-accent,color-mix(in srgb,var(--primary) 10%,var(--bg))));
  }
  &.bk-cs-accent {
    background:color-mix(in srgb,var(--on-primary,#fff) 12%,var(--primary));
    border-color:color-mix(in srgb,var(--on-primary,#fff) 28%,var(--primary));
  }
  &.bk-cs-dark-accent {
    background:color-mix(in srgb,var(--dark-text,#f1f5f9) 10%,var(--dark-bg,#1e293b));
    border-color:color-mix(in srgb,var(--dark-text,#f1f5f9) 24%,var(--dark-bg,#1e293b));
  }
  &.bk-cs-dark {
    background:color-mix(in srgb,var(--dark-text,#f1f5f9) 10%,var(--dark-bg,#0b0b0b));
    border-color:color-mix(in srgb,var(--dark-text,#f1f5f9) 24%,var(--dark-bg,#0b0b0b));
  }
}

/* ── Variant layouts (the A/B/C style switch) ─────────────────────────────── */
/* Scoped per block type so a/b/c map to real, visible layout differences.
   Variant 'a' is the existing default for every block, so it needs no rules. */

/* hero: a: centered (default) · b: left align · c: minimal */
.page-block--hero {
  &.bk-variant-b {
    & .bk-hero   { text-align:left; }
    & .bk-hero p { margin-inline:0; }
  }
  &.bk-variant-c {
    & .bk-hero    { padding:1rem 0 .5rem; }
    & .bk-hero h2 { font-size:clamp(1.25rem,3vw,1.85rem); }
    & .bk-hero p  { font-size:.95rem; margin-bottom:.75rem; }
  }
}

/* split: a: image left (default) · b: image right */
.page-block--split.bk-variant-b {
  & .bk-split-img { order:2; }
}

/* text: a: single column (default) · b: two columns (magazine flow of one field) */
.page-block--text.bk-variant-b {
  & .bk-text { column-count:2; column-gap:2.5rem; max-width:none; }
  /* Headings span both columns so a leading title sits above the flowing body
    . being stranded in one column: reads as intentional with one field. */
  & .bk-text :is(h1,h2,h3,h4) { column-span:all; margin-top:0; }
}
@media(max-width:640px) { .page-block--text.bk-variant-b .bk-text { column-count:1; } }

/* image: SIZE: a small · b medium (contained, narrower than the page default)
   · c large, framed (contained, near-edge with a slim gutter) · d full-bleed.
   b and c used to differ by width alone (b fell through to .page-block's own
   1360px default tier, c capped at 1500px via the SAME bleed-style negative
   margin d uses): at any viewport under 1500px c's cap never binds, so c
   rendered pixel-identical to d, and b — whose 1360px cap is itself wider
   than most viewports — rendered within a few percent of both (#1501, A3).
   b now gets its own, visibly narrower cap. c's outer width alone stayed
   near-identical to d even after that fix: the distinctness gate screenshots
   each block's OWN box, and at typical viewports c's cap barely binds, so the
   photo fills that box edge-to-edge exactly like d once both are downscaled
   for comparison — the outer whitespace difference lives outside the box
   being measured. c now carries its own visible treatment INSIDE the
   box — a bordered card with a background mat around the photo — so it reads
   as "a framed large photo" against d's true edge-to-edge bleed regardless of
   how close their outer widths land. */
.page-block--image {
  &.bk-variant-a { width:min(560px, 100% - 3rem); }
  &.bk-variant-b { width:min(900px, 100% - 3rem); }
  &.bk-variant-c { width:min(1500px, 100% - 1.5rem); margin-inline:auto; }
  &.bk-variant-d { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
    & .bk-image img { border-radius:0; }
  }
}

/* image c's framed mat. Also reaches a columns child (.bk-col-child--image,
   #1976); the width cap above stays top level, a cell already sets its width. */
:is(.page-block--image,.bk-col-child--image).bk-variant-c {
  & .bk-image { padding:1rem; background:var(--card); border:1px solid var(--border); border-radius:calc(1rem * var(--pf-radius-k,1)); }
  & .bk-image img { border-radius:var(--pf-radius-md); }
}

/* gallery_row: LAYOUT: a 3-col · b 2-col · c 4-col · d masonry · e full-bleed grid */
.bk-gallery-row { gap:.75rem; }
/* Default grid honours the chosen column count (--gr-cols, from the editor's
   Columns control). Variants b/c are fixed 2/4-col presets for the LAYOUT pill. */
.page-block--gallery_row {
  &.bk-variant-a { & .bk-gallery-row { grid-template-columns:repeat(var(--gr-cols,3),1fr); } }
  &.bk-variant-b { & .bk-gallery-row { grid-template-columns:repeat(2,1fr); } }
  &.bk-variant-c { & .bk-gallery-row { grid-template-columns:repeat(4,1fr); } }
  /* masonry: natural aspect ratios, varied tile heights (Pixieset look) */
  &.bk-variant-d {
    & .bk-gallery-row { display:block; column-count:var(--gr-cols,3); column-gap:.75rem; }
    & .bk-gallery-row img { aspect-ratio:auto; height:auto; margin:0 0 .75rem; break-inside:avoid; }
  }
  /* full-bleed grid: edge to edge, 3 columns of big tiles */
  &.bk-variant-e { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
    & .bk-gallery-row { grid-template-columns:repeat(3,1fr); gap:.5rem; }
    & .bk-gallery-row img { border-radius:0; }
  }
}
@media(max-width:900px) { .page-block--gallery_row.bk-variant-d .bk-gallery-row { column-count:2; } }
@media(max-width:640px) {
  .page-block--gallery_row .bk-gallery-row { grid-template-columns:repeat(2,1fr); }
  .page-block--gallery_row.bk-variant-d .bk-gallery-row { column-count:2; }
}

/* full_bleed: a: centered overlay (default) · b: side text panel
   · c: bottom gradient caption · d: framed matte · e: solid boxed panel */
.page-block--full_bleed {
  &.bk-variant-b {
    & .bk-full-bleed-overlay { opacity:.3; }
    & .bk-full-bleed-content  {
      background:var(--pf-black-45); border-radius:var(--pf-radius-md); padding:1.75rem 2rem;
      max-width:min(480px,62%); backdrop-filter:blur(2px);
    }
  }
  /* c: copy sinks to the bottom over a soft gradient scrim */
  &.bk-variant-c {
    & .bk-full-bleed         { align-items:flex-end; }
    & .bk-full-bleed-overlay { opacity:1 !important;
      background:linear-gradient(to top, var(--pf-black-78), var(--pf-black-15) 55%, var(--pf-black-00)) !important; }
    & .bk-full-bleed-content { max-width:900px; padding-bottom:clamp(2rem,5vw,3.5rem); }
  }
  /* d: thin inset frame floats a matte border inside the image */
  &.bk-variant-d {
    & .bk-full-bleed::after  { content:""; position:absolute;
      inset:clamp(.9rem,3vw,2.25rem); border:1px solid var(--pf-white-65); z-index:1; pointer-events:none; }
    & .bk-full-bleed-content { z-index:2; }
  }
  /* e: copy in a solid light card that lifts off the photo */
  &.bk-variant-e {
    & .bk-full-bleed-overlay { opacity:.28; }
    /* Opaque: --card is a 4% tint over nothing, which left the copy on the
       photo itself (#1460). */
    & .bk-full-bleed-content { background:color-mix(in srgb,var(--text) 4%,var(--bg)); color:var(--text);
      border-radius:calc(.9rem * var(--pf-radius-k,1)); padding:clamp(1.75rem,3vw,2.5rem); max-width:min(460px,64%);
      box-shadow:0 24px 60px -22px var(--pf-black-60); }
    & .bk-full-bleed-heading,
    & .bk-full-bleed-subtext { color:var(--text); }
  }
}

/* split (Info Block): a: image left (default) · b: image right · c: stacked centered */
.page-block--split.bk-variant-c {
  & .bk-split        { grid-template-columns:1fr; max-width:760px; margin-inline:auto; text-align:center; }
  & .bk-split-img    { aspect-ratio:16/9; }
  & .bk-split-body   { align-items:center; }
}

/* faq accordion (variant e) */
.bk-faq-accordion         { display:flex; flex-direction:column; gap:.4rem; }
.bk-faq-item              { border:1px solid var(--border); border-radius:var(--pf-radius-md); overflow:hidden; }
.bk-faq-summary           { padding:1rem 1.25rem; cursor:pointer; font-weight:600;
                             list-style:none; display:flex; justify-content:space-between;
                             align-items:center; gap:1rem; user-select:none;
  &::-webkit-details-marker { display:none; }
  &::after    { content:'+'; font-size:1.1rem; font-weight:300; line-height:1;
                color:var(--primary); flex-shrink:0; }
}
.bk-faq-item[open] > .bk-faq-summary::after { content:'−'; }
.bk-faq-item[open] > .bk-faq-summary { border-bottom:1px solid var(--border); }
.bk-faq-body              { padding:.75rem 1.25rem 1rem; color:var(--muted); line-height:1.6; }

/* faq: a: bordered list (default) · b: two columns · c: question-left / answer-right */
.page-block--faq {
  &.bk-variant-b {
    & .bk-faq { display:grid; grid-template-columns:1fr 1fr; gap:1.5rem 2.5rem; }
    & .bk-faq > div { border:none; padding:0; }
  }
  &.bk-variant-c {
    & .bk-faq > div { display:grid; grid-template-columns:1fr 2fr; gap:1.5rem; border:none; padding:0 0 1.25rem; border-bottom:1px solid var(--border); border-radius:0; }
  }
}
@media(max-width:640px){
  .page-block--faq.bk-variant-b .bk-faq, .page-block--faq.bk-variant-c .bk-faq > div { grid-template-columns:1fr; }
}

/* quote: a: centered plain (default) · b: centered boxed · c: text-left + author panel */
/* Also reaches a columns child through its .bk-col-child--quote box (#1976). */
:is(.page-block--quote,.bk-col-child--quote) {
  &.bk-variant-a { & .bk-quote { border-left:none; background:none; text-align:center; max-width:680px; margin-inline:auto; padding:1.5rem 1rem; } }
  &.bk-variant-b { & .bk-quote { border-left:none; text-align:center; padding:2.5rem 2rem; border-radius:var(--pf-radius-md); border:1px solid var(--border); background:var(--card); box-shadow:0 1px 3px var(--pf-black-06), 0 8px 24px var(--pf-black-05); } }
  &.bk-variant-c {
    & .bk-quote { border-left:none; background:none; display:grid; grid-template-columns:3fr 1fr; gap:2rem; align-items:center; padding:1.5rem 0; }
    & .bk-quote footer { margin-top:0; text-align:right; }
  }
}
@media (max-width:640px){ :is(.page-block--quote,.bk-col-child--quote).bk-variant-c .bk-quote { grid-template-columns:1fr; gap:1rem; } :is(.page-block--quote,.bk-col-child--quote).bk-variant-c .bk-quote footer { text-align:left; } }
:is(.page-block--quote,.bk-col-child--quote):is(.bk-variant-a,.bk-variant-b) .bk-quote p { font-size:1.35rem; }

/* The collage c caption card is paper (03-blocks-content.css), the same light
   stock on every scheme, so its ink is the page's own --text there too. The
   band's ink (white on accent and dark) and --muted (4.33:1 on the 6% paper)
   both failed on it (#1460). (0,3,0) beats the scheme :is() rules. */
.bk-collage--c .bk-collage-text :is(.bk-collage-heading,.bk-collage-body) { color:var(--text); }
/* ── Link columns (CTA columns) ───────────────────────────────────────────── */
.bk-link-columns        { display:grid; grid-template-columns:repeat(3,1fr); gap:2.5rem; text-align:center; padding:2rem 0; }
.bk-lc-col              { text-decoration:none; color:inherit; display:block; }
.bk-lc-heading          { font-family:var(--font-display); font-size:clamp(1.5rem,3vw,2.25rem); font-weight:600; margin-bottom:.85rem; line-height:1.1; }
.bk-lc-sub              { font-size:.78rem; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
.bk-lc-col:hover .bk-lc-sub { color:var(--primary); }
.page-block--link_columns {
  &.bk-variant-b { & .bk-link-columns { grid-template-columns:repeat(2,1fr); } }
  &.bk-variant-c { & .bk-link-columns { text-align:left; } }
}
@media(max-width:640px) { .bk-link-columns { grid-template-columns:1fr; gap:2.5rem; } }

/* ── Banner (image banner / slider: one block, many layouts) ─────────────── */
.page-block--banner     { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }
.bk-banner              { position:relative; overflow:hidden; }
.bk-banner-track        { display:flex; transition:transform .5s ease; }
/* Height is a share of the viewport (--bk-banner-h) less a fixed offset
   (--bk-banner-sub), so a template sets the two numbers and the canvas clamp
   in _canvas.tmpl applies the same share to the previewed viewport. A template
   that set min-height in vh itself would be overridden by that clamp and the
   canvas would disagree with the site (#1501 A4). */
.bk-banner-slide        { position:relative; flex:0 0 100%; min-height:calc(var(--bk-banner-h,.72) * 100vh - var(--bk-banner-sub,0px)); background:var(--text) center/cover no-repeat;
                          display:flex; align-items:flex-end; }
.bk-banner--empty       { min-height:48vh; background:var(--card); display:flex; align-items:center; justify-content:center; border:2px dashed var(--border); }
.bk-banner-overlay      { position:absolute; inset:0; background:linear-gradient(to top, var(--pf-black-45), var(--pf-black-05)); }
.bk-banner-content      { position:relative; z-index:1; padding:clamp(1.5rem,4vw,4rem); color:var(--pf-white); max-width:760px; }
.bk-banner-eyebrow      { font-size:.78rem; letter-spacing:.2em; text-transform:uppercase; opacity:.85; margin-bottom:.75rem; }
.bk-banner-title        { font-family:var(--font-display); font-size:clamp(2rem,5vw,4.5rem); font-weight:600; line-height:1.03; margin:0 0 1.25rem; }
.bk-banner-cta          { display:inline-flex; align-items:center; min-height:44px; color:var(--pf-white); text-decoration:none; font-size:.8rem; letter-spacing:.12em;
                          text-transform:uppercase; border-bottom:1px solid var(--pf-white-70); padding-bottom:2px; }
.bk-banner-nav          { display:none; position:absolute; bottom:1.75rem; z-index:2; width:44px; height:44px; border-radius:50%;
                          border:1px solid var(--pf-white-50); background:var(--pf-black-20); color:var(--pf-white); cursor:pointer; font-size:1rem; }
.bk-banner-prev         { right:5rem; } .bk-banner-next { right:1.5rem; }
.bk-banner-count        { display:none; position:absolute; bottom:2.4rem; left:clamp(1.5rem,4vw,4rem); z-index:2; color:var(--pf-white);
                          font-size:.8rem; letter-spacing:.1em; }
/* non-slider variants show only the first slide */
.bk-banner-slide:not(:first-child) { display:none; }
.page-block--banner {
  /* variant b: centered */
  &.bk-variant-b {
    & .bk-banner-slide   { align-items:center; justify-content:center; }
    & .bk-banner-content { text-align:center; margin-inline:auto; }
  }
  /* variant c: text panel on the right */
  &.bk-variant-c {
    & .bk-banner-slide   { align-items:stretch; }
    & .bk-banner-overlay { display:none; }
    & .bk-banner-content { background:var(--bg); color:var(--text); margin-left:auto; max-width:38%;
                          display:flex; flex-direction:column; justify-content:center; text-align:center; }
    & .bk-banner-cta     { color:var(--text); border-bottom-color:var(--text); }
  }
  /* variant d: centered title only, played as an oversized poster statement.
     Used to just be b with the eyebrow/CTA hidden: on a real slide those two
     lines are a sliver of a 72vh-tall image, so removing them barely moved a
     pixel and d measured near-identical to b (#1501, A3). A flat, darker
     scrim (replacing b's top-to-bottom gradient) plus a much bigger,
     letter-spaced title makes the two variants read as different intents —
     "a photo with a caption" vs. "a headline over a photo" — not just one
     with two lines removed. */
  &.bk-variant-d {
    & .bk-banner-slide   { align-items:center; justify-content:center; }
    & .bk-banner-overlay { background:var(--pf-black-40); }
    & .bk-banner-content { text-align:center; max-width:min(90%,960px); }
    & .bk-banner-title    { font-size:clamp(2.75rem,7vw,6rem); letter-spacing:.03em; text-transform:uppercase; }
    & .bk-banner-eyebrow,
    & .bk-banner-cta     { display:none; }
  }
  /* variant e: centered title + CTA */
  &.bk-variant-e {
    & .bk-banner-slide   { align-items:center; justify-content:center; }
    & .bk-banner-content { text-align:center; }
    & .bk-banner-eyebrow { display:none; }
  }
  /* variant f: slider: show all slides in the track + nav/count */
  &.bk-variant-f {
    & .bk-banner-slide   { display:flex; }
    & .bk-banner-nav,
    & .bk-banner-count   { display:flex; align-items:center; justify-content:center; }
  }
}
@media(max-width:640px) { .page-block--banner.bk-variant-c .bk-banner-content { max-width:65%; } }

/* Sliders (variants f and g) draw each photo as an <img> so the first can load
   eagerly at high priority and every one carries alt text (#2228). The photo
   fills the slide; the scrim and the text sit above it in DOM order. */
.bk-banner-img          { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.bk-banner--f:focus-visible,
.bk-banner--g:focus-visible { outline:2px solid var(--primary); outline-offset:-4px; }
.bk-banner-live         { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }

/* ── Variant g: full-screen slider (#2228) ────────────────────────────────
   Slides are stacked in one grid cell and cross-fade (fade and zoom), or sit
   side by side on the translating track (slide). The first slide carries
   .is-active in the markup, so with no script the block shows slide one and
   hides nothing else that matters. Everything that moves is switched off under
   prefers-reduced-motion at the end of this block. */
.bk-banner--g           { touch-action:pan-y; }
.bk-banner.bk-banner--g .bk-banner-slide { display:flex; align-items:flex-end; overflow:hidden; }
.bk-banner.bk-banner--g.bk-banner--h-full .bk-banner-slide { --bk-banner-h:1; --bk-banner-sub:var(--pf-header-h,72px); }
.bk-banner.bk-banner--g.bk-banner--h-80 .bk-banner-slide   { --bk-banner-h:.8; --bk-banner-sub:0px; }
.bk-banner--g:not(.bk-banner--tr-slide) .bk-banner-track { display:grid; transition:none; }
.bk-banner--g:not(.bk-banner--tr-slide) .bk-banner-slide { grid-area:1 / 1; opacity:0; visibility:hidden;
                          transition:opacity .9s ease, visibility 0s linear .9s; }
.bk-banner--g:not(.bk-banner--tr-slide) .bk-banner-slide.is-active { opacity:1; visibility:visible; z-index:1; transition-delay:0s; }
.bk-banner--tr-zoom .bk-banner-img { transform:scale(1.1); transition:transform 7s ease-out; }
.bk-banner--tr-zoom .is-active .bk-banner-img { transform:scale(1); }
.bk-banner--g .bk-banner-overlay { background:linear-gradient(to top,
                          color-mix(in srgb, var(--pf-black) calc(var(--bk-ov,.5) * 100%), transparent),
                          color-mix(in srgb, var(--pf-black) calc(var(--bk-ov,.5) * 25%), transparent)); }
.bk-banner--g .bk-banner-content { max-width:min(100%,820px); }
.bk-banner--g .bk-banner-eyebrow { font-size:.8rem; opacity:1; }
.bk-banner--g .bk-banner-title   { font-size:clamp(2.25rem,6vw,5.5rem); line-height:1.02; }
.bk-banner--g .bk-banner-cta     { padding:.6rem 1.6rem; border:1px solid var(--pf-white); border-radius:999px; }
.bk-banner--g .bk-banner-cta:hover { background:var(--pf-white-85); color:var(--text); }
@keyframes bk-bn-rise { from { opacity:0; transform:translateY(18px); } to { opacity:1; transform:none; } }
.bk-banner--g .is-active .bk-banner-eyebrow { animation:bk-bn-rise .8s .25s ease-out both; }
.bk-banner--g .is-active .bk-banner-title   { animation:bk-bn-rise .8s .45s ease-out both; }
.bk-banner--g .is-active .bk-banner-cta     { animation:bk-bn-rise .8s .65s ease-out both; }
/* The control panel, in the brand colour: previous, index, progress track with
   a thumb that moves one step per slide, total, next. The thumb is driven by
   --bk-i and --bk-n, which the script and the markup set. */
.bk-banner--g .bk-banner-panel   { position:absolute; right:clamp(1rem,4vw,4rem); bottom:clamp(1rem,3vw,2.5rem); z-index:2;
                          display:flex; align-items:center; gap:.9rem; padding:.55rem .75rem; border-radius:999px;
                          background:var(--primary); color:var(--on-primary,var(--pf-white)); }
.bk-banner--g .bk-banner-nav     { display:inline-flex; align-items:center; justify-content:center; position:static; width:44px; height:44px;
                          border:1px solid currentColor; background:transparent; color:inherit; }
.bk-banner--g .bk-banner-idx,
.bk-banner--g .bk-banner-total   { font-size:.9rem; letter-spacing:.08em; font-variant-numeric:tabular-nums; }
.bk-banner--g .bk-banner-progress { position:relative; flex:0 0 clamp(60px,10vw,140px); height:2px; background:color-mix(in srgb, currentColor 30%, transparent); }
.bk-banner--g .bk-banner-thumb   { position:absolute; left:0; top:-1px; height:4px; border-radius:2px; background:currentColor;
                          width:calc(100% / var(--bk-n,1)); transform:translateX(calc(var(--bk-i,0) * 100%)); transition:transform .6s ease; }
@media(max-width:1100px) { .bk-banner--g .bk-banner-content { padding-bottom:6.5rem; } }
@media(max-width:640px) {
  .bk-banner--g .bk-banner-title { font-size:clamp(1.9rem,9vw,2.6rem); }
  .bk-banner--g .bk-banner-panel { left:1rem; right:1rem; bottom:1rem; justify-content:space-between; }
  .bk-banner--g .bk-banner-progress { flex:1 1 auto; }
  /* The fixed bottom tab bar (60px) covers the foot of a full-height slide, and
     the controls sit there. Stop the slide above it. */
  body:has(.snav-bottom) .bk-banner.bk-banner--g.bk-banner--h-full .bk-banner-slide { --bk-banner-sub:calc(var(--pf-header-h,72px) + 60px + env(safe-area-inset-bottom)); }
}
@media(prefers-reduced-motion:reduce) {
  .bk-banner--g .bk-banner-slide,
  .bk-banner--g .bk-banner-img,
  .bk-banner--g .bk-banner-thumb { transition:none; }
  /* .is-active matches the specificity of the rules that start the rise, or the
     animation (and its hidden delay phase) would still run. */
  .bk-banner--g .is-active .bk-banner-eyebrow,
  .bk-banner--g .is-active .bk-banner-title,
  .bk-banner--g .is-active .bk-banner-cta   { animation:none; }
  .bk-banner--tr-zoom .bk-banner-img { transform:none; }
}

/* ── Full-width & full-screen variants ────────────────────────────────────── */
/* Every wrapper below already bleeds to `--pf-vw` for ANY scheme (it's a
   LAYOUT choice, not a colour one), and each puts its own content padding on
   an INNER element (`.bk-hero`, `.bk-split`, `.bk-statement`, …), never on
   the wrapper itself. 02-layout.css's coloured-band rule also lands
   `padding-inline` on `.page-block` for a non-light scheme, sized to the
   block's own tier: harmless in most cases, but here the wrapper would gain
   a SECOND, larger gutter on top of the inner element's own, doubling the
   inset on a variant + non-light-scheme combination. Zero it back off on
   these wrappers specifically: the inner element's own padding already does
   the job the band rule exists for. Same (0,2,0) specificity as that rule,
   and this file loads after 02-layout.css, so it wins the tie regardless of
   scheme. */
.page-block--hero.bk-variant-d, .page-block--hero.bk-variant-e,
.page-block--split.bk-variant-d,
.page-block--text.bk-variant-c,
.page-block--quote.bk-variant-d, .page-block--quote.bk-variant-e,
.page-block--statement.bk-variant-b,
.page-block--callout.bk-variant-b,
.page-block--faq.bk-variant-d,
.page-block--services.bk-variant-b,
.page-block--process.bk-variant-b,
.page-block--video.bk-variant-b,
.page-block--gallery_list.bk-variant-b,
.page-block--link_columns.bk-variant-d,
.page-block--section.bk-variant-b {
  padding-inline:0;
}

/* hero: d full width · e full screen */
.page-block--hero.bk-variant-d,
.page-block--hero.bk-variant-e { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }
.page-block--hero.bk-variant-d .bk-hero,
.page-block--hero.bk-variant-e .bk-hero { padding-inline:clamp(1.5rem,6vw,6rem); }
.page-block--hero.bk-variant-e .bk-hero { min-height:100svh; display:flex; flex-direction:column; align-items:center; justify-content:center; padding-block:4rem; }

/* split: d full width */
.page-block--split.bk-variant-d { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .bk-split { max-width:1500px; margin-inline:auto; padding-inline:clamp(1.5rem,4vw,4rem); }
  & .bk-split-img { border-radius:0; }
}

/* text: c full width (background band; content stays readable width) */
.page-block--text.bk-variant-c { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .bk-text { max-width:80ch; margin-inline:auto; padding:2rem clamp(1.5rem,6vw,6rem); }
}

/* quote: d: full width · e: full screen */
.page-block--quote.bk-variant-d,
.page-block--quote.bk-variant-e { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }
.page-block--quote.bk-variant-d { & .bk-quote { border-left:none; text-align:center; padding:3rem clamp(2rem,8vw,8rem); background:none; max-width:none; }
  & .bk-quote p { font-size:clamp(1.25rem,3vw,2rem); }
}
.page-block--quote.bk-variant-e { & .bk-quote { border-left:none; text-align:center; padding:3rem clamp(2rem,8vw,8rem); background:none; max-width:none; min-height:100svh; display:flex; flex-direction:column; align-items:center; justify-content:center; }
  & .bk-quote p { font-size:clamp(1.5rem,4vw,3.5rem); line-height:1.2; }
  & .bk-quote footer { font-size:1.1rem; margin-top:2rem; }
}

/* statement: b: full width · c: boxed · d: left accent · e: oversized · f: serif */
.page-block--statement.bk-variant-b { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .bk-statement { max-width:none; padding:2rem clamp(2rem,8vw,8rem) 1rem; }
}
/* c-f also reach a columns child through its .bk-col-child--statement box
   (#1976); b bleeds to the viewport, which a cell cannot. */
:is(.page-block--statement,.bk-col-child--statement) {
  /* c: framed card */
  &.bk-variant-c { & .bk-statement { max-width:760px; border:1px solid var(--border);
    border-radius:calc(1rem * var(--pf-radius-k,1)); background:var(--card); padding:clamp(1.75rem,4vw,2.75rem); } }
  /* d: vertical accent rule */
  &.bk-variant-d { & .bk-statement { border-left:4px solid var(--primary); padding-left:clamp(1.25rem,3vw,1.75rem); } }
  /* e: oversized display heading */
  &.bk-variant-e { & .bk-statement-heading { font-size:clamp(2.5rem,7vw,5rem); line-height:1; letter-spacing:-.03em; } }
  /* f: editorial serif */
  &.bk-variant-f { & .bk-statement-heading { font-family:var(--font-display,Georgia,"Times New Roman",serif);
    font-weight:500; font-style:italic; letter-spacing:normal; } }
}

/* callout: b: full width · c: elevated card · d: primary accent · e: minimal */
.page-block--callout.bk-variant-b { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .bk-callout { border:none; border-radius:0; padding:1.5rem clamp(1.5rem,6vw,6rem); }
}
/* c-e also reach a columns child through its .bk-col-child--callout box (#1976). */
:is(.page-block--callout,.bk-col-child--callout) {
  /* c: borderless, elevated */
  &.bk-variant-c { & .bk-callout { border:none; border-radius:calc(1rem * var(--pf-radius-k,1)); padding:1.75rem 2rem;
    box-shadow:0 1px 2px var(--pf-black-05), 0 18px 44px -22px var(--pf-black-30); } }
  /* d: tinted with a primary edge */
  &.bk-variant-d { & .bk-callout { border:none; border-left:4px solid var(--primary);
    border-radius:0 calc(.6rem * var(--pf-radius-k,1)) calc(.6rem * var(--pf-radius-k,1)) 0; background:color-mix(in srgb, var(--primary) 8%, var(--card)); }
    & .bk-callout h3 { color:var(--primary-ink); }
  }
  /* e: stripped back, no frame */
  &.bk-variant-e { & .bk-callout { border:none; background:none; padding:.5rem 0; } }
}

/* faq: d: full width */
.page-block--faq.bk-variant-d { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .bk-faq { padding-inline:clamp(1.5rem,6vw,6rem); max-width:none; }
}

/* services: b: full width · c: bordered · d: two column · e: price rows */
.page-block--services.bk-variant-b { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .bk-services { padding-inline:clamp(1.5rem,6vw,6rem); }
}
/* c-e also reach a columns child through its .bk-col-child--services box
   (#1976); b bleeds to the viewport, which a cell cannot. */
:is(.page-block--services,.bk-col-child--services) {
  /* c: outlined cards. tinted fills */
  &.bk-variant-c { & .bk-services-card { background:transparent !important; border:1px solid var(--border); } }
  /* d: locked to two columns */
  &.bk-variant-d { & .bk-services { grid-template-columns:repeat(2,1fr); } }
}
@media(max-width:640px){ :is(.page-block--services,.bk-col-child--services).bk-variant-d .bk-services { grid-template-columns:1fr; } }
/* e: priced list rows, name left / price right */
:is(.page-block--services,.bk-col-child--services).bk-variant-e {
  & .bk-services { display:flex; flex-direction:column; gap:0; margin-bottom:2.5rem; }
  & .bk-services-card { background:transparent !important; border:none;
    border-bottom:1px solid var(--border); border-radius:0; padding:1.4rem .25rem;
    display:grid; grid-template-columns:1fr auto; grid-template-areas:"name price" "desc price";
    column-gap:1.5rem; align-items:center; }
  & .bk-services-name  { grid-area:name; align-self:end; }
  & .bk-services-price { grid-area:price; margin:0; font-size:1.4rem; text-align:right; }
  & .bk-services-desc  { grid-area:desc; align-self:start; }
}

/* process: b: full width (steps become horizontal columns) · c: timeline
   · d: cards · e: outline numbers */
.page-block--process.bk-variant-b { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .bk-process { padding-inline:clamp(1.5rem,6vw,6rem); }
  & .bk-process-steps { grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); }
}
/* c-e also reach a columns child through its .bk-col-child--process box (#1976). */
:is(.page-block--process,.bk-col-child--process) {
  /* c: connected vertical timeline */
  &.bk-variant-c {
    & .bk-process-steps { gap:0; }
    & .bk-process-step  { position:relative; padding-bottom:2rem; }
    & .bk-process-step::before { content:""; position:absolute; left:1.25rem;
      top:2.75rem; bottom:0; width:2px; background:var(--border); }
    & .bk-process-step:last-child { padding-bottom:0; }
    & .bk-process-step:last-child::before { display:none; }
    & .bk-process-num   { position:relative; z-index:1; }
  }
  /* d: each step a standalone card */
  &.bk-variant-d {
    & .bk-process-steps { grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:1.25rem; }
    & .bk-process-step  { display:block; border:1px solid var(--border);
      border-radius:calc(.9rem * var(--pf-radius-k,1)); background:var(--card); padding:1.75rem 1.5rem; }
    & .bk-process-num   { margin-bottom:1rem; }
  }
  /* e: ghost outline number badges */
  &.bk-variant-e { & .bk-process-num   { background:transparent; color:var(--primary-ink);
    border:2px solid var(--primary-ink); } }
}

/* video: b: full width (cinematic) · c: framed matte · d: bordered
   The width bleed stays keyed to the WRAPPER (it only means anything for a
   top-level block: a nested video can't break out of its column). Every
   other rule keys on `.bk-video--<v>`, the class the renderer now puts on
   the figure itself (block_render_registry.go), so the matte/border looks
   apply the same way whether the video sits at the top level or nested in
   columns/section, which never gave it a `.page-block` wrapper at all. */
.page-block--video.bk-variant-b { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }
.bk-video--b .bk-video-ratio { border-radius:0; }
/* c: the player sits in a lifted matte card */
.bk-video--c { padding:clamp(.5rem,1.5vw,.85rem); border:1px solid var(--border);
  border-radius:calc(.85rem * var(--pf-radius-k,1)); background:var(--card); box-shadow:0 26px 55px -28px var(--pf-black-50); }
/* d: crisp bordered rectangle */
.bk-video--d .bk-video-ratio { border-radius:0; border:1px solid var(--border); }

/* gallery_list: b: full width */
/* b: Full-bleed card grid */
.page-block--gallery_list.bk-variant-b { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .cp-gal-grid { padding-inline:clamp(1rem,4vw,3rem); }
}

/* Shared bits used across the new layouts */
.cp-gal-ph { width:100%; height:100%; display:flex; align-items:center; justify-content:center; color:var(--muted); font-size:.9rem; }
.cp-gal-covertitle { position:absolute; inset:0; display:none; align-items:flex-end; padding:1.1rem 1.25rem;
                     background:linear-gradient(to top, var(--pf-black-60), var(--pf-black-00) 62%); z-index:1;
  & span { color:var(--pf-white); font-weight:600; font-size:1.15rem; letter-spacing:.01em; text-shadow:0 1px 12px var(--pf-black-40); }
}

/* d: Cover masonry: varied-height covers in flowing columns */
.page-block--gallery_list.bk-variant-d {
  & .cp-gal-grid { display:block; column-count:3; column-gap:1.25rem; }
  & .cp-gal-card { break-inside:avoid; margin:0 0 1.25rem; display:block; }
  & .cp-gal-cover { aspect-ratio:auto; }
  & .cp-gal-cover img { height:auto; }
}
@media(max-width:900px){ .page-block--gallery_list.bk-variant-d .cp-gal-grid { column-count:2; } }
@media(max-width:560px){ .page-block--gallery_list.bk-variant-d .cp-gal-grid { column-count:1; } }

/* g: Overlay title: big covers, title on the image, no card chrome */
.page-block--gallery_list.bk-variant-g {
  & .cp-gal-grid { grid-template-columns:repeat(auto-fill,minmax(340px,1fr)); gap:1rem; }
  & .cp-gal-card { border:0; border-radius:var(--pf-radius-md); background:none; }
  & .cp-gal-cover { aspect-ratio:3/4; border-radius:var(--pf-radius-md); }
  & .cp-gal-covertitle { display:flex; }
  & .cp-gal-body { display:none; }
}

/* c: Editorial rows: large alternating cover + info */
.cp-gal-editorial { display:flex; flex-direction:column; gap:clamp(1.5rem,4vw,3.5rem); margin-top:1rem; }
.cp-gal-erow { display:grid; grid-template-columns:1.4fr 1fr; gap:clamp(1.25rem,3vw,3rem); align-items:center; text-decoration:none; color:inherit;
  &:nth-child(even) { grid-template-columns:1fr 1.4fr; }
  &:nth-child(even) .cp-gal-ecover { order:2; }
}
.cp-gal-ecover { position:relative; aspect-ratio:4/3; overflow:hidden; border-radius:var(--pf-radius-md); background:var(--border);
  & img { width:100%; height:100%; object-fit:cover; display:block; transition:transform .5s ease; }
}
.cp-gal-erow:hover .cp-gal-ecover img { transform:scale(1.04); }
.cp-gal-etitle { font-size:clamp(1.5rem,3vw,2.4rem); font-weight:700; line-height:1.1; margin-bottom:.5rem; }
.cp-gal-einfo {
  & .cp-gal-meta { font-size:.95rem; }
  & .cp-gal-cta { display:inline-block; margin-top:1rem; color:var(--primary); font-weight:600; }
}
@media(max-width:720px){ .cp-gal-erow, .cp-gal-erow:nth-child(even) { grid-template-columns:1fr; } .cp-gal-erow:nth-child(even) .cp-gal-ecover { order:0; } }
/* Same brand-colour-as-ink bug as .cp-cover-cta (#1511): `.cp-gal-einfo
   .cp-gal-cta` above is already (0,2,0), so the scheme rule needs its own
   ancestor named to out-specify it rather than just naming the scheme (06's
   .cp-cover-cta trick loses this one on file order: 06 loads before 08). Not
   `.cp-gal-cta` alone, or this would also repaint `.cp-gal-heroinfo`'s CTA,
   whose unconditional white is correct there regardless of scheme (it sits on
   a photo, not the scheme's own ground). */
.bk-cs-accent .cp-gal-einfo .cp-gal-cta { color:var(--on-primary,#fff); }
.bk-cs-dark-accent .cp-gal-einfo .cp-gal-cta,
.bk-cs-dark .cp-gal-einfo .cp-gal-cta { color:var(--dark-text,#f1f5f9); }
.bk-cs-light-accent .cp-gal-einfo .cp-gal-cta { color:var(--text); }

/* e: Photo mosaic: each gallery shows a tiled strip of its OWN photos */
.cp-gal-mosaicwrap { display:flex; flex-direction:column; gap:2rem; margin-top:1rem; }
.cp-gal-mosaic { display:block; text-decoration:none; color:inherit; }
.cp-gal-tiles { display:grid; grid-template-columns:repeat(4,1fr); grid-auto-rows:1fr; gap:.4rem; border-radius:var(--pf-radius-md); overflow:hidden;
  & img { width:100%; height:100%; object-fit:cover; display:block; aspect-ratio:1; transition:transform .5s ease; }
  /* .cp-gal-tile-crop wraps a photo carrying a hand-set crop (framePhoto in
     render.go): the mosaic's <img> is normally the grid item itself, but a
     crop needs a position:relative;overflow:hidden ancestor to absolutely
     place the scaled <img> inside (internal/imagepipeline/focal), so a
     cropped tile gets this wrapper as the grid item instead, sized
     identically to a plain tile so the grid geometry doesn't shift. */
  & > .cp-gal-tile-crop { width:100%; height:100%; aspect-ratio:1; overflow:hidden; position:relative; display:block; }
  /* first tile spans a 2×2 hero block for a magazine feel */
  & > img:first-child, & > .cp-gal-tile-crop:first-child { grid-column:span 2; grid-row:span 2; aspect-ratio:auto; }
}
.cp-gal-mosaic:hover .cp-gal-tiles img { transform:scale(1.03); }
.cp-gal-mcap { display:flex; align-items:baseline; gap:.75rem; margin-top:.7rem;
  & .cp-gal-title { font-size:1.2rem; font-weight:600; }
  & .cp-gal-meta { margin:0; }
}
/* a single gallery fills the space with a taller, denser mosaic */
.page-block--gallery_list.bk-variant-e .cp-gal-mosaicwrap:has(.cp-gal-mosaic:only-child) .cp-gal-tiles { grid-template-columns:repeat(4,1fr); }
@media(max-width:640px){ .cp-gal-tiles { grid-template-columns:repeat(2,1fr); } .cp-gal-tiles > img:first-child, .cp-gal-tiles > .cp-gal-tile-crop:first-child { grid-column:span 2; grid-row:span 1; } }

/* f: Hero + grid: first gallery large, the rest as cards below */
.cp-gal-featurehero { position:relative; display:block; aspect-ratio:16/7; border-radius:calc(.7rem * var(--pf-radius-k,1)); overflow:hidden; background:var(--border); margin-bottom:1.25rem; text-decoration:none;
  & img { width:100%; height:100%; object-fit:cover; display:block; transition:transform .6s ease; }
  &:hover img { transform:scale(1.03); }
}
/* .cp-gal-cover carries its OWN aspect-ratio (4/3, for the regular grid
   cards it's shared with), which here fought the hero's 16/7: the child's
   box came out taller than the parent's, so .cp-gal-heroinfo (inset:0 on
   that child) rendered its title/meta/cta near the BOTTOM of a 924px box
   inside a 539px visible one, past the hero's own overflow:hidden clip.
   The overlay text was never painted there at all, so a reader saw a plain
   photo, and the pixel the contrast gate sampled was whatever the grid
   below (painting on top, in normal flow) happened to show through.
   Filling the hero exactly, instead of by a conflicting ratio, is the fix
   for both. */
.cp-gal-featurehero .cp-gal-cover { position:absolute; inset:0; aspect-ratio:auto; }
.cp-gal-heroinfo { position:absolute; inset:0; display:flex; flex-direction:column; justify-content:flex-end; padding:clamp(1.25rem,3vw,2.5rem);
                   background:linear-gradient(to top, var(--pf-black-66), var(--pf-black-00) 60%); color:var(--pf-white); z-index:1;
  /* `.cp-gal-title`'s own base rule sets an explicit `color:var(--text)`
     (05-galleries.css), and an explicit colour always wins over one this
     block would otherwise inherit from the gradient wrapper above, on every
     scheme, not only the broken ones — so this needs its own explicit `#fff`
     too, matching the meta/cta rules right below it, rather than relying on
     inheritance like the rest of this block can (#1511). */
  & .cp-gal-title { font-size:clamp(1.6rem,3.5vw,2.6rem); font-weight:700; text-shadow:0 2px 16px var(--pf-black-40); color:var(--pf-white); }
  & .cp-gal-meta { color:var(--pf-white-85); }
  & .cp-gal-cta { color:var(--pf-white); font-weight:600; margin-top:.5rem; }
}
@media(max-width:640px){ .cp-gal-featurehero { aspect-ratio:4/3; } }

/* contact_form: companion content + split layout */
/* The 4/3 plate is sized for `.bk-cf-split`, where it shares the row with
   the form and lands near 450px tall. Stacked above the form it gets the
   whole content column instead: at 1354px that is 1015px of photograph, three
   quarters of a laptop viewport, before the page's one short form. The cap
   holds it to a band and `object-fit:cover` does the cropping. */
.bk-cf-img        { width:100%; aspect-ratio:4/3; max-height:clamp(12rem,32vh,26rem); object-fit:cover; border-radius:calc(.5rem * var(--pf-radius-k,1)); margin-bottom:1.25rem; display:block; }
.bk-cf-eyebrow    { font-size:.75rem; font-weight:700; letter-spacing:.12em;
                    text-transform:uppercase; color:var(--primary-ink); margin:0 0 .5rem; }
.bk-cf-heading    { font-size:clamp(1.35rem,3vw,2rem); font-weight:700; margin:0 0 .75rem; line-height:1.15; }
.bk-cf-intro      { color:var(--muted); line-height:1.7;
  & p { margin:0 0 .75rem; }
}
.bk-cf-split      { display:grid; grid-template-columns:1fr 1fr; gap:3.5rem; align-items:start; }
/* Stacked, the companion is a sibling of the form rather than a column beside
   it, and none of its three parts carried a measure: on the site builder's
   contact page at 1440px the intro ran x=42..1398 as one 1356px line, roughly
   175 characters, above a form capped at 36rem and centred. Give the stacked
   parts the form's own measure so the block is one column. `.bk-cf-companion`
   exists only inside `.bk-cf-split` (`shared/_blocks.tmpl`), where the column
   sets the width and this must not. */
:not(.bk-cf-companion) > .bk-cf-img,
:not(.bk-cf-companion) > .bk-cf-eyebrow,
:not(.bk-cf-companion) > .bk-cf-heading,
:not(.bk-cf-companion) > .bk-cf-intro { max-width:36rem; margin-inline:auto; }
@media(max-width:700px) { .bk-cf-split { grid-template-columns:1fr; gap:2rem; } }
/* contact_form: padding at the wrapper tier, for all four variants.
   padding-inline made room on the sides but never on top/bottom (#1323): the
   band's own colour scheme (.bk-cs-*, 06-schemes-variants.css) is on the
   `.page-block` wrapper, so ALL of its vertical space came from
   `.page-block { margin-bottom:2.5rem }` (02-layout.css) — outside the band —
   leaving the tint with no bottom edge of its own and the form's shadow
   clipped by the section boundary. padding-block gives the band symmetric
   room the way cta_band's own bleed band already does.
   padding-inline then landed on `.bk-contact-form` alone (variant b), so at
   1440px the form's fields sat inset by the padding while the heading,
   intro, eyebrow and the "email us directly" line below the form kept using
   their own max-width:36rem box with no inset, leaving the two columns
   offset from each other (#1409) — fixed by repeating the padding across
   every one of the block's own children (b), then again as `.bk-cf2`'s own
   padding (d). Both were still a per-variant fix: `.bk-cf-split` (variant c)
   and the plain a layout, which put nothing between their content and the
   page-block's own edge, never got either round (#1448). `.bk-contact-form`,
   `.bk-cf-split` and `.bk-cf2` are all direct children of one wrapper,
   `.page-block--contact_form`, regardless of variant, so setting the padding
   there once reaches every variant's content without enumerating it. */
.page-block--contact_form { padding-block:clamp(2.5rem,6vw,5rem); padding-inline:clamp(1.5rem,6vw,6rem); }
/* contact_form: b: full width */
.page-block--contact_form.bk-variant-b { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }

/* contact_form: d: concierge two-column (form + direct-contact card + badges) */
.page-block--contact_form.bk-variant-d { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }
.bk-variant-d .bk-cf2          { max-width:1180px; margin-inline:auto;
                                 display:grid; grid-template-columns:1.25fr .85fr; gap:clamp(1.5rem,4vw,4rem); align-items:start; }
.bk-variant-d .bk-cf2--noaside { grid-template-columns:minmax(0,46rem); justify-content:center; }
@media(max-width:820px) { .bk-variant-d .bk-cf2 { grid-template-columns:1fr; gap:1.75rem; } }
.bk-cf2-eyebrow        { display:flex; align-items:center; gap:.85rem; font-size:.72rem; font-weight:700;
                         letter-spacing:.16em; text-transform:uppercase; color:var(--primary-ink); margin:0 0 1.1rem;
  & span   { width:2.5rem; height:1px; background:currentColor; opacity:.6; flex:none; }
}
.bk-cf2-eyebrow--muted { color:var(--muted); }
.bk-cf2-card           { background:var(--card); border:1px solid var(--border); border-radius:calc(1rem * var(--pf-radius-k,1));
                         padding:clamp(1.5rem,3vw,2.5rem);
                         box-shadow:0 1px 2px var(--pf-black-04),0 14px 36px -18px var(--pf-black-22); }
.bk-cf2-col--aside     { display:flex; flex-direction:column; gap:1.5rem;
  & .bk-cf2-card { padding:clamp(1.25rem,2.5vw,2rem); }
}
/* form */
.bk-cf2-form           { gap:1.4rem; max-width:none; margin:0; }
.bk-cf2-grid2          { display:grid; grid-template-columns:1fr 1fr; gap:1.4rem; }
@media(max-width:520px){ .bk-cf2-grid2 { grid-template-columns:1fr; } }
.bk-cf2-field          { display:flex; flex-direction:column; gap:.5rem;
  & span     { font-size:.72rem; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); }
}
.bk-cf2-required span::after { content:" *"; color:var(--primary); }
.bk-cf2-form input,
.bk-cf2-form textarea,
.bk-cf2-form select    { padding:.85rem 1rem; border-radius:var(--pf-radius-md); border:1px solid var(--border);
                         background:color-mix(in srgb,var(--card) 90%,var(--text) 5%); color:var(--text);
                         font-family:inherit; font-size:.95rem; transition:border-color .15s,box-shadow .15s; }
.bk-cf2-form textarea  { resize:vertical; min-height:8rem; }
.bk-cf2-form select    { appearance:none; cursor:pointer; padding-right:2.5rem;
                         background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
                         background-repeat:no-repeat; background-position:right 1rem center; }
.bk-cf2-form input:focus,
.bk-cf2-form textarea:focus,
.bk-cf2-form select:focus { outline:none; border-color:var(--primary);
                            box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 18%,transparent); }
.bk-cf2-submit         { display:inline-flex; align-items:center; justify-content:center; min-height:44px;
                         width:100%; background:var(--primary); color:var(--on-primary,#fff); border:none;
                         padding:1rem 1.5rem; border-radius:var(--pf-radius-md); font-size:.78rem; font-weight:700;
                         letter-spacing:.12em; text-transform:uppercase; cursor:pointer; transition:opacity .15s;
  &:hover   { opacity:.9; }
}
/* direct-contact rows */
.bk-cf2-row            { display:flex; gap:1rem; align-items:flex-start; }
.bk-cf2-ic             { flex:none; width:2.6rem; height:2.6rem; border-radius:50%;
                         display:flex; align-items:center; justify-content:center;
                         background:color-mix(in srgb,var(--primary) 12%,var(--card)); color:var(--primary);
  & svg         { width:1.2rem; height:1.2rem; }
}
.bk-cf2-rowbody        { min-width:0; }
.bk-cf2-rowlabel       { font-size:.7rem; font-weight:700; letter-spacing:.12em; text-transform:uppercase;
                         color:var(--muted); margin-bottom:.3rem; }
.bk-cf2-rowval         { color:var(--text); text-decoration:none; font-size:1rem; word-break:break-word; }
a.bk-cf2-rowval        { display:inline-flex; align-items:center; min-height:44px; }
a.bk-cf2-rowval:hover  { color:var(--primary); }
.bk-cf2-rowval--strong { font-weight:700; }
.bk-cf2-rownote        { color:var(--muted); font-size:.9rem; margin:.15rem 0; }
.bk-cf2-rownote--em    { font-style:italic; line-height:1.5; }
.bk-cf2-rowsep         { border:none; border-top:1px solid var(--border); margin:1.25rem 0; }
/* response card (dark, highlighted) */
.bk-cf2-response       { background:var(--text); border-color:var(--text); color:var(--bg,#fff); }
.bk-cf2-response-head  { display:flex; align-items:center; gap:.7rem; margin-bottom:.85rem; }
.bk-cf2-bolt           { color:var(--pf-amber); display:flex;
  & svg       { width:1.3rem; height:1.3rem; }
}
.bk-cf2-response-title { font-size:1.1rem; font-weight:700; }
.bk-cf2-response-body  { margin:0; line-height:1.65; opacity:.85; font-size:.95rem; }
/* trust badges */
.bk-cf2-badges         { display:flex; flex-wrap:wrap; justify-content:center; gap:1.5rem 1.9rem;
                         max-width:1180px; margin:2.5rem auto 0; padding-inline:clamp(1.25rem,5vw,4rem); }
.bk-cf2-badge          { display:inline-flex; align-items:center; gap:.5rem; color:var(--muted); font-size:.9rem; }
.bk-cf2-badge-ic       { display:flex; color:var(--primary);
  & svg   { width:1rem; height:1rem; }
}
/* edit-mode hints: surface the optional concierge fields when they're empty so
   the photographer can see what's available. Owner-only (gated on .EditMode). */
.bk-cf2-aside-hint     { opacity:.9; }
.bk-cf2-hintcard       { border-style:dashed; box-shadow:none;
                         background:color-mix(in srgb,var(--primary) 5%,var(--card)); }
.bk-cf2-hint-title     { margin:0 0 .5rem; font-size:.95rem; font-weight:700; color:var(--text); }
.bk-cf2-hint-text      { margin:0; font-size:.88rem; line-height:1.6; color:var(--muted); }
.bk-cf2-fieldhint      { border:1px dashed color-mix(in srgb,var(--primary) 35%,var(--border));
                         border-radius:var(--pf-radius-md); padding:.7rem .9rem; font-size:.82rem; line-height:1.5;
                         color:var(--muted); background:color-mix(in srgb,var(--primary) 4%,transparent);
  & strong { color:var(--text); font-weight:700; }
}
.bk-cf2-fieldhint--center { max-width:1180px; margin:2.5rem auto 0; text-align:center;
                            margin-inline:auto; }

/* link_columns: d: full width */
.page-block--link_columns.bk-variant-d { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .bk-link-columns { padding-inline:clamp(1.5rem,6vw,6rem); }
}

/* cta_band: bleed on EVERY variant.
   The band's whole argument is that it runs edge to edge at the point where the
   visitor has to decide: a contained closing CTA is a callout, which is the
   block cta_band exists not to be. So this is a type-level tier, like marquee's
   (11-marquee.css) and banner's (07-links-banner.css), and it lives here rather
   than in 02-layout.css with the four concentric tiers because those are
   `width: min(cap, 100% - gutter)` rules and this one is the 100vw breakout.
   The same shape as every other bleed in this file, which is ordered after
   02-layout precisely so breakouts win. */
.page-block--cta_band { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }

/* divider: c: a short, bold accent mark in a full-width band, not just a
   wider copy of a's hairline (#1501 A2: at the default width the only
   difference from `a` was where the line stopped, easy to miss entirely at
   a glance). Thicker, coloured, and capped to a short measure so it reads
   as a deliberate ornament centred in the band rather than a rule that
   forgot to stop. */
.page-block--divider.bk-variant-c { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0;
  & .bk-divider { border-top-width:3px; border-color:var(--primary); max-width:220px; margin-inline:auto; }
}

/* section: b: full width · c: bordered · d: tinted · e: contained narrow */
.page-block--section.bk-variant-b { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }
/* c: wrap the group in a soft outlined panel */
.page-block--section.bk-variant-c { border:1px solid var(--border); border-radius:calc(1.1rem * var(--pf-radius-k,1));
  padding:clamp(1.5rem,4vw,2.75rem); }
/* d: tinted backdrop. c is a white/card panel distinguished only by a 1px
   border; a border that thin survives the distinctness gate's downscale as
   noise, so c and d measured near-identical even though one has a visible
   outline and the other doesn't (#1501, A3). A stronger tint (was 5%, barely
   off var(--card)) gives d a fill colour that reads as different from c's
   plain panel at a glance, not just up close.

   The tint is mixed against EACH scheme's own ground, not a single fixed
   var(--card): the old unconditional color-mix(..., var(--card)) painted the
   same light tint under .bk-cs-dark/-dark-accent/-accent too, silently
   replacing that scheme's own (dark or brand-coloured) background. A child's
   text colour is chosen for the scheme it's actually in, so on a dark
   scheme it came out light-on-light-tint: unreadable. Increasing the tint
   strength (see above) only made that pre-existing mismatch easier to hit,
   so it gets fixed here rather than just dialled back down. */
.page-block--section.bk-variant-d {
  border-radius:calc(1.1rem * var(--pf-radius-k,1)); padding:clamp(1.5rem,4vw,2.75rem);
  background:color-mix(in srgb, var(--primary) 16%, var(--bg));
  &.bk-cs-light-accent { background:color-mix(in srgb, var(--primary) 16%, var(--bg-accent,color-mix(in srgb,var(--primary) 10%,var(--bg)))); }
  /* Darker, not lighter: on-primary ink is tuned against the scheme's own
     plain primary ground, and lightening toward --on-primary ate the margin
     it needs (3.62:1 measured at a 16% white mix). Mixing toward black keeps
     the tint visible without touching that ink. */
  &.bk-cs-accent { background:color-mix(in srgb, black 16%, var(--primary)); }
  &.bk-cs-dark-accent { background:color-mix(in srgb, var(--primary) 16%, var(--dark-bg,#1e293b)); }
  &.bk-cs-dark { background:color-mix(in srgb, var(--primary) 16%, var(--dark-bg,#0b0b0b)); }
  /* A child's secondary copy (a nested image's figcaption) resolves the
     scheme's MUTED ink, tuned for that scheme's own untinted ground; blended
     16% toward primary, three of the five grounds no longer clear 4.5:1 for
     it. Full-strength ink (not muted) restores the margin on the tint
     without touching the scheme's real background elsewhere. */
  & figcaption { color:var(--text); }
  &.bk-cs-accent figcaption { color:var(--on-primary,#fff); }
  &.bk-cs-dark-accent figcaption,
  &.bk-cs-dark figcaption { color:var(--dark-text,#f1f5f9); }
}
/* e: keep the content in a centred narrow measure */
.page-block--section.bk-variant-e .bs-flex { max-width:720px; margin-inline:auto; }

/* columns: a responsive multi-column layout container holding nested child
   blocks. The column count + width ratios come from the block variant; on
   narrow viewports the grid collapses to a single stacked column. */
.bk-columns {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  align-items: start;
  width: 100%;
}

/* Each column is a grid cell; its children stack vertically with a gentle gap. */
.bk-columns .bk-col {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  min-width: 0; /* let content (images, forms) shrink. overflowing */
}

/* Variant-driven track templates. a/b/c = equal columns; d/e/f/g = ratios. */
.bk-columns--a { grid-template-columns: 1fr 1fr; }
.bk-columns--b { grid-template-columns: 1fr 1fr 1fr; }
.bk-columns--c { grid-template-columns: repeat(4, 1fr); }
.bk-columns--d { grid-template-columns: 2fr 1fr; }   /* wide left  (2:1) */
.bk-columns--e { grid-template-columns: 1fr 2fr; }   /* wide right (1:2) */
.bk-columns--f { grid-template-columns: 1fr 2fr 1fr; } /* wide centre (1:2:1) */
.bk-columns--g { grid-template-columns: 3fr 2fr; }   /* golden-ish (3:2) */

/* Collapse to one stacked column on phones: the responsive requirement. */
@media (max-width: 767px) {
  .bk-columns {
    grid-template-columns: 1fr;
    gap: 1.5rem;
  }
}

/* Nested children shouldn't add their own outer page margins inside a cell. */
.bk-columns .bk-col > * {
  margin: 0;
}
/* ── Marquee (oversized claim over a drifting photo strip) ─────────────────── */
/* The loop is pure CSS: each row holds two identical lanes and slides by -50%,
   which is exactly one lane's width, so the restart lands on a matching frame
   and the seam never shows. Duration rides in as --mq-dur from the renderer
   (seconds-per-photo × photo count) so more photos means a longer strip rather
   than a faster one. The strip is decorative: it never pauses and nothing in it
   is clickable. */
.page-block--marquee { width:var(--pf-vw,100vw); margin-left:calc(50% - var(--pf-vw,100vw)/2); margin-right:0; }
.bk-marquee          { position:relative; overflow:hidden; padding:clamp(2rem,5vw,4rem) 0; }
.bk-marquee-rows     { display:flex; flex-direction:column; gap:clamp(.5rem,1vw,1rem); }
.bk-marquee-row      { display:flex; width:max-content; }
.bk-marquee-lane     { display:flex; flex:none; gap:clamp(.5rem,1vw,1rem); padding-right:clamp(.5rem,1vw,1rem); }
.bk-marquee-lane img { flex:none; height:clamp(150px,20vw,280px); width:auto; object-fit:cover; border-radius:calc(.25rem * var(--pf-radius-k,1)); }

/* The claim sits over the strip. Its own radial tint is the scrim: painted by
   the same box, so it sits under the text without an extra element. pointer-events
   are off so the claim never becomes a click target over the (inert) photos. */
.bk-marquee-claim {
  position:absolute; inset:0; pointer-events:none;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  text-align:center; padding:0 1rem;
  background:radial-gradient(64% 58% at 50% 50%, color-mix(in srgb, var(--bg) 62%, transparent), transparent 76%);
}
.bk-marquee-kicker {
  font-size:clamp(.95rem,1.8vw,1.5rem); font-weight:600; line-height:1.15;
  margin-bottom:.35em; max-width:22ch;
}
.bk-marquee-heading {
  font-size:clamp(2.25rem,8.5vw,7.5rem); font-weight:800; line-height:.92;
  letter-spacing:-.02em; text-transform:uppercase; color:var(--primary);
  /* The strip clips overflow, so a long claim must wrap. be cut off. */
  max-width:100%; overflow-wrap:break-word; text-wrap:balance;
}
.page-block--marquee {
  /* variant b: claim pinned left, as in an editorial lockup */
  &.bk-variant-b {
    & .bk-marquee-claim  { align-items:flex-start; text-align:left; padding-inline:clamp(1.5rem,5vw,4rem);
                           background:linear-gradient(to right, color-mix(in srgb, var(--bg) 68%, transparent) 30%, transparent 68%); }
    & .bk-marquee-kicker { max-width:18ch; }
  }
}
@media (prefers-reduced-motion: no-preference) {
  .bk-marquee-row--left  { animation:mqDriftLeft  var(--mq-dur,40s) linear infinite; }
  .bk-marquee-row--right { animation:mqDriftRight var(--mq-dur,40s) linear infinite; }
  @keyframes mqDriftLeft  { from { transform:translateX(0);    } to { transform:translateX(-50%); } }
  @keyframes mqDriftRight { from { transform:translateX(-50%); } to { transform:translateX(0);    } }
}
@media (max-width:640px) {
  .bk-marquee-lane img    { height:clamp(110px,32vw,170px); }
  .bk-marquee-heading     { line-height:.98; }
}

/* Skeleton (edit mode, before any photo is picked) */
.sk-marquee       { display:flex; gap:.75rem; overflow:hidden; }
.sk-marquee .sk-tile { flex:none; width:22%; }
.sk-marquee-claim {
  text-align:center; color:var(--muted); margin-top:1rem;
  font-size:clamp(1.25rem,4vw,2.5rem); font-weight:800; text-transform:uppercase;
}
/* ── Page-level restyle presets (#304) ────────────────────────────────────────

   Three mood systems, each a cohesive bundle of type scale + colour treatment +
   spacing rhythm, applied to a whole page by one class on .pf-custom-page.

   Every preset works by REDEFINING THE THEME TOKENS. by restyling
   blocks. --font-display, --text, --muted, --primary, --w-gutter and the width
   caps are all inherited, so overriding them re-art-directs every block inside
  : including blocks added tomorrow, and including the nine themes' own rules,
   which read the same variables. A preset that targeted block classes would
   have to enumerate 27 block types and would go stale on the 28th.

   Deliberately relative, never absolute: colours are color-mix() over the
   theme's own --bg/--text/--primary, so a preset reads as a treatment OF the
   photographer's palette. a replacement for it. Hard-coding cream
   and charcoal would make all three presets look identical across nine themes
   and would break every dark-appearance site.

   ── Why the colour tokens are set on `> *` and not on the wrapper ────────────
   A custom property cannot be derived from ITSELF on the same element:
   `--text: color-mix(in srgb, var(--text) 86%, #6b4a2f)` is a cyclic
   dependency, so the declaration is dropped as guaranteed-invalid. It fails
   SILENTLY: no console error, no fallback, the page just renders unchanged,
   which is exactly how the first version of this file shipped a warm-film
   preset visually identical to no preset at all.

   Setting the derived token one level down breaks the cycle: on a child,
   `var(--text)` resolves to the value inherited from the wrapper (still the
   theme's), and every descendant inherits the derived one from there. Blocks
   are direct children of .pf-custom-page, so `> *` covers the whole page.

   Literal-valued tokens (widths, gutters, font stacks) have no such problem and
   stay on the wrapper where they belong. */

/* ── Editorial ────────────────────────────────────────────────────────────────
   Magazine restraint. Serif display at a large but not shouty scale, prose
   pulled to a tighter measure, wide margins, colour dialled down so the
   photographs carry the page. */
.pf-custom-page.pf-preset-editorial {
  --font-display: Georgia, 'Iowan Old Style', 'Times New Roman', serif;
  --w-measure: 680px;
  --w-default: 1180px;
  --w-wide: 1400px;
  --w-gutter: 5rem;

  & h1, & h2, & h3 {
    font-family: var(--font-display);
    font-weight: 400;
    letter-spacing: -0.015em;
    line-height: 1.12;
  }
  & h1 { font-size: clamp(2.4rem, 4.4vw, 3.9rem); }
  & h2 { font-size: clamp(1.8rem, 3vw, 2.7rem); }
  & p  { line-height: 1.75; }

  & .page-block { margin-bottom: 4rem; }
}
/* Quiet the accent. remove it: a desaturated primary still marks
   links and buttons, but stops competing with the imagery.

   No --muted override here (nor in warm film/bold minimal below): a fixed
   color-mix(text, X%, transparent) can never guarantee AA against an
   arbitrary photographer's own text/bg pair (#836): mixing toward
   transparent only ever LOSES contrast as the percentage climbs, so there is
   no percentage that is safe for every brand palette. --muted is left to
   inherit the theme's own :root value instead, which render.go computes per
   render with a real contrast floor (mutedTierFloor). */
.pf-custom-page.pf-preset-editorial > * {
  --primary: color-mix(in srgb, var(--primary) 45%, var(--text));
}

/* ── Warm film ────────────────────────────────────────────────────────────────
   Analogue warmth. Contrast softened off pure black, the ground warmed toward
   cream, everything given air. The most forgiving of the three on a mixed set
   of photographs, which is the point. */
.pf-custom-page.pf-preset-warm-film {
  --w-measure: 760px;
  --w-gutter: 4rem;

  /* A distinct property name, so deriving from --bg here is not a cycle. */
  --wf-ground: color-mix(in srgb, var(--bg) 90%, #d9a441);

  /* Regular properties, so they read the theme's own --bg/--text with no cycle.
     The wrapper paints the warmed ground; the token block below hands the same
     treatment to everything inside it. */
  background: var(--wf-ground);
  color: color-mix(in srgb, var(--text) 84%, #6b4a2f);

  /* The article is width-capped inside main.container, so painting its
     background alone renders the mood as a CARD floating on the theme's white.
     Which reads as a component, not as a restyled page. Spread the same colour
     sideways past the cap and clip the vertical overflow, so the ground runs
     edge to edge without changing layout or affecting the header and footer. */
  box-shadow: 0 0 0 100vmax var(--wf-ground);
  clip-path: inset(0 -100vmax);

  & h1, & h2, & h3 {
    font-weight: 500;
    letter-spacing: -0.005em;
    line-height: 1.2;
  }
  & h1 { font-size: clamp(2.2rem, 3.8vw, 3.3rem); }
  & p  { line-height: 1.8; }

  & .page-block { margin-bottom: 3.5rem; }
  /* Softened edges everywhere the theme allows one. */
  & img, & .bk-card, & .bs-round { border-radius: calc(10px * var(--pf-radius-k,1)); }
}
.pf-custom-page.pf-preset-warm-film > * {
  --bg: color-mix(in srgb, var(--bg) 90%, #d9a441);
  --text: color-mix(in srgb, var(--text) 84%, #6b4a2f);
  /* --muted deliberately not redefined here: see the editorial preset's
     comment above; the theme's own server-computed floor is a close enough
     read on this preset's small (10-16%) warm shift, and no static ratio can
     hold a guarantee the way that per-render floor can. */
  --primary: color-mix(in srgb, var(--primary) 80%, #c8763a);
  --border: color-mix(in srgb, var(--text) 14%, transparent);
  color: var(--text);
}

/* ── Bold minimal ─────────────────────────────────────────────────────────────
   High contrast, oversized type, tight rhythm, hard edges. The loudest preset:
   headings run to the full width and the spacing closes up so the page reads as
   a single dense statement. a sequence of sections. */
.pf-custom-page.pf-preset-bold-minimal {
  --w-measure: 900px;
  --w-default: 1500px;
  --w-wide: 1760px;
  --w-gutter: 2rem;

  & h1, & h2, & h3 {
    font-weight: 800;
    letter-spacing: -0.045em;
    line-height: 0.98;
    text-transform: uppercase;
  }
  & h1 { font-size: clamp(2.8rem, 7.5vw, 6rem); }
  & h2 { font-size: clamp(2rem, 4.4vw, 3.4rem); }
  & p  { line-height: 1.55; }

  & .page-block { margin-bottom: 1.5rem; }
  /* Hard edges: override the rounding themes and blocks apply by default. */
  & img, & .bk-card, & .bs-round { border-radius: 0; }
}
/* Contrast is raised by pushing the border tone toward the text colour rather
   than by forcing --text itself: "more contrast" has no direction that is
   correct on both a light and a dark site, and the theme already knows which
   way its own text points. --muted is left inherited: see the editorial
   preset's comment above. */
.pf-custom-page.pf-preset-bold-minimal > * {
  --border: color-mix(in srgb, var(--text) 30%, transparent);
}
/* ── Site-wide photo grade ────────────────────────────────────────────────────
   One consistent tone across every photo on the site. The owner picks a preset
   + intensity in Design → Photo grade; render.go resolves it to a CSS filter
   chain and emits `--pf-grade` into :root (see PhotoGradeFilter for why this is
   a render-layer filter. a baked variant).

   The var is unset on an ungraded site, so every rule below falls back to
   `none` and costs nothing. Scoped to `.page-block`: the wrapper every block
   renders inside: plus the lightbox, so site chrome (nav logo, badges, icons)
   keeps its own colour.                                                       */

.page-block img,
.lb-img {
  filter: var(--pf-grade, none);
}

/* The cover hero paints its photo as a background-image, so it can't be
   filtered directly without dragging the headline and CTA along with it.
   .bph-overlay already sits inset:0 above the photo and below .bph-content
   (z-index:1), which makes it the exact layer to grade through. */
.bk-portfolio-hero--cover .bph-overlay {
  -webkit-backdrop-filter: var(--pf-grade, none);
  backdrop-filter: var(--pf-grade, none);
}
/* ── Blog post page (#1646) ──────────────────────────────────────────────── */
/* Same "release the container cap" rule .pf-custom-page uses (02-layout.css):
   a post's body blocks can breakout full-bleed the same as any other page. */
main.container:has(> .pf-blogpost) { max-width:none; }

.pf-blogpost {
  --w-measure:820px; --w-default:1360px; --w-wide:1600px; --w-gutter:3rem;
}

.pf-blogpost-head {
  max-width:820px; margin:0 auto 2rem; padding:0 var(--w-gutter,3rem); text-align:center;
}
.pf-blogpost-title   { font-family:var(--font-display); font-size:clamp(2rem,5vw,3rem);
                        font-weight:700; line-height:1.1; margin:0 0 .75rem; }
.pf-blogpost-meta     { color:var(--muted); font-size:.95rem; margin:0 0 1rem; }
.pf-blogpost-excerpt  { color:var(--muted); font-size:1.15rem; margin:0; }

.pf-blogpost-cover     { margin:0 0 2.5rem; }
.pf-blogpost-cover img { width:100%; height:auto; display:block; border-radius:calc(.5rem * var(--pf-radius-k,1)); }

.pf-blogpost-body { margin-bottom:2.5rem; }

.pf-blogpost-nav {
  display:flex; justify-content:space-between; gap:1.5rem; max-width:1360px;
  margin:0 auto 3rem; padding:2rem var(--w-gutter,3rem) 0; border-top:1px solid var(--border);
}
.pf-blogpost-nav-link  { display:flex; flex-direction:column; gap:.25rem;
                          text-decoration:none; color:inherit; max-width:45%;
                          padding:.5rem 0; margin:-.5rem 0; }
.pf-blogpost-nav-next  { margin-left:auto; text-align:right; }
.pf-blogpost-nav-label { font-size:.8rem; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
.pf-blogpost-nav-title { font-weight:600; }

@media(max-width:640px) {
  .pf-blogpost-head     { padding:0 1.25rem; }
  .pf-blogpost-nav      { padding:1.5rem 1.25rem 0; flex-direction:column; }
  .pf-blogpost-nav-link { max-width:100%; }
  .pf-blogpost-nav-next { margin-left:0; text-align:left; }
}

/* Tags (#1679): the chips under a post's body, and the /blog/tag/{slug} page
   that reuses the blog_posts block's card grid (03-blocks-content.css). */
.pf-blogpost-tags {
  display:flex; flex-wrap:wrap; gap:.5rem; list-style:none; margin:0 auto 2rem;
  padding:0 var(--w-gutter,3rem); max-width:820px;
}
.pf-blogpost-tag {
  display:inline-flex; align-items:center; min-height:44px; padding:0 1rem;
  border:1px solid var(--border); border-radius:999px; color:inherit;
  text-decoration:none; font-size:.9rem;
}
.pf-blogpost-tag:hover { border-color:var(--text); }

.pf-blogtag-title {
  font-family:var(--font-display); font-size:clamp(1.75rem,4vw,2.5rem);
  font-weight:700; line-height:1.15; margin:0;
}

@media(max-width:640px) {
  .pf-blogpost-tags { padding:0 1.25rem; }
}

/* Comments (#1694): the approved thread and the visitor form under a post. The
   form reuses .bk-contact-form for its field and button styling. */
.pf-comments { max-width:820px; margin:0 auto 3rem; padding:2rem var(--w-gutter,3rem) 0; border-top:1px solid var(--border); }
.pf-comments-title { font-family:var(--font-display); font-size:1.5rem; margin:0 0 1rem; }
.pf-comments-notice { padding:.75rem 1rem; border-radius:calc(.5rem * var(--pf-radius-k,1)); margin:0 0 1.5rem;
                      background:color-mix(in srgb,var(--primary) 10%,var(--card));
                      border:1px solid color-mix(in srgb,var(--primary) 25%,transparent); }
.pf-comments-notice-error { border-color:var(--text); }
.pf-comments-list { list-style:none; margin:0 0 2rem; padding:0; display:flex; flex-direction:column; gap:1.5rem; }
.pf-comments-replies { margin:1rem 0 0 1.5rem; padding-left:1rem; border-left:2px solid var(--border); gap:1rem; }
.pf-comment-head { margin:0 0 .35rem; display:flex; flex-wrap:wrap; align-items:center; gap:.25rem .75rem; }
.pf-comment-author a { display:inline-flex; align-items:center; min-height:44px; color:inherit; }
.pf-comment-badge { font-size:.75rem; text-transform:uppercase; letter-spacing:.05em; padding:.1rem .5rem;
                    border:1px solid var(--border); border-radius:999px; color:var(--muted); }
.pf-comment-date { color:var(--muted); font-size:.85rem; }
.pf-comment-body { overflow-wrap:anywhere; }
.pf-comment-body a { color:inherit; text-decoration:underline; }
.pf-comments-empty { color:var(--muted); margin:0 0 2rem; }
.pf-comments-closed { color:var(--muted); margin:0; }
.pf-comment-reply { margin:.5rem 0 0; }
.pf-comment-reply-toggle { display:inline-flex; align-items:center; min-height:44px; cursor:pointer; color:var(--muted); text-decoration:underline; }
.pf-comment-reply[open] > .pf-comments-form { margin-top:.5rem; }
.pf-comments-form { margin-inline:0; max-width:none; }
.pf-comments-form-title { font-size:1.15rem; margin:0; }
.pf-comments-hint { color:var(--muted); }
/* Honeypot: off-screen rather than display:none, which some bots skip. */
.pf-comments-trap { position:absolute; left:-10000px; width:1px; height:1px; overflow:hidden; }

@media(max-width:640px) {
  .pf-comments { padding:1.5rem 1.25rem 0; }
  .pf-comments-replies { margin-left:.5rem; }
}
/* ── Breadcrumb trail on nested pages (#1727) ───────────────────────────────
   Sits above the block list, on the same width and gutter as a default-tier
   block. Colour comes from the injected theme tokens only (--text, --muted),
   so every portfolio theme and its dark mode carry it without a per-theme
   rule. The links are --text, the same ground as body copy; --muted only
   paints the decorative separators. */
.pf-crumbs {
  width:min(var(--w-default,1360px), 100% - var(--w-gutter,3rem));
  margin:0 auto .5rem; box-sizing:border-box; font-size:.9rem;
}
.pf-crumbs ol {
  list-style:none; margin:0; padding:0;
  display:flex; flex-wrap:wrap; align-items:center; column-gap:.5rem;
}
.pf-crumbs li { display:inline-flex; align-items:center; min-height:44px; min-width:0; }
/* The separator trails its crumb, so a wrapped trail ends a line on "/"
   instead of starting the next one with it. */
.pf-crumbs li:not(:last-child)::after {
  content:"/"; color:var(--muted); margin-left:.5rem;
}
.pf-crumbs a { color:var(--text); text-decoration:underline; text-underline-offset:.2em;
               display:inline-flex; align-items:center; min-height:44px; }
.pf-crumbs a:hover { text-decoration-thickness:2px; }
/* A single unbroken word (a long slug-like title) must wrap inside its crumb
   instead of pushing the page wider than the viewport. */
.pf-crumbs a, .pf-crumbs li > span { min-width:0; overflow-wrap:anywhere; }
.pf-crumbs [aria-current="page"] { font-weight:600; }
@media (max-width:639px) {
  .pf-crumbs { font-size:.85rem; }
}
