/* Page-builder blocks — shared by every theme, like commerce.css.
   Blocks reuse the theme's own component classes (.btn, .product-grid, .collection-card,
   .newsletter, .lead, .eyebrow, .muted) wherever one exists; this file only supplies the
   structure those classes do not cover, and it reads colours and fonts from the active
   theme's custom properties so a built page always matches the chosen design. */

/* Written against the **semantic colour roles**, which the theme engine emits per mode, and not
   against `--color-text`/`--color-background`. Those two come from `settings_schema` defaults, so
   `css_vars` emits the same value for light and dark — the mode-static legacy pair a test already
   forbids inside a theme stylesheet. This file is shared rather than a theme's, so it was never
   swept: in dark mode `--pb-soft` was a near-white panel behind the quote, video and FAQ blocks,
   `--pb-line` was an invisible border, `--pb-muted` was dark grey on a dark ground (3.1:1) and
   `.pb-prose h2` was near-black ink on it. Every built page, on all six themes. */
.builder-page {
  --pb-ink: var(--color-ink, var(--c-text, #0f172a));
  --pb-bg: var(--color-surface, var(--c-bg, #fff));
  --pb-line: color-mix(in srgb, var(--pb-ink) 15%, transparent);
  --pb-soft: color-mix(in srgb, var(--pb-ink) 6%, var(--pb-bg));
  --pb-muted: var(--color-ink-muted, color-mix(in srgb, var(--pb-ink) 65%, var(--pb-bg)));
  --pb-radius: 14px;
}
.builder-page .pb-block { width: min(72rem, 100% - 3rem); margin-inline: auto; padding-block: var(--pb-space, clamp(1.5rem, 4vw, 3rem)); }
.builder-page .pb-block > * + * { margin-top: 1.25rem; }
.builder-page .pb-heading { margin: 0; }
/* The contact block's sentence under its heading is not a direct child of the block, so the
   rhythm above never reached it and it sat on the heading. */
.builder-page .pb-contact-copy > .muted { margin-top: 0.75rem; }
/* `align-items: center`, not `baseline`: the heading is a large `<h2>` and the "View all" link
   is small body text, so lining up their text baselines put the link noticeably above the
   visual middle of the heading's own line height rather than beside it. The link centres its
   own text too and is a flex row of its own, so a theme whose block link is a real button (a
   filled or bordered chip, rather than the bare underlined text every current theme draws) still
   comes out centred inside its own box instead of top-aligned inside it. */
.builder-page .pb-block-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.builder-page .pb-block-head .text-link { display: inline-flex; align-items: center; text-align: center; }
.builder-page .pb-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.builder-page .pb-empty, .builder-page .pb-caption { color: var(--pb-muted); }
.builder-page .pb-placeholder {
  display: grid; place-items: center; min-height: 10rem; border: 2px dashed var(--pb-line);
  border-radius: var(--pb-radius); color: var(--pb-muted); font-size: 0.9rem;
}
/* A block with nothing to show, in the editor only — on the shop it renders nothing at all
   (`builder/blocks/_nothing.html`). It is marked out as a thing to fill in rather than styled
   as content, because that is what it is: the merchant is looking at their own page, and the
   band has to read as unfinished rather than as a section that says very little. */
.builder-page .pb-nothing {
  border: 2px dashed var(--pb-line); border-radius: var(--pb-radius);
  padding-inline: clamp(1rem, 3vw, 2rem);
}
.builder-page .pb-nothing > * + * { margin-top: 0.4rem; }
.builder-page .pb-nothing-name {
  margin: 0; font-size: 0.8rem; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--pb-muted);
}

/* The layout group ------------------------------------------------------------------------
   Alignment, width and vertical space are asked of every block (`blocks.LAYOUT_SETTINGS`) and
   arrive as these classes on its root element via `{{ layout_class }}`. A theme restyles the
   whole vocabulary by overriding this handful of rules — which is why the block templates
   carry semantic classes rather than the editor writing per-page CSS. */
.builder-page .pb-align-left { text-align: left; }
.builder-page .pb-align-center { text-align: center; }
.builder-page .pb-align-right { text-align: right; }
.builder-page .pb-align-center .pb-actions, .builder-page .pb-align-center .lead { margin-inline: auto; justify-content: center; }
.builder-page .pb-align-right .pb-actions { justify-content: flex-end; }
/* `.lead` and `.pb-prose` keep a readable measure at every width; the *container* is what the
   width setting moves, so a full-bleed hero still holds a column of legible copy. */
.builder-page .pb-align-center .pb-prose { margin-inline: auto; }
/* **And the heading is a box too.** The rule above was applied to `.lead`, `.pb-actions` and
   `.pb-prose` and not to headings — so a theme that gives a heading a `max-width` (most do,
   for a readable measure) left its box hard against the gutter while `text-align: center`
   centred the words *inside that box*. On vault's centred hero the heading's centre measured
   510px against 720px for the standfirst and the button directly beneath it: three elements
   that are all "centred" and visibly line up with nothing. Only geometry can see it — the
   markup and the CSS both read exactly as intended, which is why
   `tests/themes/test_block_alignment_browser.py` measures boxes rather than reading rules. */
.builder-page .pb-align-center .pb-heading,
.builder-page .pb-align-center .pb-hero-heading { margin-inline: auto; }
.builder-page .pb-align-right .pb-heading,
.builder-page .pb-align-right .pb-hero-heading { margin-inline: auto 0; }
.builder-page .pb-align-left .pb-heading,
.builder-page .pb-align-left .pb-hero-heading { margin-inline: 0; }
/* Alignment has to move the box, not only the text inside it. A theme styles `.lead` for its
   *own* hero and most of them centre it — aurora's is `max-width: 38rem; margin-inline: auto` —
   so a left-aligned builder hero put its heading and its button hard against the gutter and
   floated the sentence between them. That is the About page every new store ships. */
.builder-page .pb-align-left .lead, .builder-page .pb-align-left .pb-prose { margin-inline: 0; }
.builder-page .pb-align-left .pb-actions { justify-content: flex-start; }
.builder-page .pb-align-right .lead, .builder-page .pb-align-right .pb-prose { margin-inline: auto 0; }
.builder-page .pb-width-narrow { width: min(48rem, 100% - 3rem); }
.builder-page .pb-width-wide { width: min(72rem, 100% - 3rem); }
.builder-page .pb-width-full { width: 100%; }
/* A narrow block is a narrower *container*, and a centred one — so a left-aligned narrow block
   sat indented while the wide block above it started at the gutter, and a page mixing the two
   (hero wide, copy narrow: the shipped default) had two left edges and lined up with neither.
   Aligned left or right, a narrow block takes the wide track's edge and spends its slack on the
   other side. `(100% - 72rem) / 2` is that track's own gutter, floored at the 1.5rem the width
   rules use once the viewport is narrower than the track. */
.builder-page .pb-align-left.pb-width-narrow { margin-inline: max(1.5rem, (100% - 72rem) / 2) auto; }
.builder-page .pb-align-right.pb-width-narrow { margin-inline: auto max(1.5rem, (100% - 72rem) / 2); }
.builder-page .pb-width-narrow .pb-prose, .builder-page .pb-width-narrow > img { max-width: 42rem; }
.builder-page .pb-space-none { --pb-space: 0rem; }

/* The first band on a page touches the header only when it *bleeds*.
   A block's vertical space is `padding-block`, which for a toned section is painted inside the
   tone — so an inset coloured band began flush against the header with white gutters either
   side, reading as a box shoved under the navigation rather than as the top of the page. That
   was harbour (soft) and depot (inverted); a full-bleed hero touching the header is the design,
   which is why `.pb-bleed` is exempt rather than the rule being "always leave a gap". */
.builder-page > .pb-section:first-child > * > .pb-block.pb-tone-soft:not(.pb-bleed),
.builder-page > .pb-section:first-child > * > .pb-block.pb-tone-raised:not(.pb-bleed),
.builder-page > .pb-section:first-child > * > .pb-block.pb-tone-accent:not(.pb-bleed),
.builder-page > .pb-section:first-child > * > .pb-block.pb-tone-inverted:not(.pb-bleed) {
  margin-top: clamp(1.25rem, 3vw, 2.5rem);
}
/* And "first" means the first band with anything in it. A block with nothing to show renders
   nothing at all now (`blocks/_nothing.html`), so its section is still in the document and still
   `:first-child` while being zero pixels tall — which would hand the gap above to a band nobody
   can see and leave the real opening band shoved under the navigation. Its own rule rather than
   another selector in the list above: an invalid selector invalidates the whole list it is in,
   so a browser without `:has()` drops this one and keeps that one, which is the behaviour before
   the block could vanish. */
.builder-page > .pb-section:not(:has(.pb-block)) + .pb-section > * > .pb-block.pb-tone-soft:not(.pb-bleed),
.builder-page > .pb-section:not(:has(.pb-block)) + .pb-section > * > .pb-block.pb-tone-raised:not(.pb-bleed),
.builder-page > .pb-section:not(:has(.pb-block)) + .pb-section > * > .pb-block.pb-tone-accent:not(.pb-bleed),
.builder-page > .pb-section:not(:has(.pb-block)) + .pb-section > * > .pb-block.pb-tone-inverted:not(.pb-bleed) {
  margin-top: clamp(1.25rem, 3vw, 2.5rem);
}

.builder-page .pb-space-small { --pb-space: clamp(0.75rem, 2vw, 1.5rem); }
.builder-page .pb-space-medium { --pb-space: clamp(1.5rem, 4vw, 3rem); }
.builder-page .pb-space-large { --pb-space: clamp(3rem, 7vw, 6rem); }

/* Grids ---------------------------------------------------------------------------------- */
.builder-page .pb-grid { display: grid; gap: 1.5rem; }
.builder-page .pb-cols-2 { grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr)); }
.builder-page .pb-cols-3 { grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); }
.builder-page .pb-cols-4 { grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr)); }

/* A column count the merchant chose ---------------------------------------------------------
   `pb-cols-N` above is a *minimum width* for grids that fit as many cards as the page allows
   (stats, testimonials, a collection rail). It is the wrong thing for a "Columns" select: on a
   76rem page "3 columns" drew five and "2 columns" drew four, `auto-fit` collapsed the empty
   tracks so a lookbook holding one wide picture drew one, and every theme re-pointed the
   minimum to its own taste (studio and voyage to 24–26rem). So the five blocks with that select
   (gallery, columns, lookbook, product grid, collection tabs) carry `pb-count-N`, which is a
   count: exactly N on a wide screen, and fewer as the page narrows — four steps down to three on
   a tablet, and no more than two on a phone.

   `body … div:is()` is (0,2,2) on purpose. A theme sizes its own product grid from
   `body.theme-<name> .product-grid` at (0,2,1), which is why `product_grid` never honoured the
   select at all. The step-downs set the custom property, not the template, so the count is
   written once. */
.builder-page .pb-count-2 { --pb-count: 2; }
.builder-page .pb-count-3 { --pb-count: 3; }
.builder-page .pb-count-4 { --pb-count: 4; }
body .builder-page div:is(.pb-count-2, .pb-count-3, .pb-count-4) {
  grid-template-columns: repeat(var(--pb-count), minmax(0, 1fr));
}
@media (max-width: 1000px) {
  body .builder-page div.pb-count-4 { --pb-count: 3; }
}
@media (max-width: 700px) {
  body .builder-page div.pb-count-3, body .builder-page div.pb-count-4 { --pb-count: 2; }
}
/* Prose in columns is not a picture: two columns of it on a phone is a column of ragged
   fragments, so the text block goes to one. */
@media (max-width: 600px) {
  body .builder-page div.pb-count-stack { --pb-count: 1; }
}

/* Hero -----------------------------------------------------------------------------------
   A hero with a picture shows the **picture**. This was `opacity: 0.28`, which is a wash and
   not a photograph: on a light palette the image all but vanished, so a front page that had
   been given real artwork still read as an empty band of colour — and because that is the one
   screen the theme gallery previews, it is a large part of why seven different themes
   photographed as one. The legibility of the words over it is a `scrim`'s job, not the
   image's, for the reason the carousel already documents: dim the picture and there is no
   contrast figure to measure, tint behind the text and there is.

   The scrim is **two layers, and the flat one is the load-bearing half**. A merchant uploads a
   photograph nobody has measured, so a purely directional gradient is a contrast figure that
   depends on the picture: `--pb-hero-veil` covers the whole band and is what guarantees a
   floor, and `--pb-hero-cover` adds the rest behind the copy so the far side still reads as a
   photograph rather than a tint. A theme raises either for its own palette.

   It follows the block's own alignment, because a gradient that always came from the left put
   its cover on the empty side of a centred or right-aligned hero — the same shape as
   `pb-align-*` centring text without moving the box. */
.builder-page .pb-hero { position: relative; overflow: hidden; border-radius: var(--pb-radius); }
/* Raised from 0.5/0.45 after looking at a real shop: a bright, busy photograph (a flower
   stall) left the standfirst hard to read while the heading just held. The floor has to
   be set for the worst photograph a merchant will upload, not the average one — and
   nothing measures this, because the sweeps compare ink against the *painted* backdrop
   rather than against the picture. */
.builder-page .pb-hero[data-has-image] { --pb-hero-veil: 0.62; --pb-hero-cover: 0.55; }
/* `overlay` is a per-block setting (none/light/medium/strong); these win over the fallback above
   by coming later at equal specificity. A page published before the setting existed has no
   `data-overlay` attribute at all and keeps the fallback — which is `medium`, the setting's own
   default, so nothing already published changes. */
.builder-page .pb-hero[data-overlay="none"] { --pb-hero-veil: 0; --pb-hero-cover: 0; }
.builder-page .pb-hero[data-overlay="light"] { --pb-hero-veil: 0.35; --pb-hero-cover: 0.28; }
.builder-page .pb-hero[data-overlay="medium"] { --pb-hero-veil: 0.62; --pb-hero-cover: 0.55; }
.builder-page .pb-hero[data-overlay="strong"] { --pb-hero-veil: 0.8; --pb-hero-cover: 0.72; }
.builder-page .pb-hero-image { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.builder-page .pb-hero[data-has-image]::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(
      to right,
      color-mix(in srgb, var(--pb-bg) calc(var(--pb-hero-cover) * 100%), transparent) 0%,
      color-mix(in srgb, var(--pb-bg) calc(var(--pb-hero-cover) * 55%), transparent) 52%,
      transparent 88%),
    color-mix(in srgb, var(--pb-bg) calc(var(--pb-hero-veil) * 100%), transparent);
}
.builder-page .pb-hero[data-has-image].pb-align-right::after { background-position: right; transform: scaleX(-1); }
.builder-page .pb-hero[data-has-image].pb-align-center::after {
  background:
    radial-gradient(
      ellipse 70% 80% at 50% 50%,
      color-mix(in srgb, var(--pb-bg) calc(var(--pb-hero-cover) * 100%), transparent) 0%,
      transparent 100%),
    color-mix(in srgb, var(--pb-bg) calc(var(--pb-hero-veil) * 100%), transparent);
}
/* `z-index` because the scrim is an `::after`, which paints after every child: without it the
   tint that exists to make the words readable would be drawn over the words. */
.builder-page .pb-hero-body { position: relative; z-index: 1; padding: clamp(1.5rem, 5vw, 3.5rem) 0; }
.builder-page .pb-hero[data-has-image] .pb-hero-body { padding-block: clamp(3rem, 12vw, 7rem); }

/* The hero photograph settles rather than sitting still — the slow scale every theme in the
   reference set opens with. Two rules it keeps, both the reveal's:

   - **The starting scale is behind `html.js-reveal`**, the class `reveal.js` adds itself. With
     no JavaScript the picture is simply at rest, rather than left permanently 6% too large and
     cropped — the same failure mode as a hidden state nothing ever un-hides.
   - **The settled rule repeats the whole selector.** A short `.js-reveal .is-revealed
     .pb-hero-image` would be (0,3,0) against (0,5,0) here and would never apply, which is
     exactly how the block reveal came to leave every home page invisible.

   It runs **once**, on arrival. Nothing loops: a background that never stops moving is the
   thing WCAG 2.2.2 is about, and the carousel already refuses to auto-advance for that reason. */
.js-reveal .builder-page .pb-hero[data-has-image] .pb-hero-image {
  transform: scale(var(--pb-hero-zoom, 1.06));
  transition: transform var(--pb-hero-settle, 1400ms) cubic-bezier(0.22, 0.61, 0.36, 1);
}
.js-reveal .builder-page .pb-hero[data-has-image].is-revealed .pb-hero-image { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .js-reveal .builder-page .pb-hero[data-has-image] .pb-hero-image {
    transform: none;
    transition: none;
  }
}
/* The logo above a hero's heading. The plate is white on purpose and in both colour modes: a
   brand's logo is drawn for white paper, and the ground behind it here is a darkened photograph
   or whatever the palette is tonight. Every other colour on the page follows a role; this one
   follows the picture. */
.builder-page .pb-hero-logo { margin: 0 0 1.25rem; line-height: 0; }
.builder-page .pb-hero-logo img {
  display: inline-block; box-sizing: content-box;
  height: clamp(2.25rem, 6vw, 3.5rem); width: auto; max-width: min(16rem, 70vw);
  object-fit: contain; padding: 0.6rem 0.9rem;
  background: #fff; border-radius: calc(var(--pb-radius, 8px) * 0.75);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.12);
}
.builder-page .pb-hero-heading { margin: 0 0 0.5rem; }
/* Every theme resets `p` to `margin: 0`, and nothing else spaced the hero's parts — so the
   standfirst's last line rested on the button beneath it (1–3px) on the About page every new
   shop ships, on all twelve themes. (0,3,0) outranks the reset without a theme needing a rule. */
.builder-page .pb-hero-body > .lead { margin-top: 0.75rem; }
.builder-page .pb-hero-body > .pb-actions { margin-top: 1.5rem; }
/* The hero's own search box (`search`): the shared form, larger, on the page's surface so it
   reads over a photograph. A centred hero centres it; `.site-search` is `margin-inline: auto`
   for the header's sake, which would centre it under left-aligned words too. */
.builder-page .pb-hero-body > .pb-hero-search {
  margin: 1.5rem 0 0; max-width: 40rem; width: 100%;
  background: var(--pb-bg); color: var(--pb-ink);
}
.builder-page .pb-align-center .pb-hero-body > .pb-hero-search { margin-inline: auto; }
.builder-page .pb-align-right .pb-hero-body > .pb-hero-search { margin-inline: auto 0; }
.builder-page .pb-hero-search .site-search-input { padding-block: 0.85rem; font-size: 1.0625rem; }

/* Opening screen ------------------------------------------------------------------------- */
/* One photograph the size of the window, the shop's name and a way in. In flow it is a
   window-high band (that is all the editor's canvas and a visitor with no script ever see).
   `is-over` is added by the block's own template while it is the first thing on the page, and
   lifts it over the header: 58 is above every header (the phone bar's 55 is the highest) and
   below the cookie banner (60), which a visitor has to be able to answer, and the drawers.

   The words sit on the page's own surface colour washed over the photograph, the hero's
   treatment and for the same reason: the ink is the palette's, so the wash under it has to be
   too, or a dark palette gets dark words on a dark picture. Here the wash is gathered behind
   the words (`--pb-open-cover`) and thin everywhere else (`--pb-open-veil`), because this is
   the one block whose whole point is the photograph. */
.builder-page .pb-opening {
  --pb-open-veil: 0.3; --pb-open-cover: 0.8;
  position: relative; isolation: isolate; overflow: hidden; box-sizing: border-box;
  display: grid; place-items: center; text-align: center;
  min-height: 100vh; min-height: 100svh;
  background: var(--pb-soft);
}
.builder-page .pb-opening[data-overlay="none"] { --pb-open-veil: 0; --pb-open-cover: 0; }
.builder-page .pb-opening[data-overlay="light"] { --pb-open-veil: 0.15; --pb-open-cover: 0.55; }
.builder-page .pb-opening[data-overlay="medium"] { --pb-open-veil: 0.3; --pb-open-cover: 0.8; }
.builder-page .pb-opening[data-overlay="strong"] { --pb-open-veil: 0.55; --pb-open-cover: 0.92; }
.builder-page .pb-opening-image {
  position: absolute; inset: 0; z-index: -2; width: 100%; height: 100%; object-fit: cover;
}
.builder-page .pb-opening[data-has-image]::after {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(ellipse 34rem 20rem at 50% 50%,
      color-mix(in srgb, var(--pb-bg) calc(var(--pb-open-cover) * 100%), transparent) 0%,
      color-mix(in srgb, var(--pb-bg) calc(var(--pb-open-cover) * 82%), transparent) 45%,
      transparent 100%),
    color-mix(in srgb, var(--pb-bg) calc(var(--pb-open-veil) * 100%), transparent);
}
/* `.pb-block > * + *` spaces a block's parts apart; here that is the words after the
   photograph, which would sit them below the middle of the window. */
.builder-page .pb-opening > .pb-opening-body { margin-top: 0; max-width: 40rem; padding: 2rem 0; color: var(--pb-ink); }
.builder-page .pb-opening-name {
  margin: 0; font-family: var(--font-heading, inherit); color: var(--pb-ink);
  font-size: clamp(2.25rem, 7vw, 5rem); line-height: 1.05; overflow-wrap: anywhere;
}
/* (0,3,0), as the hero's are: every theme resets `p` margins, so the parts need their own. */
.builder-page .pb-opening-body > .pb-opening-line { margin-top: 1rem; font-size: 1.125rem; color: var(--pb-ink); }
.builder-page .pb-opening-body > .pb-actions { margin-top: 2rem; }
/* The way in is drawn here rather than left to each design's `.btn-outline`: several of them
   draw that with no border at all, which over a photograph is a word nobody knows to press.
   (0,4,0) so a design's own button rule, at (0,3,1) at most, does not undo it. Ink on the
   wash, and the two swapped under the pointer. */
.builder-page .pb-opening .pb-opening-enter.btn {
  display: inline-block; min-width: 9rem; padding: 0.8rem 2rem;
  border: 1px solid var(--pb-ink); border-radius: 0; background: transparent; color: var(--pb-ink);
  font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; text-decoration: none;
  box-shadow: none; transition: background-color 160ms ease, color 160ms ease;
}
.builder-page .pb-opening .pb-opening-enter.btn:hover,
.builder-page .pb-opening .pb-opening-enter.btn:focus-visible { background: var(--pb-ink); color: var(--pb-bg); }
.builder-page .pb-opening .pb-opening-enter.btn:focus-visible { outline: 2px solid var(--pb-ink); outline-offset: 3px; }
.builder-page .pb-opening-end { display: block; height: 0; outline: 0; scroll-margin-top: 6rem; }

/* The class twice for (0,4,0): the full-bleed rules further down set `width: 100%` at (0,3,0),
   which with their gutter padding is wider than the window wherever a design has not made
   every box a border box. `inset: 0` with no width is the window exactly. */
.builder-page .pb-opening.pb-opening.is-over {
  position: fixed; inset: 0; z-index: 58; width: auto; height: auto; min-height: 0;
  background: var(--pb-bg);
  transition: opacity 480ms ease, transform 480ms ease;
}
.builder-page .pb-opening.is-leaving { opacity: 0; transform: translateY(-1.5rem); pointer-events: none; }
.builder-page .pb-opening.is-away { display: none; }
/* The page behind a covering screen stays still: a wheel or a swipe over it would otherwise
   scroll the shop underneath, unseen, and leave the visitor partway down it once they are let
   in. Not when the screen has been dismissed by its link alone (no script), which has to be
   able to scroll to the mark. */
html.has-opening:not(:has(.pb-opening-end:target)) { overflow: hidden; }
/* The button with no script behind it: it links to the mark that ends the block, and a
   covering screen whose mark is the page's target has been asked to get out of the way. */
.builder-page:has(.pb-opening-end:target) .pb-opening.is-over { display: none; }
@media (prefers-reduced-motion: reduce) {
  .builder-page .pb-opening.is-over { transition: none; }
}

/* Prose ---------------------------------------------------------------------------------- */
.builder-page .pb-prose { color: var(--pb-muted); }
.builder-page .pb-prose > * + * { margin-top: 1rem; }
.builder-page .pb-prose h2, .builder-page .pb-prose h3 { color: var(--pb-ink); margin-top: 2rem; }
/* Underlined for the reason commerce.css underlines a link inside a sentence: a merchant's
   own words are exactly the case where nothing but the colour says it is a link, and a
   palette may give the accent and the ink around it the same weight. */
.builder-page .pb-prose a { color: var(--color-accent, currentColor); text-decoration: underline; text-underline-offset: 2px; }

/* Media ---------------------------------------------------------------------------------- */
.builder-page figure { margin: 0; }
.builder-page .pb-image img, .builder-page .pb-gallery img { width: 100%; border-radius: var(--pb-radius); }
.builder-page figcaption { margin-top: 0.6rem; font-size: 0.9rem; }
.builder-page .pb-video-frame { position: relative; aspect-ratio: 16 / 9; border-radius: var(--pb-radius); overflow: hidden; background: var(--pb-soft); }
.builder-page .pb-video-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* The shop's own uploaded video (`blocks/_hosted_video.html`): the player on the page, and in
   the editor its still with a line saying which video it is and whether it is ready yet. */
.builder-page .pb-hosted-video .video-player { margin: 0; }
.builder-page .pb-hosted-video .video-player-el { border-radius: var(--pb-radius); }
body .builder-page p.pb-hosted-note { margin: 0.5rem 0 0; font-size: 0.9rem; color: var(--pb-muted); }

/* Columns, quotes, FAQ -------------------------------------------------------------------- */
.builder-page .pb-column { display: grid; gap: 0.55rem; align-content: start; }
.builder-page .pb-quote { padding: 1.5rem; border: 1px solid var(--pb-line); border-radius: var(--pb-radius); background: var(--pb-soft); }
.builder-page .pb-quote blockquote { margin: 0 0 1rem; font-size: 1.05rem; }
/* Testimonials as letters: each beside its photograph, which changes sides down the page, and
   all of it one column on a phone. The paragraphs get their space back from the themes' reset. */
.builder-page .pb-letter-list { display: grid; gap: clamp(3rem, 7vw, 5.5rem); }
.builder-page .pb-letter {
  margin: 0; display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  gap: clamp(1.5rem, 4vw, 4rem); align-items: center;
}
.builder-page .pb-letter:nth-child(even) { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
.builder-page .pb-letter:nth-child(even) .pb-letter-photo { order: 2; }
.builder-page .pb-letter.pb-letter-words-only { grid-template-columns: minmax(0, 1fr); max-width: 46rem; margin-inline: auto; }
.builder-page .pb-letter-photo img { display: block; width: 100%; aspect-ratio: 4 / 5; object-fit: cover; border-radius: var(--pb-radius); }
.builder-page .pb-letter-words { max-width: 40rem; }
.builder-page .pb-letter blockquote { margin: 0; line-height: 1.75; }
.builder-page .pb-letter blockquote p + p { margin-top: 1em; }
.builder-page .pb-letter figcaption { display: grid; gap: 0.25rem; margin-top: 1.5rem; }
@media (max-width: 720px) {
  .builder-page .pb-letter,
  .builder-page .pb-letter:nth-child(even) { grid-template-columns: minmax(0, 1fr); }
  .builder-page .pb-letter:nth-child(even) .pb-letter-photo { order: 0; }
  .builder-page .pb-letter-photo img { aspect-ratio: 4 / 3; }
}
.builder-page .pb-faq-list { display: grid; gap: 0.5rem; max-width: 48rem; }
.builder-page .pb-faq-item { border: 1px solid var(--pb-line); border-radius: var(--pb-radius); padding: 0.9rem 1.1rem; }
.builder-page .pb-faq-item summary { cursor: pointer; font-weight: 600; }
.builder-page .pb-faq-item p { margin-top: 0.6rem; }

/* Lesson blocks ---------------------------------------------------------------------------
   A callout carries its signal in a border over the page's own ink on a soft ground, never a
   status colour behind text: the palettes guarantee ink on soft and nothing about amber on a tint.
   The anchors (video, file, quiz, assignment, routes) are the lesson's own panels, so they sit in
   the lesson's rhythm and only the editor's stand-in card is drawn here. */
.builder-page.lesson-blocks { display: grid; gap: 1.25rem; margin-block: 1rem; }
.builder-page .pb-callout { padding: 1rem 1.25rem; border: 1px solid var(--pb-line); border-inline-start-width: 0.3rem; border-radius: var(--pb-radius); background: var(--pb-soft); color: var(--pb-ink); }
.builder-page .pb-callout-info { border-inline-start-color: var(--color-accent, var(--pb-ink)); }
.builder-page .pb-callout-tip { border-inline-start-color: var(--color-success, var(--pb-ink)); }
.builder-page .pb-callout-warn { border-inline-start-color: var(--color-warning, var(--pb-ink)); }
body .builder-page .pb-callout-title { margin: 0 0 0.4rem; }
.builder-page .pb-callout .pb-prose > :last-child { margin-bottom: 0; }
body .builder-page p.pb-lesson-anchor { margin: 0; padding: 0.9rem 1.1rem; border: 1px dashed var(--pb-line); border-radius: var(--pb-radius); background: var(--pb-soft); }
.builder-page .pb-lesson-video .video-embed, .builder-page .pb-lesson-video video { max-width: 100%; }
/* The editor's stand-in for the player: the video's own still frame, with a play mark, so a video
   is seen on the canvas the way a picture is. The learner never sees it (the real player replaces
   it), so the ground is the page's ink and the mark is the page's background, both from the palette. */
.builder-page .pb-video-still { margin: 0; display: grid; gap: 0.5rem; }
.builder-page .pb-video-frame { position: relative; aspect-ratio: 16 / 9; overflow: hidden; border-radius: var(--pb-radius); background: var(--pb-ink); }
.builder-page .pb-video-frame img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.builder-page .pb-video-frame[data-pb-play] { cursor: pointer; }
.builder-page .pb-video-frame.is-playing { cursor: auto; }
.builder-page .pb-video-frame .video-player { margin: 0; }
.builder-page .pb-video-frame .video-player-fallback { position: absolute; inset-inline: 0; bottom: 0; margin: 0; padding: 0.5rem 0.75rem; background: var(--pb-ink); color: var(--pb-bg); }
.builder-page .pb-video-play { position: absolute; inset: 0; margin: auto; width: 4.5rem; height: 4.5rem; }
.builder-page .pb-video-play circle { fill: var(--pb-ink); fill-opacity: 0.7; stroke: var(--pb-bg); stroke-width: 2; }
.builder-page .pb-video-play path { fill: var(--pb-bg); }
body .builder-page figcaption.pb-lesson-anchor { margin: 0; padding: 0.6rem 0.9rem; border: 1px dashed var(--pb-line); border-radius: var(--pb-radius); background: var(--pb-soft); }

/* Featured product & contact --------------------------------------------------------------- */
.builder-page .pb-featured-inner { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
.builder-page .pb-featured-media img { width: 100%; border-radius: var(--pb-radius); }
.builder-page .pb-featured-body { display: grid; gap: 0.75rem; align-content: start; }
.builder-page .pb-contact { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
.builder-page .pb-contact-details { list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: 0.4rem; color: var(--pb-muted); }
.builder-page .pb-contact-form { min-width: 0; }
.builder-page .pb-contact-form textarea { width: 100%; }
/* As a button: one column, the band's words and the button that opens the dialog. */
.builder-page .pb-contact.pb-contact-popup { grid-template-columns: minmax(0, 1fr); }
.builder-page .pb-contact-popup .pb-actions { margin-top: 1.25rem; }

@media (max-width: 820px) {
  .builder-page .pb-featured-inner, .builder-page .pb-contact { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
  .builder-page *, .builder-page *::before, .builder-page *::after {
    animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
  }
}

/* The appearance group -----------------------------------------------------------------------
   `blocks.APPEARANCE_SETTINGS` — background tone, full bleed and per-side space — arrive as
   these classes alongside the layout ones. Every tone is a **pair**: a background role and the
   ink role that belongs on it, both from the theme's palette, never a colour a merchant typed.
   A free colour box is how a section ends up unreadable in dark mode; the palettes already
   guarantee their own pairs and the theme tests already measure them. */

/* Each tone restates the derived tokens rather than inheriting them. A custom property's
   `var()`s resolve against the element the property is *declared* on, so `--pb-soft` and
   `--pb-muted` computed up on `.builder-page` are mixes of the *page's* ink and background —
   which on an inverted section is the wrong pair in both halves. Same trap as `--c-accent-soft`
   being mixed at `:root` while every theme re-points the palette one element lower. */
/* `.pb-block` is on every one of these elements and is here for **specificity**, not selection.
   A tone at `.builder-page .pb-tone-accent` is (0,2,0), and a theme styling its own component —
   `body.theme-petal .newsletter { background: … }` — is (0,2,1). The newsletter block carries the
   bare `newsletter` class on purpose (that is how it borrows the theme's look), so the theme's
   background beat the tone's and an accent band rendered as the page ground: white ink on white,
   1.15:1, on the front page of every shop using that theme. Same collision as `.slot-day` meaning
   two things in one stylesheet. (0,3,0) settles it. */
.builder-page .pb-block.pb-tone-soft,
.builder-page .pb-block.pb-tone-raised,
.builder-page .pb-block.pb-tone-accent,
.builder-page .pb-block.pb-tone-inverted {
  background: var(--pb-bg);
  color: var(--pb-ink);
  border-radius: var(--pb-radius);
  padding-inline: clamp(1.25rem, 4vw, 2.5rem);
  --pb-line: color-mix(in srgb, var(--pb-ink) 18%, transparent);
  --pb-soft: color-mix(in srgb, var(--pb-ink) 8%, var(--pb-bg));
  /* Deliberately *not* a dimmed mix. On a toned section the palette has handed us one pair it
     stands behind; a 65% blend of it is a contrast figure nobody has measured, and pairing a
     dimmed signal colour with its own tint is exactly how `.pill-warn` came to measure 1.51:1
     on summit. Muted copy on a tone is the same ink, and the type scale does the work. */
  --pb-muted: var(--pb-ink);
}
.builder-page .pb-block.pb-tone-soft {
  --pb-bg: color-mix(in srgb, var(--color-ink, #0f172a) 6%, var(--color-surface, #fff));
  --pb-ink: var(--color-ink, #0f172a);
}
.builder-page .pb-block.pb-tone-raised {
  --pb-bg: var(--color-surface-raised, var(--color-surface, #fff));
  --pb-ink: var(--color-ink, #0f172a);
  border: 1px solid var(--color-border, #e2e8f0);
}
.builder-page .pb-block.pb-tone-accent {
  --pb-bg: var(--color-accent, #4f46e5);
  --pb-ink: var(--color-accent-ink, #fff);
  /* A tile tinted 8% toward its own ink is *less* readable in that ink, and an accent pair
     only has to clear AA, not clear it by a margin: studio's white on #d8352a is 4.71:1 and the
     tinted tile under it 4.26:1. On the two bold grounds a tile is the band's own colour and its
     border (`--pb-line`) is what marks its edge. */
  --pb-soft: var(--pb-bg);
}
.builder-page .pb-block.pb-tone-inverted {
  --pb-bg: var(--color-ink, #0f172a);
  --pb-ink: var(--color-surface, #fff);
  --pb-soft: var(--pb-bg);
}
.builder-page .pb-tone-accent .collection-rail .collection-card,
.builder-page .pb-tone-inverted .collection-rail .collection-card { border: 1px solid var(--pb-line); }
/* A theme's `.btn` is painted from `--c-primary` on `--c-bg`, which on an accent or inverted
   ground is a button the same colour as the band it sits in. Inside a tone the button borrows
   the pair it is standing on, the one way round that is guaranteed to read. */
.builder-page .pb-block.pb-tone-accent .btn,
.builder-page .pb-block.pb-tone-inverted .btn {
  background: var(--pb-ink);
  color: var(--pb-bg);
  border-color: var(--pb-ink);
}
.builder-page .pb-tone-accent .btn-outline,
.builder-page .pb-tone-inverted .btn-outline {
  background: transparent;
  color: var(--pb-ink);
  border: 1px solid var(--pb-ink);
}
.builder-page .pb-tone-accent a:not(.btn),
.builder-page .pb-tone-inverted a:not(.btn) { color: var(--pb-ink); }
/* **A component that paints its own ground restates the pair for its own subtree.** Everything
   above reads `--pb-ink`/`--pb-muted`, and those are the *band's* — right for what is drawn on the
   band, wrong inside a product card that is a `--color-surface` panel of its own, where they made
   white names, prices and kinds on a white card on harbour, depot, tally, vault and voyage (and
   dark on dark in the other mode). A custom property resolves where it is declared, so the card
   re-declaring the two tokens is enough for every rule above to read the card's pair without any
   of them learning what a card is: it is the "every tone restates the derived tokens" rule again,
   applied to a component rather than a tone. **A theme whose card paints a surface says so on the
   card** (`--pb-ink`, `--pb-muted`, beside the `background`); one whose card is transparent says
   nothing and correctly takes the band's. `test_block_tones_browser.py` measures both. */

/* **The newsletter borrows a theme component, and a tone moves the ground under it.** The
   block carries the bare `newsletter` class so it takes the theme's look, and a theme draws that
   look as a whole — studio's is an ink band with its heading and field written in
   `--color-surface`, syllabus's a raised strip with an ink underline. A chosen tone repaints only
   the *band* (at (0,3,0), which is what lets it beat the theme), so the parts kept the ink they
   were written for and studio's heading and email box came out 1.13:1 on the soft tint — "Join
   our mailing list", black on near-black, on the live flooringmu shop. Once a tone is chosen the
   tone's pair decides every part of the block. */
.builder-page .pb-newsletter[class*="pb-tone-"] h2 { color: var(--pb-ink); }
.builder-page .pb-newsletter[class*="pb-tone-"] .newsletter-form input {
  background: var(--pb-bg); color: var(--pb-ink);
  border-color: color-mix(in srgb, var(--pb-ink) 55%, transparent);
}
.builder-page .pb-newsletter[class*="pb-tone-"] .newsletter-form input::placeholder { color: var(--pb-muted); }
/* "From" before a price is the muted half of the price, and most designs paint it with the page's
   muted ink, which on an Accent or Inverted band is grey on colour (1.0 to 2.7:1). On the bold
   bands it takes the band's muted ink; a card that paints its own ground restates `--pb-muted` for
   itself, so inside one this is still the card's. Nothing in the sweep had a "from" price until
   the client galleries' photographs, sold as a file or a print, put one on a card. */
.builder-page .pb-block.pb-tone-accent .price-from,
.builder-page .pb-block.pb-tone-inverted .price-from { color: var(--pb-muted); }
/* The gallery code box: a field with its own ground, so on a toned band it keeps the page's
   surface pair, as the contact form's fields do. */
.builder-page .pb-gallery-code[class*="pb-tone-"] .gallery-code-row > .gallery-code-input {
  background: var(--color-surface, #fff); color: var(--color-ink, #0f172a);
}

/* A section stretched to the edge of the page must not keep the rounded corners a *contained*
   block wears — a rounded corner sitting against the browser's own edge reads as a mistake, not
   a design. `.pb-bleed` used to reset it alone, at (0,2,0), which lost outright to the tone
   rule's (0,3,0) above (a toned full-bleed section kept its corners) and to a theme's own
   `body.theme-<name> .builder-page .pb-hero { border-radius: … }` at (0,3,1) (depot, counter,
   harbour and linden's heroes, and linden's newsletter). `pb-width-full` had no reset at all,
   so a *contained*-width block set to "Full" with no tone still rounded nothing, but a toned one
   did. Doubling each class costs nothing semantically and buys (0,4,0), which beats every rule
   above it without touching any of them. */
.builder-page .pb-block.pb-bleed.pb-bleed,
.builder-page .pb-block.pb-width-full.pb-width-full {
  border-radius: 0;
}

/* Full bleed: the colour runs to both edges while the words keep a readable measure, so the
   gutter becomes padding rather than margin. Written without `100vw`, which is wider than the
   page whenever a scrollbar is showing and buys a horizontal scroll for the whole document. */
.builder-page .pb-bleed,
.builder-page .pb-bleed.pb-width-wide,
.builder-page .pb-bleed.pb-width-full,
.builder-page .pb-bleed.pb-width-narrow,
.builder-page .pb-bleed.pb-align-left.pb-width-narrow,
.builder-page .pb-bleed.pb-align-right.pb-width-narrow {
  width: 100%;
  max-width: none;
  margin-inline: 0;
  padding-inline: max(1.5rem, (100% - 72rem) / 2);
}
.builder-page .pb-bleed.pb-width-narrow,
.builder-page .pb-bleed.pb-align-left.pb-width-narrow,
.builder-page .pb-bleed.pb-align-right.pb-width-narrow {
  padding-inline: max(1.5rem, (100% - 48rem) / 2);
}

/* `space_top`/`space_bottom` override `space` on one side. Longhands, declared after
   `.pb-block`'s `padding-block` shorthand, so leaving one blank really does mean "as above". */
.builder-page .pb-top-none { padding-block-start: 0; }
.builder-page .pb-top-small { padding-block-start: clamp(0.75rem, 2vw, 1.5rem); }
.builder-page .pb-top-medium { padding-block-start: clamp(1.5rem, 4vw, 3rem); }
.builder-page .pb-top-large { padding-block-start: clamp(3rem, 7vw, 6rem); }
.builder-page .pb-bottom-none { padding-block-end: 0; }
.builder-page .pb-bottom-small { padding-block-end: clamp(0.75rem, 2vw, 1.5rem); }
.builder-page .pb-bottom-medium { padding-block-end: clamp(1.5rem, 4vw, 3rem); }
.builder-page .pb-bottom-large { padding-block-end: clamp(3rem, 7vw, 6rem); }

/* Image & text ------------------------------------------------------------------------------ */
.builder-page .pb-media { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
.builder-page .pb-media-figure { margin: 0; }
.builder-page .pb-media-figure img { width: 100%; border-radius: var(--pb-radius); }
.builder-page .pb-media-body { display: grid; gap: 0.9rem; align-content: start; }
/* `order` rather than a reversed column list, so the image stays first in the DOM: the reading
   order a screen reader follows must not depend on which side the picture is on. */
.builder-page .pb-media-right .pb-media-figure { order: 2; }

/* Divider ----------------------------------------------------------------------------------- */
.builder-page .pb-divider { display: block; }
.builder-page .pb-rule { border: 0; margin: 0; height: 1px; }
.builder-page .pb-rule-line { background: var(--pb-line); }
.builder-page .pb-rule-dots { height: 4px; background: radial-gradient(circle, var(--pb-line) 1.5px, transparent 1.6px) repeat-x center / 12px 4px; }
.builder-page .pb-rule-blank { background: none; }

/* Call to action ---------------------------------------------------------------------------- */
.builder-page .pb-banner-body { display: grid; gap: 0.75rem; max-width: 44rem; }
.builder-page .pb-align-center .pb-banner-body { margin-inline: auto; }
.builder-page .pb-align-right .pb-banner-body { margin-inline: auto 0; }

/* Social feed -------------------------------------------------------------------------------
   Cards drawn from our own copy of a shop's posts (`apps.feeds`). Palette tokens only, so the
   same rules serve a light page, a dark one, an inverted band and the footer. Every picture is
   the same square, so a row of cards agrees on its height whatever was posted; a post with no
   picture is a text card that takes the row's height instead. `body` is in the selectors because
   every theme resets `a`, `p` and `span` margins and decoration at (0,1,2). */
body .builder-page a.pb-feed-card {
  display: flex; flex-direction: column; min-width: 0; color: inherit; text-decoration: none;
  background: var(--pb-soft); border: 1px solid var(--pb-line); border-radius: var(--pb-radius);
  overflow: hidden;
}
body .builder-page a.pb-feed-card:hover { border-color: currentColor; }
body .builder-page .pb-feed-media { display: block; aspect-ratio: 1; overflow: hidden; background: var(--pb-line); }
body .builder-page .pb-feed-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
body .builder-page .pb-feed-body { display: grid; align-content: start; gap: 0.5rem; padding: 0.75rem 1rem 1rem; }
body .builder-page .pb-feed-meta { display: flex; justify-content: space-between; gap: 0.5rem; font-size: 0.8125rem; color: var(--pb-muted); }
body .builder-page .pb-feed-network { font-weight: 600; }
body .builder-page .pb-feed-text {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;
  overflow-wrap: anywhere;
}
body .builder-page .pb-feed-textual .pb-feed-text { -webkit-line-clamp: 8; }
body .builder-page .pb-feed-rail {
  display: flex; gap: 1rem; overflow-x: auto; scroll-snap-type: x proximity; padding-bottom: 0.5rem;
}
body .builder-page .pb-feed-rail > a.pb-feed-card { flex: 0 0 min(16rem, 70%); scroll-snap-align: start; }
body .builder-page .pb-feed .pb-grid { gap: 1rem; }
body .builder-page p.pb-feed-follow { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; margin-block-start: 1rem; }
body .builder-page .pb-align-center p.pb-feed-follow { justify-content: center; }

/* Buttons, logos, figures -------------------------------------------------------------------- */
.builder-page .pb-buttons .pb-actions { margin: 0; }
.builder-page .pb-logo-row { display: flex; flex-wrap: wrap; align-items: center; gap: clamp(1.5rem, 4vw, 3rem); }
.builder-page .pb-align-center .pb-logo-row { justify-content: center; }
.builder-page .pb-logo { display: inline-flex; }
.builder-page .pb-logo img { max-height: 3rem; width: auto; }
.builder-page .pb-stat { display: grid; gap: 0.25rem; }
.builder-page .pb-stat-value { font-family: var(--font-heading, inherit); font-size: clamp(1.9rem, 5vw, 2.75rem); line-height: 1.05; font-weight: 700; }
.builder-page .pb-stat-label { color: var(--pb-muted); font-size: 0.95rem; }

/* Listings — events, experiences, courses, services ------------------------------------------
   One card shape for all four (`blocks/_listing.html`), because they differ in exactly one
   line. Deliberately not the theme's own `.product-card`: that card has nowhere to put the
   date, the grade or the length, which is the only reason these blocks exist. */
/* A listing card carries a meta line and a price under the picture, so it needs more room
   than the three-up grid gives a collection card; the track is a token a theme can re-point. */
.builder-page .pb-listing { --pb-listing-min: 15rem; grid-template-columns: repeat(auto-fit, minmax(min(var(--pb-listing-min), 100%), 1fr)); }
.builder-page .pb-listing-card { display: grid; gap: 0.6rem; align-content: start; border: 1px solid var(--pb-line); border-radius: var(--pb-radius); overflow: hidden; background: var(--pb-soft); }
.builder-page .pb-listing-media { display: block; aspect-ratio: 4 / 3; background: var(--pb-soft); }
.builder-page .pb-listing-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.builder-page .pb-listing-body { display: grid; gap: 0.45rem; padding: 0 1rem 1.1rem; justify-items: start; }
.builder-page .pb-listing-meta { color: var(--pb-muted); font-size: 0.85rem; letter-spacing: 0.04em; text-transform: uppercase; margin: 0; }
.builder-page .pb-listing-title { margin: 0; font-size: 1.05rem; }
.builder-page .pb-listing-price { margin: 0; font-weight: 600; }
/* The old price beside a reduced one, in a section's own muted ink: a theme colours
   `.price-compare` for its page ground, which on a dark or accent tone is grey on the tone. */
.builder-page .pb-block .price-compare { color: var(--pb-muted); font-weight: 400; }

/* Updates: the list page, the header over one, and the filter. The card is the listing's. Spaced
   with `gap` on a grid, because every theme resets paragraph and heading margins to zero at a
   specificity a one-class margin cannot beat. */
.builder-page .pb-update-index,
.builder-page .pb-update-header { display: grid; gap: 1rem; align-content: start; }
.builder-page .pb-update-header { padding-bottom: 0; }
.builder-page .pb-update-header .pb-heading { font-size: clamp(2rem, 4.5vw, 3rem); line-height: 1.1; }
.builder-page .pb-update-byline { margin: 0; }
.builder-page .pb-update-cover { display: block; width: 100%; aspect-ratio: 16 / 9; max-height: min(28rem, 60svh); object-fit: cover; border-radius: var(--pb-radius); align-self: start; }
.builder-page .pb-update-filter { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.builder-page .pb-update-pages { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; justify-content: center; }
.builder-page .pb-update-pages p { margin: 0; }
.builder-page .pb-update-feed { margin: 0; }


/* Reviews ----------------------------------------------------------------------------------- */
.builder-page .pb-review { margin: 0; padding: 1.25rem; border: 1px solid var(--pb-line); border-radius: var(--pb-radius); background: var(--pb-soft); display: grid; gap: 0.6rem; align-content: start; }
.builder-page .pb-review-stars { margin: 0; letter-spacing: 0.15em; }
.builder-page .pb-review-title { margin: 0; font-size: 1rem; }
.builder-page .pb-review-body { margin: 0; }
.builder-page .pb-review-by { font-size: 0.9rem; }

/* Where to find us --------------------------------------------------------------------------- */
.builder-page .pb-address { font-style: normal; line-height: 1.7; }
/* Several addresses: a row of cards, spaced by `gap` (a margin on the <p> inside would lose to every
   theme's `p { margin: 0 }`). One address is the same markup in a single track. */
.builder-page .pb-places { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr)); }
.builder-page .pb-place { display: flex; flex-direction: column; gap: 0.5rem; align-items: flex-start; min-width: 0; }
.builder-page .pb-places + .pb-map-frame { margin-top: 1.5rem; }
.builder-page .pb-map-frame { height: 320px; border-radius: var(--pb-radius); overflow: hidden; border: 1px solid var(--pb-line); }
.builder-page .pb-hint { font-size: 0.85rem; color: var(--pb-muted); }

@media (max-width: 820px) {
  .builder-page .pb-media { grid-template-columns: 1fr; }
  .builder-page .pb-media-right .pb-media-figure { order: 0; }
}

/* ---- Block classes that used to live in one theme ----------------------------------------
   `collection_list`, `hero` and `newsletter` are block templates every theme renders and none
   overrides, so `builder.css` owns them — these three were defined only in the old default
   theme's stylesheet, which is the pooled-sweep hole described in commerce.css. */
.builder-page .collection-grid { display: grid; gap: 1rem; }
.builder-page .collection-rail .collection-card {
  position: relative; display: grid; align-content: end; gap: 0.2rem;
  min-height: 11rem; padding: 1.1rem; overflow: hidden;
  border-radius: var(--pb-radius, 12px);
  background-color: var(--pb-soft); background-size: cover; background-position: center;
  color: var(--pb-ink);
}
/* A tile with a photograph behind it needs its own scrim: the label is the page's ink, and a
   merchant's collection image is whatever they uploaded. */
.builder-page .collection-rail .collection-card[style*="background-image"]::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to top, color-mix(in srgb, #000 72%, transparent), transparent 68%);
}
.builder-page .collection-rail .collection-card[style*="background-image"] .collection-label,
.builder-page .collection-rail .collection-card[style*="background-image"] .collection-hint {
  position: relative; z-index: 2; color: #fff;
}
.builder-page .collection-label { font-weight: 700; font-size: 1.05rem; }
.builder-page .collection-hint { font-size: 0.85rem; color: var(--pb-muted); }
.builder-page .collection-rail .collection-card[style*="background-image"] .collection-hint {
  color: color-mix(in srgb, #fff 86%, transparent);
}

/* The eyebrow's leading mark, and the newsletter block's copy column. */
.builder-page .eyebrow .dot {
  display: inline-block; width: 0.4rem; height: 0.4rem; border-radius: 999px;
  margin-right: 0.5rem; vertical-align: middle; background: var(--pb-accent, currentColor);
}
.builder-page .newsletter-copy { display: grid; gap: 0.4rem; }


/* Muted copy inside a tone takes the tone's ink, and this has to out-specify the theme.
   Every theme sets `body.theme-<name> .lead { color: var(--color-ink-muted) }` at (0,2,1) — the
   *page's* muted ink — which on an inverted hero is dark grey on the dark band: 2.65:1 in light
   and 1.86:1 in dark, on the standfirst of the front page. The tone already computes
   `--pb-muted` as its own ink for exactly this reason; it just needed to win. (0,3,0). */
.builder-page .pb-block.pb-tone-soft .lead,
.builder-page .pb-block.pb-tone-raised .lead,
.builder-page .pb-block.pb-tone-accent .lead,
.builder-page .pb-block.pb-tone-inverted .lead,
.builder-page .pb-block.pb-tone-soft .muted,
.builder-page .pb-block.pb-tone-raised .muted,
.builder-page .pb-block.pb-tone-accent .muted,
.builder-page .pb-block.pb-tone-inverted .muted,
.builder-page .pb-block.pb-tone-soft .c-muted,
.builder-page .pb-block.pb-tone-raised .c-muted,
.builder-page .pb-block.pb-tone-accent .c-muted,
.builder-page .pb-block.pb-tone-inverted .c-muted {
  color: var(--pb-muted);
}
/* And the copy that carries no class at all. The newsletter block's body is a bare `<p>`, which
   a theme reaches with `body.theme-<name> .newsletter p` (0,2,2) — so the class list above,
   however long, could never have covered it. (0,3,1). */
.builder-page .pb-block.pb-tone-soft p,
.builder-page .pb-block.pb-tone-raised p,
.builder-page .pb-block.pb-tone-accent p,
.builder-page .pb-block.pb-tone-inverted p {
  color: var(--pb-muted);
}

/* ---- Carousel ---------------------------------------------------------------------------
   A scroll-snapping strip, not a stack of absolutely-positioned slides driven by a script: with
   no JavaScript this is still a usable, swipeable banner, and the dots are ordinary anchors to
   the slide ids. Nothing advances on its own — see the note in the template. */
/* `pb-bleed` makes a block full width by adding inset *padding*, which is right for a band of
   text and wrong for this: the slides are the picture, so they run edge to edge and it is
   `.pb-slide-body` that holds the reading width. Without this the banner sat in a 72rem box
   with the page showing either side of it. */
.builder-page .pb-carousel { position: relative; }
.builder-page .pb-carousel.pb-bleed { padding-inline: 0; }
.builder-page .pb-carousel-track {
  display: grid; grid-auto-flow: column; grid-auto-columns: 100%;
  overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth;
  scrollbar-width: none;
}
.builder-page .pb-carousel-track::-webkit-scrollbar { display: none; }
.builder-page .pb-carousel-track:focus-visible { outline: 2px solid var(--pb-accent, currentColor); outline-offset: -4px; }
/* The slide's own ground is the **ink**, not the soft tint. Two reasons, and the second is the
   one that bites: a merchant may not have uploaded a picture yet, and the words are `--pb-bg`,
   so on a near-white ground they are invisible; and the scrim above the picture is a *sibling*
   overlay rather than an ancestor, which means no contrast sweep that walks the ancestor chain
   can see it. Painting the floor dark makes the pair guaranteed whether or not there is an
   image and whether or not anything can measure the wash. */
.builder-page .pb-slide {
  position: relative; scroll-snap-align: start; display: grid; align-content: center;
  overflow: hidden; background: var(--pb-ink);
}
.builder-page .pb-carousel-band .pb-slide { min-height: clamp(14rem, 26vh, 20rem); }
.builder-page .pb-carousel-tall .pb-slide { min-height: clamp(22rem, 52vh, 34rem); }
.builder-page .pb-carousel-screen .pb-slide { min-height: min(88vh, 46rem); }
.builder-page .pb-slide-image { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* The wash under the words. A merchant's photograph has no contrast guarantee of its own, so
   the text sits on this rather than on the picture. Strength is `overlay`
   (none/light/medium/strong, `--pb-carousel-scrim`, a multiplier on the same three stops) — the
   `var(…, 1)` fallback is what keeps a carousel published before the setting existed at exactly
   today's fixed 82/62/34, since it never gained a `data-overlay` attribute to select on. This is
   the *photograph's* darkening; voyage's separate `header_overlay` only fades the strip behind
   the header's own words at the very top of it. */
.builder-page .pb-slide-scrim {
  position: absolute; inset: 0;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--pb-ink) calc(var(--pb-carousel-scrim, 1) * 82%), transparent) 0%,
    color-mix(in srgb, var(--pb-ink) calc(var(--pb-carousel-scrim, 1) * 62%), transparent) 52%,
    color-mix(in srgb, var(--pb-ink) calc(var(--pb-carousel-scrim, 1) * 34%), transparent) 100%);
}
.builder-page .pb-carousel[data-overlay="none"] { --pb-carousel-scrim: 0; }
.builder-page .pb-carousel[data-overlay="light"] { --pb-carousel-scrim: 0.55; }
.builder-page .pb-carousel[data-overlay="medium"] { --pb-carousel-scrim: 1; }
.builder-page .pb-carousel[data-overlay="strong"] { --pb-carousel-scrim: 1.15; }
.builder-page .pb-slide-body {
  position: relative; z-index: 2; display: grid; gap: 0.75rem; justify-items: start;
  width: min(var(--pb-shell, 72rem), calc(100% - 3rem));
  margin-inline: auto; padding: 2rem 0;
}
/* The slide paints its own ground, so its ink is the *opposite* of the page's — the scrim is
   built from `--pb-ink`, so the words have to be `--pb-bg` or they are ink on ink. */
.builder-page .pb-slide-eyebrow,
.builder-page .pb-slide-heading,
.builder-page .pb-slide-text { color: var(--pb-bg); }
.builder-page .pb-slide-eyebrow {
  font-size: 0.75rem; letter-spacing: 0.2em; text-transform: uppercase; font-weight: 700; margin: 0;
}
.builder-page .pb-slide-heading { font-size: clamp(1.8rem, 4.5vw, 3.4rem); margin: 0; max-width: 18ch; }
.builder-page .pb-slide-text { margin: 0; max-width: 46ch; }
.builder-page .pb-slide .btn {
  background: var(--pb-bg); color: var(--pb-ink); border-color: var(--pb-bg);
}
/* The slide follows the block's own `align`, rather than the carousel deciding for it — the
   same rule the themes had to learn: overriding `align` is overriding the merchant. */
.builder-page .pb-carousel.pb-align-center .pb-slide-body {
  justify-items: center; text-align: center;
}
.builder-page .pb-carousel.pb-align-center .pb-slide-heading,
.builder-page .pb-carousel.pb-align-center .pb-slide-text { margin-inline: auto; }
.builder-page .pb-carousel-dots {
  position: absolute; left: 0; right: 0; bottom: 1rem; z-index: 3;
  display: flex; gap: 0.5rem; justify-content: center;
}
.builder-page .pb-carousel-dot {
  width: 2.25rem; height: 0.28rem; border-radius: 999px;
  background: color-mix(in srgb, var(--pb-bg) 55%, transparent);
}
.builder-page .pb-carousel-dot:hover,
.builder-page .pb-carousel-dot:focus-visible { background: var(--pb-bg); }
/* The slide the strip is actually on. Nothing marked it before autoplay, and a row of identical
   dots is a position indicator that indicates no position. */
.builder-page .pb-carousel-dot[aria-current="true"] { background: var(--pb-bg); }

/* The pause control, added by `autoplay.js` and by nothing else — there is no markup for it in
   any template, on purpose. It sits over the picture on a carousel and over the band on a
   marquee, so it paints the same pair the words above it do rather than the page's: `--pb-bg` on
   a wash of `--pb-ink` is guaranteed by the palette in both modes, where a translucent white
   would be whatever the merchant's photograph happens to be. 44px is the touch target, and it is
   the whole point of the control — hover and `:focus-within` already served everybody else. */
.builder-page .pb-autoplay-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.75rem; height: 2.75rem; padding: 0; border: 0; border-radius: 999px;
  background: color-mix(in srgb, var(--pb-ink) 55%, transparent);
  color: var(--pb-bg); cursor: pointer;
}
.builder-page .pb-autoplay-toggle:hover,
.builder-page .pb-autoplay-toggle:focus-visible {
  background: color-mix(in srgb, var(--pb-ink) 78%, transparent);
}
.builder-page .pb-autoplay-toggle svg { width: 1.1rem; height: 1.1rem; fill: currentColor; }
/* On the carousel it is its own corner, clear of the dots row rather than nested inside it —
   nested there it inherited the row's centring and sat in the middle of the strip wearing the
   row's own padding, indistinguishable from a dot and in the wrong place either way. Its own
   position keeps it reachable and visible whether or not the carousel has dots to sit beside. */
.builder-page .pb-carousel > .pb-autoplay-toggle {
  position: absolute; z-index: 3; inset-block-end: 1rem; inset-inline-end: 1rem;
}
/* A tab strip has no picture under it, so there the control is ink on the page. */
.builder-page .pb-tabs .pb-autoplay-toggle {
  width: 2rem; height: 2rem; margin-inline-start: auto; margin-bottom: 0.35rem;
  background: transparent; color: var(--pb-muted);
}
.builder-page .pb-tabs .pb-autoplay-toggle:hover,
.builder-page .pb-tabs .pb-autoplay-toggle:focus-visible {
  background: var(--pb-soft); color: var(--pb-ink);
}
.builder-page .pb-tabs .pb-autoplay-toggle svg { width: 0.9rem; height: 0.9rem; }

/* ============================================================================================
   The six sections the reference themes had and this one did not.

   All of them read the palette through `--pb-*` and none names a colour literal, so they follow
   `color_scheme` and a theme restyles them from `body.theme-<name>` — commerce.css and this file
   are both linked *after* the theme stylesheet, so a theme's tweak needs its own scope to win.
   ============================================================================================ */

/* Marquee -----------------------------------------------------------------------------------
   The track holds the phrase list twice and travels exactly -50% of itself, so the second copy
   lands where the first began and the loop has no seam. `width: max-content` is what makes that
   50% mean "half the duplicated run" rather than half the viewport.

   It pauses on hover and on focus-within, and `prefers-reduced-motion` stops it dead — but the
   viewport keeps `overflow-x: auto`, so somebody who has asked for no animation can still read
   the whole band by scrolling it. Stopping the animation without that leaves the tail of the
   list unreachable, which is worse than the motion. */
.builder-page .pb-marquee { overflow: hidden; position: relative; }
.builder-page .pb-marquee-viewport { overflow-x: auto; scrollbar-width: none; }
.builder-page .pb-marquee-viewport::-webkit-scrollbar { display: none; }
.builder-page .pb-marquee-viewport:focus-visible { outline: 2px solid var(--pb-ink); outline-offset: -3px; }
.builder-page .pb-marquee-track {
  display: flex; width: max-content;
  animation: pb-marquee-scroll var(--pb-marquee-duration, 32s) linear infinite;
}
.builder-page .pb-marquee-slow { --pb-marquee-duration: 52s; }
.builder-page .pb-marquee-medium { --pb-marquee-duration: 32s; }
.builder-page .pb-marquee-fast { --pb-marquee-duration: 18s; }
.builder-page .pb-marquee-viewport:hover .pb-marquee-track,
.builder-page .pb-marquee-viewport:focus-within .pb-marquee-track { animation-play-state: paused; }
/* The explicit stop. `autoplay.js` sets the class; the CSS hover rule above is the same
   behaviour for a pointer, which is why this is a second rule rather than a replacement — the
   band still holds still under a mouse on a page whose script never arrived. */
.builder-page .pb-marquee.is-paused .pb-marquee-track { animation-play-state: paused; }
/* Over the band's inline end, clear of the words and inside the clipped box. */
.builder-page .pb-marquee .pb-autoplay-toggle {
  position: absolute; inset-block: 0; inset-inline-end: 0.5rem; margin-block: auto;
  background: color-mix(in srgb, var(--pb-bg) 78%, transparent); color: var(--pb-ink);
}
.builder-page .pb-marquee .pb-autoplay-toggle:hover,
.builder-page .pb-marquee .pb-autoplay-toggle:focus-visible { background: var(--pb-bg); }
.builder-page .pb-marquee-run {
  display: flex; align-items: center; gap: var(--pb-marquee-gap, 2.5rem);
  list-style: none; margin: 0; padding: 0 var(--pb-marquee-gap, 2.5rem) 0 0;
}
.builder-page .pb-marquee-item { display: flex; align-items: center; gap: var(--pb-marquee-gap, 2.5rem); white-space: nowrap; }
.builder-page .pb-marquee-sep { opacity: 0.55; }
.builder-page .pb-marquee-small .pb-marquee-track { font-size: 0.8rem; letter-spacing: 0.18em; text-transform: uppercase; padding-block: 0.75rem; }
.builder-page .pb-marquee-medium .pb-marquee-track { font-size: clamp(1.1rem, 2.2vw, 1.6rem); padding-block: 1.1rem; }
.builder-page .pb-marquee-large .pb-marquee-track { font-size: clamp(1.6rem, 4.5vw, 3rem); padding-block: 1.4rem; }
@keyframes pb-marquee-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  .builder-page .pb-marquee-track { animation: none; }
}

/* Lookbook ----------------------------------------------------------------------------------
   `grid-auto-flow: dense` so a wide tile that will not fit the remaining columns does not leave
   a hole behind it — a merchant reordering pictures should not have to think about packing. */
.builder-page .pb-lookbook-grid {
  display: grid; gap: var(--pb-look-gap, 0.9rem); grid-auto-flow: dense;
  grid-auto-rows: var(--pb-look-row, clamp(7rem, 14vw, 11rem));
}
.builder-page .pb-look { position: relative; margin: 0; overflow: hidden; border-radius: var(--pb-radius); background: var(--pb-soft); }
.builder-page .pb-look-wide { grid-column: span 2; }
.builder-page .pb-look-tall { grid-row: span 2; }
.builder-page .pb-look-large { grid-column: span 2; grid-row: span 2; }
.builder-page .pb-look-link { display: block; position: relative; width: 100%; height: 100%; color: inherit; text-decoration: none; }
.builder-page .pb-look-image { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 600ms cubic-bezier(0.22, 0.61, 0.36, 1); }
/* The caption is drawn over a photograph the merchant chose, so its colours are not the palette's:
   `--pb-bg` on a wash of `--pb-ink` is dark type on a light wash in a dark palette, which is the
   unreadable caption that was reported. White on black, as the collection cards above do, with a
   shadow for the bright patch of a photograph the wash does not reach. The wash belongs to the
   caption's own box so it is exactly as tall as the words, however many lines they run to. */
.builder-page .pb-look-caption {
  position: absolute; inset-inline: 0; bottom: 0; margin: 0; padding: 1.7rem 1rem 0.85rem;
  font-size: 0.8rem; line-height: 1.3; letter-spacing: 0.06em; color: #fff;
  text-wrap: balance; text-shadow: 0 1px 2px rgb(0 0 0 / 0.6);
  /* Solid behind every line and fading only in the padding above the first: a wash that thins
     towards the top puts the first line of a long caption on the thinnest part of it. */
  background: linear-gradient(to top, rgb(0 0 0 / 0.86) calc(100% - 1.5rem), transparent 100%);
}
/* A tile with no picture keeps its caption, so it reads as a quiet cell rather than a fault. */
.builder-page .pb-look:not(:has(.pb-look-image)) .pb-look-caption {
  position: static; background: none; color: var(--pb-muted); padding: 1rem; text-shadow: none;
}
/* Which tiles go somewhere. A hover is no use on a phone, so a linked tile always carries a small
   arrow in its corner; an unlinked one carries nothing, and the difference is the message. */
.builder-page .pb-look-link::before {
  content: ""; position: absolute; z-index: 2; top: 0.6rem; inset-inline-end: 0.6rem;
  width: 1.9rem; height: 1.9rem; border-radius: 999px;
  background: rgb(0 0 0 / 0.66) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 17 17 7M8 7h9v9'/%3E%3C/svg%3E") center / 62% no-repeat;
  box-shadow: 0 0 0 1px rgb(255 255 255 / 0.55);
  transition: background-color 200ms ease;
}
.builder-page .pb-look-link:hover .pb-look-caption,
.builder-page .pb-look-link:focus-visible .pb-look-caption { text-decoration: underline; text-underline-offset: 0.25em; }
.builder-page .pb-look-link:hover::before, .builder-page .pb-look-link:focus-visible::before { background-color: rgb(0 0 0 / 0.9); }
.builder-page .pb-look-link:focus-visible { outline: 3px solid var(--pb-ink); outline-offset: -3px; }
.builder-page .pb-look-link:focus-visible::after {
  content: ""; position: absolute; inset: 3px; z-index: 3; pointer-events: none; border-radius: inherit;
  box-shadow: inset 0 0 0 2px #fff;
}
@media (hover: hover) { .builder-page .pb-look-link:hover .pb-look-image { transform: scale(var(--hover-zoom, 1.045)); } }
@media (prefers-reduced-motion: reduce) { .builder-page .pb-look-image { transition: none; } }
@media (max-width: 700px) {
  /* Taller rows: a two-line caption on a 7rem tile covered most of the picture. */
  .builder-page .pb-lookbook-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); --pb-look-row: clamp(9rem, 44vw, 12rem); }
  .builder-page .pb-look-wide, .builder-page .pb-look-large { grid-column: span 2; }
}

/* Picture with pop-ups ---------------------------------------------------------------------
   Two states, and the first is the whole block. As the server writes it: the picture with a
   numbered badge at each point (`--x`/`--y`, percentages of the picture) and a numbered list
   under it. `static/js/hotspots.js` then makes each badge a button with its pop-up straight
   after it and removes the list, so nothing below `.pb-hotspot-pop` exists on a page the script
   never reached.

   - The frame wraps the picture and nothing else, so a percentage is a percentage of the
     picture. A badge is centred on its point and held inside the edge: a point at 0 or 100 would
     otherwise hang half off the picture, and off a phone.
   - A badge and a pop-up paint their own ground, so each restates the pair for itself: on an
     accent or inverted band `--pb-ink` is the band's ink, which is the band's colour on a card.
     The badge is the picture's, not the band's: ink on surface with a ring of the other, so it
     reads on a dark photograph and a pale one alike.
   - 2.75rem is the badge's whole target (the 44px a thumb needs); the disc drawn is smaller. */
.builder-page .pb-hotspots-frame { position: relative; margin-top: 1rem; }
.builder-page .pb-hotspots > .pb-hotspots-frame:first-child { margin-top: 0; }
.builder-page .pb-hotspots-image { display: block; width: 100%; height: auto; border-radius: var(--pb-radius); }
.builder-page .pb-hotspot {
  --pb-hot-size: 2.75rem;
  position: absolute; z-index: 1;
  left: clamp(calc(var(--pb-hot-size) / 2), var(--x, 50%), calc(100% - var(--pb-hot-size) / 2));
  top: clamp(calc(var(--pb-hot-size) / 2), var(--y, 50%), calc(100% - var(--pb-hot-size) / 2));
  width: var(--pb-hot-size); height: var(--pb-hot-size); margin: calc(var(--pb-hot-size) / -2) 0 0 calc(var(--pb-hot-size) / -2);
  display: grid; place-items: center; padding: 0; border: 0; background: none;
  font: inherit; font-size: 0.85rem; font-weight: 700; line-height: 1; color: var(--color-surface, #fff);
  appearance: none; cursor: default;
}
.builder-page button.pb-hotspot { cursor: pointer; }
.builder-page .pb-hotspot::before {
  content: ""; position: absolute; inset: 0.4rem; z-index: -1; border-radius: 50%;
  background: var(--color-ink, #0f172a); border: 2px solid var(--color-surface, #fff);
  box-shadow: 0 1px 6px rgb(0 0 0 / 0.35);
  transition: transform 160ms ease;
}
.builder-page button.pb-hotspot:hover::before,
.builder-page button.pb-hotspot.is-open::before { transform: scale(1.15); }
.builder-page button.pb-hotspot.is-open { z-index: 3; }
.builder-page button.pb-hotspot:focus-visible { outline: 3px solid var(--color-ink, #0f172a); outline-offset: -0.2rem; border-radius: 50%; box-shadow: 0 0 0 0.35rem var(--color-surface, #fff) inset; }
@media (prefers-reduced-motion: reduce) { .builder-page .pb-hotspot::before { transition: none; } }

/* What a number is: the list without the script, the pop-up with it. One set of rules for the
   words, because they are the same words. */
.builder-page .pb-hotspots-list { margin: 1rem 0 0; padding-inline-start: 1.5rem; display: grid; gap: 0.75rem; }
.builder-page .pb-hotspots-list > li::marker { font-weight: 700; }
body .builder-page :is(.pb-hotspot-note, .pb-hotspot-pop) p { margin: 0; }
body .builder-page :is(.pb-hotspot-note, .pb-hotspot-pop) p + p { margin-top: 0.35rem; }
.builder-page .pb-hotspot-title { font-weight: 700; color: var(--pb-ink); }
.builder-page .pb-hotspot-text { color: var(--pb-muted); }
.builder-page .pb-hotspot-product { display: flex; align-items: center; gap: 0.75rem; color: inherit; text-decoration: none; }
.builder-page .pb-hotspot-product:hover .pb-hotspot-title,
.builder-page .pb-hotspot-product:focus-visible .pb-hotspot-title { text-decoration: underline; text-underline-offset: 0.2em; }
.builder-page .pb-hotspot-thumb { flex: none; width: 4.5rem; height: 4.5rem; object-fit: cover; border-radius: calc(var(--pb-radius) / 2); background: var(--pb-soft); }
.builder-page .pb-hotspot-words { display: grid; gap: 0.2rem; min-width: 0; }

.builder-page .pb-hotspot-pop {
  --pb-bg: var(--color-surface-raised, var(--color-surface, #fff));
  --pb-ink: var(--color-ink, #0f172a);
  --pb-muted: var(--color-ink-muted, color-mix(in srgb, var(--pb-ink) 65%, var(--pb-bg)));
  --pb-soft: color-mix(in srgb, var(--pb-ink) 6%, var(--pb-bg));
  --pb-line: color-mix(in srgb, var(--pb-ink) 15%, transparent);
  position: absolute; z-index: 4;
  left: var(--x, 50%); top: calc(var(--y, 50%) + 1.25rem);
  width: max-content; max-width: min(20rem, calc(100vw - 1rem));
  transform: translateX(calc(-50% + var(--shift, 0px)));
  padding: 0.85rem 1rem; text-align: start;
  background: var(--pb-bg); color: var(--pb-ink);
  border: 1px solid var(--pb-line); border-radius: var(--pb-radius);
  box-shadow: 0 10px 30px rgb(0 0 0 / 0.18);
}
.builder-page .pb-hotspot-pop.is-above { top: auto; bottom: calc(100% - var(--y, 50%) + 1.25rem); }
/* `hidden` loses to any `display`, and a pop-up that is closed has to be closed. */
.builder-page .pb-hotspot-pop[hidden] { display: none; }
.builder-page .pb-block .pb-hotspot-pop a:not(.btn) { color: var(--pb-ink); }
.builder-page .pb-block .pb-hotspot-pop .price-compare { color: var(--pb-muted); }
@media (max-width: 40rem) {
  /* No layer on a phone: the open one is drawn under the picture, full width, and the picture
     leaves the room it measures (`--pb-hot-room`, set by the script) so nothing is covered. */
  .builder-page .pb-hotspots-frame { margin-bottom: calc(var(--pb-hot-room, 0px) + 0.75rem); }
  .builder-page .pb-hotspot-pop,
  .builder-page .pb-hotspot-pop.is-above {
    left: 0; right: 0; top: calc(100% + 0.75rem); bottom: auto;
    width: auto; max-width: none; transform: none; box-shadow: none;
  }
}
/* The script hides the list once every entry has its pop-up. Paper has no pop-ups, so there the
   list comes back and whichever pop-up was open goes. */
.builder-page .pb-hotspots-list[hidden] { display: none; }
@media print {
  .builder-page .pb-hotspots-list[hidden] { display: grid; }
  .builder-page .pb-hotspots .pb-hotspot-pop { display: none; }
  .builder-page .pb-hotspots-frame { margin-bottom: 0; }
}

/* Category filter ---------------------------------------------------------------------------
   The row `static/js/tile_filter.js` draws above a gallery or a lookbook. Nothing here exists
   in the markup, so none of it can be missing from a page the script never reached. The pressed
   button is the palette's ink on its own ground — a pair the palettes guarantee — rather than a
   colour anybody typed. `[hidden]` is restated because a picture's own `display` (a grid item,
   a figure a theme restyles) would otherwise beat the user-agent rule that hides it. */
.builder-page .pb-filter { display: flex; flex-wrap: wrap; gap: 0.5rem 0.6rem; margin-bottom: clamp(1rem, 2.5vw, 1.75rem); }
.builder-page .pb-filter-chip {
  font: inherit; font-size: 0.78rem; letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer;
  padding: 0.5rem 1rem; border-radius: 999px; border: 1px solid var(--pb-line);
  background: transparent; color: var(--pb-ink);
}
.builder-page .pb-filter-chip:hover { border-color: var(--pb-ink); }
.builder-page .pb-filter-chip[aria-pressed="true"] { background: var(--pb-ink); color: var(--pb-bg); border-color: var(--pb-ink); }
.builder-page .pb-filter-chip:focus-visible { outline: 2px solid var(--pb-ink); outline-offset: 2px; }
.builder-page [data-pb-item][hidden] { display: none; }

/* Category chips ----------------------------------------------------------------------------
   A rail, scrolled by the native gesture with scroll-snap — no script and no arrows, the
   carousel's trade. The label sits outside the picture so it is never ink-on-photograph. */
/* `margin-inline` and `margin-bottom`, never the `margin` shorthand: at (0,2,0) and later in the
   file, `margin: 0` quietly cancelled the block's own `> * + *` spacing, and "Where we go" sat
   directly on its row of pictures on every theme that seeds this block. */
.builder-page .pb-chip-rail {
  display: flex; gap: clamp(1.1rem, 2.5vw, 2.1rem); list-style: none; margin-inline: 0; margin-bottom: 0; padding: 0 0 0.5rem;
  overflow-x: auto; scroll-snap-type: x proximity; scrollbar-width: none;
}
.builder-page .pb-chip-rail::-webkit-scrollbar { display: none; }
.builder-page .pb-chip-rail > li { scroll-snap-align: start; flex: 0 0 auto; }
.builder-page .pb-chip {
  display: flex; flex-direction: column; align-items: center; gap: 0.7rem;
  text-decoration: none; color: var(--pb-ink); width: var(--pb-chip-size, 7rem);
}
.builder-page .pb-chip-media {
  display: block; width: var(--pb-chip-size, 7rem); height: var(--pb-chip-size, 7rem);
  overflow: hidden; background: var(--pb-soft);
}
.builder-page .pb-chips-round .pb-chip-media { border-radius: 999px; }
.builder-page .pb-chips-square .pb-chip-media { border-radius: var(--pb-radius); }
.builder-page .pb-chip-media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 620ms cubic-bezier(0.22, 0.61, 0.36, 1); }
@media (hover: hover) {
  .builder-page .pb-chip:hover .pb-chip-media img { transform: scale(var(--hover-zoom, 1.045)); }
}
@media (prefers-reduced-motion: reduce) {
  .builder-page .pb-chip-media img { transition: none; }
  .builder-page .pb-chip:hover .pb-chip-media img { transform: none; }
}
.builder-page .pb-chip-label { font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; text-align: center; }
/* The text-only shape, which is also the fallback when no category has a picture. */
.builder-page .pb-chips-pill .pb-chip {
  width: auto; flex-direction: row; border: 1px solid var(--pb-line); border-radius: 999px;
  padding: 0.7rem 1.3rem; min-height: 2.75rem; justify-content: center;
}
.builder-page .pb-chips-pill .pb-chip-label { text-align: left; }
/* **Pills wrap; only pictures scroll.** The rail's scrollbar is hidden, so on a phone the pills
   past the edge were not merely out of reach but unannounced: nothing on the screen said there
   was a sixth way in (reported on the live voyage shop). A pill is a word in a border and
   wraps as tags do, so every one is on the page. A row of pictures stays a swipeable rail,
   because six 7rem circles wrapped onto a phone would be a wall of them. `justify-content`
   follows the block's own alignment, so a centred block centres each wrapped line. */
.builder-page .pb-chips-pill .pb-chip-rail {
  flex-wrap: wrap; overflow-x: visible; scroll-snap-type: none;
  gap: 0.6rem; padding-bottom: 0;
}
.builder-page .pb-chips-pill.pb-align-center .pb-chip-rail { justify-content: center; }
.builder-page .pb-chips-pill .pb-chip-rail > li { flex: 0 1 auto; max-width: 100%; }

/* Collection tabs ---------------------------------------------------------------------------
   With no script every panel is visible and its own `<h3>` says which collection it is; the
   strip above is a row of in-page anchors. `tabs.js` then hides the inactive panels, hides the
   headings and adds the tablist ARIA — the markup promises no widget until it exists. */
.builder-page .pb-tab-strip { display: flex; gap: clamp(1rem, 3vw, 2rem); flex-wrap: wrap; border-bottom: 1px solid var(--pb-line); }
.builder-page .pb-tab {
  padding: 0 0 0.75rem; margin-bottom: -1px; text-decoration: none; color: var(--pb-muted);
  border-bottom: 2px solid transparent; font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase;
}
.builder-page .pb-tab[aria-selected="true"], .builder-page .pb-tab.is-current { color: var(--pb-ink); border-bottom-color: var(--pb-ink); }
.builder-page .pb-tab:hover { color: var(--pb-ink); }
.builder-page .pb-tab-panel[hidden] { display: none; }
.builder-page .pb-tab-title { margin: 1.5rem 0 1rem; }
.builder-page .pb-tabs[data-pb-tabs-ready] .pb-tab-title { display: none; }
/* The way to the rest of a tab's products, at the foot of its panel and on the trailing side:
   where a reader's eye leaves a grid it has just read across. A paragraph of its own rather
   than a flex child of the grid, so it cannot become a ninth card. `margin` is stated because
   every theme resets `p` to none. */
.builder-page .pb-tab-more { margin: 1.25rem 0 0; text-align: end; }
.builder-page .pb-tab-more .text-link { display: inline-flex; align-items: center; gap: 0.35rem; font-weight: 600; }

/* Date search --------------------------------------------------------------------------------
   The words around the form; the form itself is `commerce.css`'s, because `/whats-on/` renders
   the same partial and two owners for one control is how the search box came to be drawn as a
   box inside a box. What this adds is the section's own rhythm and the tone-awareness the rest
   of the blocks have — inside an inverted band the panel takes that band's pair, not the
   page's, which is the `--pb-*` restating every tone does. */
.builder-page .pb-date-search-head { margin-bottom: 1rem; }
.builder-page .pb-date-search-head h2 { margin: 0 0 0.35rem; }
.builder-page .pb-date-search .date-search {
  background: var(--pb-bg); border-color: var(--pb-line);
}
.builder-page .pb-date-search .date-search-field label { color: var(--pb-muted); }
.builder-page .pb-date-search .date-search-field input {
  background: var(--pb-bg); color: var(--pb-ink); border-color: var(--pb-line);
}
.builder-page .pb-date-search .date-search-field input::placeholder { color: var(--pb-muted); }

/* Video with text ---------------------------------------------------------------------------
   The still is a first-party image and the play control is a real link; the frame is swapped in
   on the click, and only where consent allows an embed at all. */
.builder-page .pb-video-text-inner { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
/* With no still and no video there is no media column, so the words must not be left sitting
   against an empty track. */
.builder-page .pb-video-text-inner.pb-no-media,
.builder-page .pb-media-right .pb-video-text-inner.pb-no-media { grid-template-columns: minmax(0, 1fr); }
.builder-page .pb-media-right .pb-video-text-inner { grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); }
.builder-page .pb-media-right .pb-video-text-media { order: 2; }
.builder-page .pb-video-still { position: relative; aspect-ratio: 16 / 9; overflow: hidden; border-radius: var(--pb-radius); background: var(--pb-soft); }
.builder-page .pb-video-poster { width: 100%; height: 100%; object-fit: cover; display: block; }
.builder-page .pb-video-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top, color-mix(in srgb, var(--pb-ink) 45%, transparent), color-mix(in srgb, var(--pb-ink) 8%, transparent));
}
.builder-page .pb-video-play {
  position: absolute; inset: 0; display: grid; place-items: center; text-decoration: none; color: var(--pb-bg);
}
.builder-page .pb-video-play-mark {
  width: 4.5rem; height: 4.5rem; border-radius: 999px; display: grid; place-items: center;
  border: 1px solid color-mix(in srgb, var(--pb-bg) 80%, transparent);
  background: color-mix(in srgb, var(--pb-ink) 35%, transparent);
  transition: transform 240ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.builder-page .pb-video-play-mark svg { width: 1.35rem; height: 1.35rem; }
@media (hover: hover) { .builder-page .pb-video-play:hover .pb-video-play-mark { transform: scale(1.08); } }
@media (prefers-reduced-motion: reduce) { .builder-page .pb-video-play-mark { transition: none; } }
.builder-page .pb-video-duration {
  position: absolute; left: 1rem; bottom: 0.9rem; margin: 0; color: var(--pb-bg);
  font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase;
}
.builder-page .pb-video-frame-live { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.builder-page .pb-video-text-body { display: grid; gap: 0.9rem; align-content: start; }

/* Split banner ------------------------------------------------------------------------------
   The whole panel is the link. A tone is a *pair* from the palette, never a typed colour. */
.builder-page .pb-split-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); gap: var(--pb-split-gap, 0.9rem); }
.builder-page .pb-split-even .pb-split-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* Three even panels are three columns, not two and one left on a row of its own. */
.builder-page .pb-split-even.pb-split-count-3 .pb-split-row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.builder-page .pb-split-wide-left .pb-split-row { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
.builder-page .pb-split-wide-right .pb-split-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); }
.builder-page .pb-split-panel {
  position: relative; display: block; overflow: hidden; border-radius: var(--pb-radius);
  text-decoration: none; color: var(--pb-bg); background: var(--pb-soft);
}
.builder-page .pb-split-landscape .pb-split-panel { aspect-ratio: 4 / 3; }
.builder-page .pb-split-square .pb-split-panel { aspect-ratio: 1 / 1; }
.builder-page .pb-split-band .pb-split-panel { aspect-ratio: 21 / 9; }
.builder-page .pb-split-image { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 700ms cubic-bezier(0.22, 0.61, 0.36, 1); }
@media (hover: hover) { .builder-page .pb-split-panel:hover .pb-split-image { transform: scale(var(--hover-zoom, 1.045)); } }
@media (prefers-reduced-motion: reduce) { .builder-page .pb-split-image { transition: none; } }
.builder-page .pb-split-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top, color-mix(in srgb, var(--pb-ink) 68%, transparent) 0%, color-mix(in srgb, var(--pb-ink) 12%, transparent) 62%, transparent 100%);
}
.builder-page .pb-split-body { position: absolute; inset-inline: clamp(1.1rem, 3vw, 1.9rem); bottom: clamp(1.1rem, 3vw, 1.7rem); display: grid; gap: 0.55rem; justify-items: start; }
/* No `opacity` here, and that is the point. It read 0.85, which on petal's accent panel
   composited white to 4.34:1 — under AA for text this small. A tone hands out a *pair* the
   palette guarantees; dimming one half of it produces a contrast figure nobody has measured,
   which is the same mistake as a 65% "muted" blend inside a toned section and how `--c-warn`
   reached 1.51:1. An eyebrow is already set apart by size, tracking and case. */
.builder-page .pb-split-eyebrow { margin: 0; font-size: 0.68rem; letter-spacing: 0.24em; text-transform: uppercase; }
.builder-page .pb-split-heading { margin: 0; }
.builder-page .pb-split-cta { font-size: 0.7rem; letter-spacing: 0.18em; text-transform: uppercase; border-bottom: 1px solid currentColor; padding-bottom: 2px; }
/* A panel with no picture: a ground and the ink that belongs on it, both from the palette. */
.builder-page .pb-split-tone-soft { background: var(--pb-soft); color: var(--pb-ink); }
.builder-page .pb-split-tone-accent { background: var(--color-accent); color: var(--color-accent-ink); }
.builder-page .pb-split-tone-inverted { background: var(--pb-ink); color: var(--pb-bg); }
.builder-page .pb-split-panel:not(:has(.pb-split-image)) .pb-split-body { position: static; padding: clamp(1.4rem, 4vw, 2.2rem); height: 100%; align-content: end; }

@media (max-width: 760px) {
  .builder-page .pb-video-text-inner,
  .builder-page .pb-media-right .pb-video-text-inner { grid-template-columns: 1fr; }
  .builder-page .pb-media-right .pb-video-text-media { order: 0; }
  .builder-page .pb-split-even .pb-split-row,
  .builder-page .pb-split-even.pb-split-count-3 .pb-split-row,
  .builder-page .pb-split-wide-left .pb-split-row,
  .builder-page .pb-split-wide-right .pb-split-row { grid-template-columns: 1fr; }
}

/* A footer built in the page builder ------------------------------------------------------ *
 * `{% built_footer %}` draws these blocks inside every theme's own `<footer class="site-footer">`
 * in place of its upper half. Three things are decided here rather than per theme:
 *
 * - **The ink is the footer's own.** depot, voyage and encore paint the footer a band opposite to
 *   the page, so the builder's usual page ink would be dark on dark. Every `--pb-*` token is
 *   re-pointed at `currentColor`, which is whatever the theme set on `.site-footer` (a footer
 *   that sets none inherits the page's, over a ground the page already reads on). "Muted" is the
 *   same ink, not a dimmed mix: the palettes guarantee the pair they gave us, not 65% of it.
 * - **The ground is the footer's**, so `--pb-bg` is transparent and a block draws nothing behind
 *   itself. A block that paints its own tone still does, with its own pair.
 * - **The legal links are the theme's and are always drawn** (`footer-legal`), so a merchant
 *   cannot build a footer without them.
 */
body .site-footer .site-footer-built {
  --pb-ink: currentColor;
  --pb-muted: currentColor;
  --pb-bg: transparent;
  --pb-line: color-mix(in srgb, currentColor 22%, transparent);
  --pb-soft: color-mix(in srgb, currentColor 8%, transparent);
  color: inherit;
}
body .site-footer .site-footer-built :is(h1, h2, h3, h4, p, li, a) { color: inherit; }
/* The brand is plain here. studio and tally paint the name on an accent chip, and `inherit` above
   would put the footer's ink on it (2.9:1 in dark), so the chip is dropped and the name or logo
   sits on the footer's own ground; the logo's variant follows `brand_ground`, which the theme's
   footer passes to `{% built_footer %}`. */
body .site-footer .site-footer-built .site-brand {
  background: transparent; border: 0; padding: 0; margin: 0; color: inherit;
}
body .site-footer .site-footer-built .site-brand::before { display: none; }
/* The legal links keep the theme's own footer-link style (`.footer-col a`: size, ink, hover), so
   they read as they do on a footer nobody built. Only the layout is ours: the theme's column is a
   grid, which would stack six links under a band that has no columns left to put them in. */
body .site-footer .footer-legal { padding-block: 1rem; }
body .site-footer .footer-legal .footer-col {
  display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; align-content: start;
}

/* The blocks a footer is made of ---------------------------------------------------------- */
.builder-page .pb-column-image { display: block; width: 5rem; max-width: 100%; height: auto; }
.builder-page .pb-align-center .pb-column-image { margin-inline: auto; }
/* A column's picture at the size the block asks for. Small is the icon it began as; the other
   two are for a logo with a name in it, which is wide and short, so the height is capped too and
   `object-fit: contain` keeps a tall mark from being stretched to the width. `object-position`
   follows the block's alignment, or a contained logo would sit in the middle of a left-set card. */
.builder-page .pb-picture-medium .pb-column-image,
.builder-page .pb-picture-large .pb-column-image {
  object-fit: contain; object-position: left center;
}
.builder-page .pb-picture-medium .pb-column-image { width: 9rem; height: 3.5rem; }
.builder-page .pb-picture-large .pb-column-image { width: 14rem; height: 5.5rem; }
.builder-page .pb-align-center :is(.pb-picture-medium, .pb-picture-large) .pb-column-image { object-position: center; }
.builder-page .pb-align-right :is(.pb-picture-medium, .pb-picture-large) .pb-column-image { object-position: right center; }
.builder-page .pb-align-right .pb-column-image { margin-inline-start: auto; }
.builder-page .pb-link-column { display: grid; gap: 0.6rem; align-content: start; }
.builder-page .pb-link-column h3 { margin: 0; }
.builder-page .pb-link-column ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.45rem; }
.builder-page .pb-site-brand { display: grid; gap: 0.75rem; justify-items: start; }
.builder-page .pb-site-brand.pb-align-center { justify-items: center; }
.builder-page .pb-site-tagline { max-width: 32rem; color: var(--pb-muted); }

/* The Contact block's layouts ----------------------------------------------------------------
   `classic` is the original two columns and is styled with the featured/contact rules further up.
   The others are arranged here, at the end of the file on purpose: the band restates the inverted
   pair at the same specificity as a tone (0,3,0) and has to come after the tones to win, and its
   padding has to lose to `.pb-bleed`'s, so it is written `:not(.pb-bleed)`.

   Written against the colour roles and the block's `--pb-*` tokens only — no hex, no white or
   black — so each one follows a theme's light and dark palettes and any background the merchant
   picks. A surface that paints its own ground (the form card, the wide card) restates the whole
   ink/muted/line pair for its subtree, because a custom property resolves where it is declared.
   The fields are repainted from the pair they stand on; left alone they keep the theme's own
   `.input` colours, which is how studio's newsletter came out black on black.

   **The form is a grid, so every field in a row starts on the same line.** `--cf-cols` columns;
   a field is one cell, and the message, address questions, notices and the product note take a
   whole row. Flex rows with a different `flex-grow` per field (what the mockups did) leave the
   second row's boxes out of step with the first. */
/* **Once a background is chosen, its pair decides every part of the form** — the same rule the
   newsletter follows above. A tone repaints the band; a theme's labels are written in the ink it
   paints its own ground with, which on an accent or inverted band measured 1.2–2.7:1 (classic,
   photo) once the sweep rendered the form on screen instead of in a closed dialog. Inside the
   split and wide cards `--pb-ink` is restated on the card, so this reads the card's ink there. */
.builder-page .pb-contact[class*="pb-tone-"] .contact-form :is(label, legend, .field-optional, .hint) {
  color: var(--pb-ink);
}
/* A field is a box with its own ground, so it keeps the page's surface pair whatever band it is
   on: the box's text inherited the band's ink (light on an accent band) while the box itself was
   still the theme's light ground. */
.builder-page .pb-contact[class*="pb-tone-"] .contact-form :is(input:not([type="checkbox"]), textarea, select).input {
  background: var(--color-surface, #fff); color: var(--color-ink, #0f172a);
}

.builder-page .pb-contact { --cf-cols: 2; --cf-half: 1; }
.builder-page .pb-contact-layout-wide { --cf-cols: 4; --cf-half: 2; }
.builder-page .pb-contact-layout-classic { /* the original arrangement: nothing to add */ }

.builder-page .pb-contact:not(.pb-contact-layout-classic) > * { margin: 0; min-width: 0; }
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form {
  display: grid;
  grid-template-columns: repeat(var(--cf-cols), minmax(0, 1fr));
  gap: 1.1rem 1rem;
  /* `end`, so the boxes in a row are level whatever their labels do: a label that wraps to two
     lines ("Company or organisation (optional)" in a theme that sets labels in small capitals)
     makes its field taller, and top-aligned that pushed its box below its neighbour's. */
  align-items: end;
}
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form > * { grid-column: 1 / -1; min-width: 0; margin: 0; }
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form > .field:not(:has(textarea)):not(.address-question):not(:has(.phone-field)),
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form > .field-check { grid-column: auto; }
/* A phone question is a country picker and a number side by side, which wants about twenty-four
   rem: a whole row where the form has two columns, two columns where it has four. In one narrow
   cell the two boxes stacked, and that field was twice as tall as every neighbour. */
.builder-page .pb-contact-layout-wide .contact-form > .field:has(.phone-field) { grid-column: span 2; }
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form > .field-check { grid-column: span var(--cf-half); align-self: center; }
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form > .btn {
  grid-column: span var(--cf-half); justify-self: end; align-self: center;
}
/* One height for every single-line box. `min-height` left a browser's date box 2px taller than a
   number box, so two fields in a row ended a hair apart whatever the theme did to `.input`. */
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form :is(input:not([type="checkbox"]):not([type="hidden"]), select).input {
  height: 3rem; padding-block: 0; box-sizing: border-box;
}
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form .phone-field :is(select, input, .phone-cc-button) {
  height: 3rem; padding-block: 0; box-sizing: border-box;
}
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form textarea.input { min-height: 8rem; }

/* The words beside or above the form. */
.builder-page .pb-contact:not(.pb-contact-layout-classic) .eyebrow {
  margin: 0 0 0.25rem; font-size: 0.72rem; letter-spacing: 0.24em; text-transform: uppercase;
  font-weight: 600; color: var(--pb-muted);
}
.builder-page .pb-contact:not(.pb-contact-layout-classic) .pb-contact-copy { display: grid; gap: 0.9rem; align-content: start; }
.builder-page .pb-contact:not(.pb-contact-layout-classic) .pb-contact-copy > * { margin: 0; }
.builder-page .pb-contact:not(.pb-contact-layout-classic) .pb-contact-details { margin: 0; gap: 1rem; }
.builder-page .pb-contact-detail { display: flex; align-items: center; gap: 0.75rem; }
.builder-page .pb-contact-detail > span { display: grid; gap: 0.1rem; }
.builder-page .pb-contact-detail-label { font-size: 0.8rem; color: var(--pb-muted); }
.builder-page .pb-contact-icon { flex: none; width: 1.4rem; height: 1.4rem; }
.builder-page .pb-contact:not(.pb-tone-accent):not(.pb-tone-inverted):not(.pb-contact-layout-band) .pb-contact-icon { color: var(--color-accent, currentColor); }
.builder-page .pb-contact-ticks, .builder-page .pb-contact-points { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.85rem; }
.builder-page .pb-contact-ticks li, .builder-page .pb-contact-points li { display: flex; align-items: flex-start; gap: 0.75rem; }
.builder-page .pb-contact-ticks .pb-contact-icon, .builder-page .pb-contact-points .pb-contact-icon { margin-top: 0.1rem; }
.builder-page .pb-contact-point-text { color: var(--pb-muted); }
.builder-page .pb-contact-points li > span { display: grid; gap: 0.2rem; }

/* Pitch and form card: the words on the left, the form in a card on the right. */
.builder-page .pb-contact-layout-split {
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: clamp(2rem, 5vw, 4rem); align-items: start;
}
.builder-page .pb-contact-layout-split .pb-contact-form,
.builder-page .pb-contact-layout-wide .pb-contact-form {
  --pb-bg: var(--color-surface, #fff);
  --pb-ink: var(--color-ink, #0f172a);
  --pb-muted: var(--color-ink-muted, var(--color-ink, #0f172a));
  --pb-line: color-mix(in srgb, var(--pb-ink) 18%, transparent);
  background: var(--pb-bg); color: var(--pb-ink);
  border: 1px solid var(--color-border, var(--pb-line)); border-radius: var(--pb-radius);
  padding: clamp(1.25rem, 3vw, 2.25rem);
}
.builder-page .pb-contact-layout-wide .pb-contact-form { --pb-bg: var(--color-surface-raised, var(--color-surface, #fff)); }

/* Wide form: a centred heading, one wide card, and the points in a row underneath. */
.builder-page .pb-contact-layout-wide { grid-template-columns: minmax(0, 1fr); gap: clamp(2rem, 4vw, 3rem); }
.builder-page .pb-contact-layout-wide .pb-contact-copy { justify-items: center; text-align: center; max-width: 46rem; margin-inline: auto; }
.builder-page .pb-contact-layout-wide .pb-contact-details { display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem 2rem; }
.builder-page .pb-contact-layout-wide .pb-contact-points { grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); gap: 1.5rem 2rem; }

/* Contrast band: the theme's inverted pair, so it is dark in light mode and light in dark mode,
   whatever background was picked. The fields are the page's own surface on the band, which keeps
   the browser's date and select controls the right way round for the mode. */
.builder-page .pb-block.pb-contact-layout-band {
  --pb-bg: var(--color-ink, #0f172a);
  --pb-ink: var(--color-surface, #fff);
  --pb-soft: var(--pb-bg);
  --pb-muted: var(--pb-ink);
  --pb-line: color-mix(in srgb, var(--pb-ink) 28%, transparent);
  background: var(--pb-bg); color: var(--pb-ink); border-radius: var(--pb-radius);
  grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); gap: clamp(2rem, 5vw, 4.5rem); align-items: start;
}
.builder-page .pb-block.pb-contact-layout-band:not(.pb-bleed) { padding-inline: clamp(1.25rem, 4%, 2.5rem); }
.builder-page .pb-block.pb-contact-layout-band :is(h2, h3, p, li, label, legend, .muted, .eyebrow, .hint, .field-optional, .field-error, .pb-contact-detail-label, .pb-contact-point-text) { color: var(--pb-ink); }
.builder-page .pb-block.pb-contact-layout-band .field-optional { font-weight: 400; }
.builder-page .pb-block.pb-contact-layout-band a:not(.btn) { color: var(--pb-ink); }
.builder-page .pb-block.pb-contact-layout-band .input {
  background: var(--color-surface, #fff); color: var(--color-ink, #0f172a); border-color: var(--color-border, var(--pb-line));
}
.builder-page .pb-block.pb-contact-layout-band .input::placeholder { color: var(--color-ink-muted, var(--color-ink, #0f172a)); }
.builder-page .pb-block.pb-contact-layout-band .has-error .input { border-color: var(--color-danger, currentColor); box-shadow: 0 0 0 2px var(--color-danger, transparent); }
.builder-page .pb-block.pb-contact-layout-band .field-error { font-weight: 600; }
.builder-page .pb-block.pb-contact-layout-band .alert { background: var(--color-surface, #fff); color: var(--color-ink, #0f172a); }
.builder-page .pb-block.pb-contact-layout-band .alert p { color: inherit; }
.builder-page .pb-block.pb-contact-layout-band .btn { background: var(--pb-ink); color: var(--pb-bg); border-color: var(--pb-ink); }
.builder-page .pb-block.pb-contact-layout-band input[type="checkbox"] { accent-color: var(--pb-ink); }

/* Photo beside the form: the picture is the left half, washed in the page's own ground and written
   over in the page's own ink — the hero's treatment (`--pb-hero-veil`/`-cover`), so it follows
   light and dark the same way and the strength is the block's `overlay`. */
.builder-page .pb-contact-layout-photo { --pb-hero-veil: 0.62; --pb-hero-cover: 0.55; }
.builder-page .pb-contact-layout-photo[data-overlay="none"] { --pb-hero-veil: 0; --pb-hero-cover: 0; }
.builder-page .pb-contact-layout-photo[data-overlay="light"] { --pb-hero-veil: 0.35; --pb-hero-cover: 0.28; }
.builder-page .pb-contact-layout-photo[data-overlay="medium"] { --pb-hero-veil: 0.62; --pb-hero-cover: 0.55; }
.builder-page .pb-contact-layout-photo[data-overlay="strong"] { --pb-hero-veil: 0.8; --pb-hero-cover: 0.72; }
.builder-page .pb-contact-layout-photo {
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: 0; align-items: stretch; overflow: hidden;
}
.builder-page .pb-contact.pb-contact-layout-photo .pb-contact-copy {
  position: relative; isolation: isolate; align-content: start; min-height: 28rem;
  padding: clamp(1.5rem, 4vw, 3rem); overflow: hidden; color: var(--pb-ink);
}
.builder-page .pb-contact-layout-photo .pb-contact-photo {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -2;
}
.builder-page .pb-contact-layout-photo .pb-contact-copy::after {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background:
    linear-gradient(to bottom,
      color-mix(in srgb, var(--pb-bg) calc(var(--pb-hero-cover) * 100%), transparent) 0%,
      color-mix(in srgb, var(--pb-bg) calc(var(--pb-hero-cover) * 55%), transparent) 55%,
      transparent 95%),
    color-mix(in srgb, var(--pb-bg) calc(var(--pb-hero-veil) * 100%), transparent);
}
.builder-page .pb-block.pb-contact-layout-photo .pb-contact-copy :is(h2, p, li, .muted, .eyebrow, .pb-contact-detail-label) { color: var(--pb-ink); }
.builder-page .pb-block.pb-contact-layout-photo .pb-contact-copy a:not(.btn) { color: var(--pb-ink); }
.builder-page .pb-contact-layout-photo .pb-contact-form { padding: clamp(1.5rem, 4vw, 3rem); align-self: center; }

@media (max-width: 960px) {
  .builder-page .pb-contact-layout-wide { --cf-cols: 2; --cf-half: 1; }
  .builder-page .pb-contact-layout-wide .contact-form > .field:has(.phone-field) { grid-column: 1 / -1; }
}
@media (max-width: 820px) {
  .builder-page .pb-contact-layout-split,
  .builder-page .pb-block.pb-contact-layout-band,
  .builder-page .pb-contact-layout-photo { grid-template-columns: minmax(0, 1fr); }
  .builder-page .pb-contact-layout-photo .pb-contact-copy { min-height: 18rem; }
}
@media (max-width: 640px) {
  .builder-page .pb-contact:not(.pb-contact-layout-classic) { --cf-cols: 1; --cf-half: 1; }
  .builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form > .btn { justify-self: stretch; }
}
/* **The form answers to its own width, not only to the window's.** Everything above asks how
   wide the *window* is, which is how wide the form is only on a design that reads across the
   whole page. linden reads in a column beside its stage: on a 1024px window that column is
   563px, the wide layout's card 452px, and four fields across it were 100px each — and a date
   box cannot be drawn narrower than its own day, month and year (174px), so "Preferred date"
   stood 22px outside the page. The same happens to any design that puts a built page beside
   something, and to a form a merchant drops into one column of a two-column section.
   So the card is a container and the count steps down by the card's width: four across needs
   48rem (a date box and its gap, four times), two across needs 26rem. The counts are set on
   the form here, where the steps above set them on the block: nearer wins, so whichever is
   fewer is what is drawn. Never the classic layout, whose form is not a grid of cells. */
.builder-page .pb-contact:not(.pb-contact-layout-classic) .pb-contact-form {
  container: pb-contact-form / inline-size;
}
@container pb-contact-form (max-width: 48rem) {
  .builder-page .pb-contact-layout-wide .contact-form { --cf-cols: 2; --cf-half: 1; }
  .builder-page .pb-contact-layout-wide .contact-form > .field:has(.phone-field) { grid-column: 1 / -1; }
}
@container pb-contact-form (max-width: 26rem) {
  .builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form { --cf-cols: 1; --cf-half: 1; }
  .builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form > .btn { justify-self: stretch; }
}
/* And a cell holds its box, whatever is in it. A field is a grid with one implicit `auto`
   track, which is as wide as its widest content; `minmax(0, 1fr)` is as wide as the cell. */
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form > .field {
  grid-template-columns: minmax(0, 1fr);
}
.builder-page .pb-contact:not(.pb-contact-layout-classic) .contact-form > .field > :is(input, select, textarea).input {
  min-width: 0; max-width: 100%;
}

/* "Find your gallery" and "Galleries" (`catalog.galleries`). The field and the cards are the
   shared partials `commerce.css` styles; this is only how the blocks space them. (0,3,0) and
   (0,2,0), so a theme's `p` reset at (0,1,2) does not eat the gaps. */
.builder-page .pb-gallery-code { display: grid; gap: 1rem; }
.builder-page .pb-gallery-code-heading { margin: 0; font-size: clamp(2rem, 4vw, 3.25rem); line-height: 1.05; }
.builder-page .pb-gallery-code > .lead { margin: 0; color: var(--pb-muted); max-width: 40rem; }
.builder-page .pb-gallery-code > .gallery-code { margin-top: 0.75rem; max-width: 44rem; }
.builder-page .pb-gallery-code-more { margin: 0; color: var(--pb-muted); }
.builder-page .pb-gallery-list > .pb-block-head + .gallery-cards { margin-top: 1.5rem; }
