/* 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;
  }
}
.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; }

/* 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; }

/* 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; }
.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; }

/* 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); }

/* 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; }

/* 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; }

/* 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; }
.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; }
}

/* 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; }

/* 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)); }
.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-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; }
.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); }
