/* compendium.css: built by make-site.mjs from DELIVERY/compendium/components; edit the components, not this file. */
/* consent-line 0.1.0 */
/* consent-line v0.1.0. The line is .fine from templates/lib/components.css (font-size --t-14, colour --mute)
   with nothing restated, so make-site.mjs adopts it with no visual change. Tokens only. */

/* the Privacy link keeps its underline so it is never told apart by colour alone (WCAG 1.4.1);
   the focus ring is the global one from tokens.css, none is added here */
.fine.consent a { text-decoration-line: underline; }

/* badge 0.1.0 */
/* badge v0.1.0. Extends .badge in templates/lib/components.css; tokens only.
   A badge is not a control: no hover, no transition, no animation, no focus ring. */

.badge {
  max-width: 100%;
  padding: var(--s-1) var(--s-2);
  border-radius: var(--r-1);
  color: var(--ink);
  font-size: var(--t-14);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-normal);
  text-transform: none;
  transition: none;
  animation: none;
  transform: none;
}

.badge.plate {
  background: var(--plate);
  color: var(--plate-ink);
  border-color: var(--line);
}

/* the group: a real list, laid out as a wrapping row */
ul.badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  list-style: none;
  max-width: 100%;
}
ul.badges > li.badge { min-width: 0; }

/* stats 0.1.0 */
/* Stats extends components.css .stats: type, colour and gap stay there.
   The column count follows the item count so no grid row is left with an orphan. */
.stats { grid-template-columns: minmax(0, 1fr); }
.stats > * { min-width: var(--r-0); overflow-wrap: anywhere; }
.stats b { color: var(--ink); letter-spacing: var(--ls-tight); background: none; }
.stats span { line-height: var(--lh-snug); }
.stats-source {
  display: block;
  margin-top: var(--s-2);
  padding-top: var(--s-2);
  border-top: 1px solid var(--line);
  font-size: var(--t-12);
  line-height: var(--lh-snug);
  color: var(--mute);
}

/* two items: two columns from 480 */
@media (min-width: 480px) {
  .stats:has(> :nth-child(2):last-child) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stats:has(> :nth-child(4):last-child) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* three items: one column, then three from 768 */
@media (min-width: 768px) {
  .stats:has(> :nth-child(3):last-child) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* four items: one, two, then four from 1024 */
@media (min-width: 1024px) {
  .stats:has(> :nth-child(4):last-child) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* on-plate: --mute on --plate falls under 4.5:1, so every part takes --plate-ink */
.stats.on-plate b,
.stats.on-plate span,
.stats.on-plate .stats-source,
.stats.on-plate .stats-source a { color: var(--plate-ink); }
.stats.on-plate .stats-source { border-top-color: var(--plate-ink); }

/* accordion 0.1.0 */
/* accordion v0.1.0. Tokens only. The focus ring is the global one in tokens.css; nothing here redraws it.
   templates/lib/components.css already sets the item border and padding, the bold summary at 24px minimum,
   the hidden native marker, the plus and minus glyphs and the answer gap and colour. The rules below extend those. */

/* question: ink on paper, the text and the marker side by side so a long question wraps beside the glyph */
.accordion summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  color: var(--ink);
  line-height: var(--lh-snug);
}

/* question text: may shrink below its content width, so a long word or URL wraps inside its box */
.accordion summary > span {
  min-width: 0;
  text-decoration-thickness: 1px;
  text-underline-offset: var(--s-1);
}

/* marker: the flex layout replaces the float from components.css; decorative, the state lives on details */
.accordion summary::after {
  float: none;
  flex: none;
  color: var(--ink);
}

/* answer: long words and URLs wrap inside the answer's box */
.accordion details > p {
  max-width: 65ch;
}

/* hover on pointer devices only, so a tap never leaves a question underlined */
@media (hover: hover) and (pointer: fine) {
  .accordion summary:hover > span { text-decoration-line: underline; }
}

/* review-card 0.1.0 */
/* Review card. One permissioned customer review: quote, then name, town, date and source.
   Not a control: no hover, no lift, no transition, no shadow, no side accent border.
   components.css .quote is a pull quote with a heavy top rule, so the card uses its own classes. */

/* the list: one li per card, --s-5 between cards */
.review-list {
  list-style: none;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-5);
}
.review-list > li { min-width: 0; }

/* card: --paper on a --paper-2 ground; .on-paper flips it for a --paper ground */
.review-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  min-width: 0;
  padding: var(--s-5);
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  box-shadow: none;
  transform: none;
  overflow-wrap: anywhere;
  transition: none !important;
  animation: none !important;
}
.review-list.on-paper .review-card { background: var(--paper-2); }

/* quote: verbatim text, never styled as a pull quote */
.review-quote p {
  font-size: var(--t-20);
  line-height: var(--lh-body);
  color: var(--ink);
}

/* attribution: name on its own line, then town, date, source and stars */
.review-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: var(--s-2);
  row-gap: var(--s-1);
  font-size: var(--t-14);
  line-height: var(--lh-snug);
  color: var(--mute);
}
.review-meta > * { min-width: 0; }
.review-name {
  flex-basis: 100%;
  font-size: var(--t-16);
  font-style: normal;
  font-weight: 700;
  color: var(--ink);
}
.review-town,
.review-date { color: var(--mute); }
.review-source {
  display: inline-flex;
  align-items: center;
  min-height: var(--s-5);
  min-width: var(--s-5);
  letter-spacing: var(--ls-normal);
  color: var(--graphite);
}
a.review-source { text-underline-offset: var(--s-1); }

/* stars: glyphs are decoration, the hidden line carries the score */
.review-stars {
  display: inline-flex;
  align-items: center;
  color: var(--ink);
  letter-spacing: var(--ls-normal);
}
.review-stars-text {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* on-plate: --mute on --plate falls under 4.5:1, so every part takes --plate-ink */
.review-card.on-plate { background: var(--plate); color: var(--plate-ink); }
.review-card.on-plate .review-quote p,
.review-card.on-plate .review-meta,
.review-card.on-plate .review-name,
.review-card.on-plate .review-town,
.review-card.on-plate .review-date,
.review-card.on-plate .review-source,
.review-card.on-plate .review-stars { color: var(--plate-ink); }

/* pricing-tiers 0.1.3 */
/* pricing-tiers 0.1.3. Pricing tiers extends components.css: .btn, .btn.ghost and the h3 reset stay there.
   Column count follows data-tiers so no card sits alone on a last row; never auto-fit.
   Cards are subgrids of two rows (body, action) so every action in a grid row starts at the same top. */
.tiers { display: flex; flex-direction: column; gap: var(--s-5); }

.tiers-list {
  list-style: none;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  /* the list's row gap equals the cards' so the subgrid rows share one gutter: a subgrid gap that differs from its parent's is
     paid as negative margin on every part, which collapsed the flag row to 0px and pushed the flagged price down (0.1.2);
     the space between rows of cards comes from each card's bottom margin instead */
  row-gap: var(--s-3);
  column-gap: var(--grid-gap);
}

.tier {
  display: grid;
  grid-row: span 6;
  grid-template-rows: subgrid;
  margin-block-end: calc(var(--grid-gap) - var(--s-3));
  min-width: 0;
  padding: var(--s-5);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
}
.tier.is-recommended { border-color: var(--line-strong); }

/* 0.1.2 (2026-09-26): every part of a card sits on a row shared across the list (name, flag, price, line, includes, action), so a
   Recommended flag on one card no longer pushes its price below the others (Saturn: $80 sat 33px under $45 and $130 at 1440).
   The body is display: contents, so its parts join the card's subgrid; the flag stays after the name on screen and in reading order. */
.tier-body { display: contents; }
/* the rows set all spacing inside a card; a site rule such as p + p must not add a margin to one card only (0.1.2) */
.tier-price, .tier-line, .tier-includes { margin-top: 0; margin-bottom: 0; }
/* one column that may shrink below its content, so a long name or compound word wraps inside the card at 320 */
.tier { grid-template-columns: minmax(0, 1fr); }
.tier-body > *, .tier > .btn { min-width: 0; }
.tier-name { grid-row: 1; }
.tier-flag { grid-row: 2; }
.tier-price { grid-row: 3; }
.tier-line { grid-row: 4; }
.tier-includes { grid-row: 5; }
.tier > .btn { grid-row: 6; margin-top: var(--s-1); }

.tier-name {
  font-size: var(--t-20);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-normal);
  font-weight: 700;
  color: var(--ink);
}
.tier-flag {
  margin-top: calc(var(--s-1) - var(--s-3));
  font-size: var(--t-14);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-normal);
  font-weight: 700;
  color: var(--graphite);
}
.tier-price {
  font-size: var(--t-32);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  color: var(--ink);
}
.tier-price b { font-weight: 800; font-variant-numeric: tabular-nums; }
.tier-unit {
  margin-left: var(--s-1);
  font-size: var(--t-14);
  letter-spacing: var(--ls-normal);
  color: var(--mute);
}
.tier-line { font-size: var(--t-16); line-height: var(--lh-body); color: var(--graphite); }
.tier-includes {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding-left: var(--s-5);
  list-style: disc;
  font-size: var(--t-16);
  line-height: var(--lh-body);
  color: var(--ink);
}

/* the action: the button component's .btn, full width at the foot of the card */
.tier > .btn { align-self: stretch; width: 100%; text-align: center; }

.tiers-quote { font-size: var(--t-14); line-height: var(--lh-body); color: var(--mute); }
.tiers-quote a { color: inherit; text-decoration: underline; }

/* the action keeps its colour feedback and drops its press movement under reduced motion */
@media (prefers-reduced-motion: reduce) {
  .tiers .btn:active { transform: none; }
}

/* two tiers: one column, then two from 768; four tiers: one, two from 768, four from 1024 */
@media (min-width: 768px) {
  .tiers-list[data-tiers="2"],
  .tiers-list[data-tiers="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* three tiers: one column, then three from 1024 */
@media (min-width: 1024px) {
  .tiers { gap: var(--s-6); }
  .tiers-list[data-tiers="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .tiers-list[data-tiers="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* on-plate: --mute on --plate falls under 4.5:1, so every text part takes --plate-ink */
.tiers.on-plate .tier { background: var(--plate); border-color: var(--mute); }
.tiers.on-plate .tier.is-recommended { border-color: var(--plate-ink); }
.tiers.on-plate .tier-name,
.tiers.on-plate .tier-flag,
.tiers.on-plate .tier-price,
.tiers.on-plate .tier-unit,
.tiers.on-plate .tier-line,
.tiers.on-plate .tier-includes,
.tiers.on-plate .tiers-quote { color: var(--plate-ink); }
.tiers.on-plate .btn.ghost { color: var(--plate-ink); border-color: var(--plate-ink); }

/* 0.1.3 (2026-09-26, the plumbing showroom lane): while the cards stack in one column no card shares a row with a flagged card, so an
   unflagged card's empty flag row only added a second gutter above its price; the price takes that gutter back. In columns the empty
   row stays, so prices line up across the row. */
@media (max-width: 767px) { .tier:not(.is-recommended) .tier-price { margin-top: calc(-1 * var(--s-3)); } }
@media (min-width: 768px) and (max-width: 1023px) { .tiers-list[data-tiers="3"] .tier:not(.is-recommended) .tier-price { margin-top: calc(-1 * var(--s-3)); } }

/* before-after-slider 0.1.1 */
/* before-after-slider 0.1.1. Tokens only; extends tokens.css and components.css, restates nothing.
   Without script (no .js on html) or with .static the pair is two plain photos: stacked below 768px,
   side by side from 768px. With script the two panes share one frame, the after pane is clipped by
   component.js and a native range drives it. The handle hit area is 44px: --s-6 plus --s-3. */

.ba { --ba-hit: calc(var(--s-6) + var(--s-3)); margin: 0; }
.ba + .ba { margin-top: var(--s-5); }

/* frame: the fallback layout */
.ba-stage { position: relative; display: grid; gap: var(--grid-gap); }
@media (min-width: 768px) {
  .ba-stage { grid-template-columns: 1fr 1fr; }
}
.ba-pane { position: relative; overflow: hidden; border-radius: var(--r-2); background: var(--paper-2); }
.ba-pane img { width: 100%; height: auto; }

/* labels and disclosure: a --paper chip over the photo, never text straight on a photo */
.ba-label,
.ba-disclosure {
  position: absolute; inset-inline-start: var(--s-3);
  max-width: calc(100% - (var(--s-3) * 2));
  padding: var(--s-1) var(--s-2); border-radius: var(--r-1);
  background: var(--paper); color: var(--ink);
  font-size: var(--t-14); line-height: var(--lh-snug); font-weight: 700;
}
.ba-label { top: var(--s-3); }
.ba-disclosure { bottom: var(--s-3); font-weight: 600; }
.ba-label:empty, .ba-disclosure:empty { display: none; }

/* caption */
.ba figcaption { margin-top: var(--s-4); font-size: var(--t-16); line-height: var(--lh-body); color: var(--ink); }
.ba-meta { display: block; font-size: var(--t-14); color: var(--mute); }

/* on-plate */
.ba.on-plate { padding: var(--s-4); border-radius: var(--r-2); background: var(--plate); color: var(--plate-ink); }
.ba.on-plate figcaption, .ba.on-plate .ba-meta { color: var(--plate-ink); }

/* empty: a pair missing either photo is not rendered at all */
.ba:not(:has(.ba-before img)),
.ba:not(:has(.ba-after img)) { display: none; }

/* enhanced: one frame, both panes in the same cell, after on top and clipped from the left, so before shows left of the divider */
.js .ba:not(.static) .ba-stage { grid-template-columns: 1fr; gap: 0; touch-action: pan-y; }
.js .ba:not(.static) .ba-pane { grid-area: 1 / 1; }
.js .ba:not(.static) .ba-after .ba-label { inset-inline-start: auto; inset-inline-end: var(--s-3); }
.js .ba:not(.static) .ba-pane.is-broken { visibility: hidden; }

/* divider: the native range covers the frame and draws nothing itself */
.ba-range {
  position: absolute; inset: 0; z-index: 2;
  width: 100%; height: 100%; margin: 0;
  opacity: 0; cursor: ew-resize; touch-action: pan-y;
  -webkit-appearance: none; appearance: none;
}
.ba-range:focus-visible { outline: none; }

/* handle: the script sets its left edge to the divider (left: value percent); the track never moves, so nothing reaches past the frame */
.ba-track { position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow-x: clip; }
.ba-track-inner { position: absolute; inset: 0; }
.ba-handle {
  position: absolute; top: 0; bottom: 0; left: 0;
  width: var(--ba-hit); margin-inline-start: calc(var(--ba-hit) / -2);
  display: flex; align-items: center; justify-content: center;
}
.ba-handle::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 50%;
  width: var(--s-1); transform: translateX(-50%);
  background: var(--paper); box-shadow: 0 0 0 1px var(--line), var(--sh-sm);
}
.ba-grip {
  position: relative; display: block;
  width: var(--s-6); height: var(--s-6); border-radius: var(--r-full);
  background: var(--paper); border: 1px solid var(--line); box-shadow: var(--sh-sm);
  transition: transform var(--d-fast) var(--e-out);
}
.ba-grip::before,
.ba-grip::after {
  content: ""; position: absolute; top: 50%;
  width: var(--s-2); height: var(--s-2);
  border-top: 2px solid var(--ink); border-left: 2px solid var(--ink);
}
.ba-grip::before { left: var(--s-2); transform: translateY(-50%) rotate(-45deg); }
.ba-grip::after { right: var(--s-2); transform: translateY(-50%) rotate(135deg); }

/* focus: the site ring from tokens.css (3px --accent, 2px offset) drawn on the handle; 3px is --s-1 less a hairline */
.ba-range:focus-visible ~ .ba-track .ba-handle,
.ba.is-focus .ba-handle {
  outline: calc(var(--s-1) - 1px) solid var(--accent); outline-offset: 2px; border-radius: var(--r-1);
}

/* active: the grip scales while dragging, the only animated property */
.ba-range:active ~ .ba-track .ba-grip,
.ba.is-dragging .ba-grip,
.ba.is-active .ba-grip { transform: scale(1.12); }

/* list of pairs */
.ba-list { display: grid; gap: var(--s-5); }

/* logo-wall 0.1.1 */
/* logo-wall v0.1.1. Loads after tokens.css, components.css and motion.css; token names only.
   A static strip: nothing here moves, and the marquee wrapper from motion.css is never used. */

.logo-wall { padding-block: var(--s-7); }
.logo-wall h2 {
  font-size: var(--t-14);
  font-weight: 700;
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--graphite);
  margin-bottom: var(--s-5);
}

/* a centered wrapping row (0.1.1, 2026-09-26): two to a row on a phone, fixed-width items from 768px, and a short last row is
   centered, so no empty grid track and no orphan at the left edge (balance rule, lesson 28). The auto-fill grid it replaces
   left one or two empty tracks beside four logos. */
.logo-wall ul {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--grid-gap) var(--s-5);
}
.logo-wall li {
  flex: 0 1 calc((100% - var(--s-5)) / 2);
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-2) var(--s-4);
}

/* the logo box is fixed by the item, so a mark that fails to load keeps its size */
.logo-wall img {
  width: 100%;
  height: var(--s-7);
  object-fit: contain;
  filter: grayscale(1);
  color: var(--ink);
  font-size: var(--t-14);
}

/* tiled: a quiet ground for marks that need one */
.logo-wall.tiled li {
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
}

/* linked: the only variant with hover and focus; the change is instant, never animated */
.logo-wall.linked a { display: flex; width: 100%; justify-content: center; }
/* the global reduced-motion rule gives every property a 1ms transition; this strip takes none */
.logo-wall a,
.logo-wall img { transition: none; }
.logo-wall.linked a:hover img,
.logo-wall.linked a:focus-visible img { filter: none; }

@media (min-width: 768px) {
  .logo-wall { padding-block: var(--s-8); }
  .logo-wall li { flex-basis: calc(var(--s-10) + var(--s-6)); }
  .logo-wall img { height: var(--s-8); }
}

/* lightbox 0.1.0 */
/* lightbox 0.1.0. Extends .gallery from components.css for the thumbnails; the site focus ring from
   tokens.css stays, recoloured to --accent-on-dark on the plate. Opacity fades only, no transform,
   so reduced motion leaves nothing moving beyond the tokens.css 1ms cap. */

/* ---------- thumbnails: plain links to the full image ---------- */
.lightbox-thumbs { list-style: none; }
.lightbox-trigger { display: block; border-radius: var(--r-2); color: var(--accent); }
.lightbox-trigger img { transition: box-shadow var(--d-fast) var(--e-out), opacity var(--d-fast) var(--e-out); }
.lightbox-trigger.is-hover img { box-shadow: 0 0 0 2px var(--accent); opacity: .9; }
.lightbox-trigger.is-focus { outline: calc(var(--s-1) - 1px) solid var(--accent); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .lightbox-trigger:hover img { box-shadow: 0 0 0 2px var(--accent); opacity: .9; }
}
.lightbox.single .lightbox-thumbs { grid-template-columns: minmax(0, 1fr); max-width: calc(var(--s-10) * 3); }

/* ---------- the dialog, and the pinned panel the proof uses to show it in flow ---------- */
.lightbox-dialog {
  width: 100%; height: 100%; max-width: 100%; max-height: 100%;
  margin: 0; inset: 0; border: 0; padding: var(--s-4);
  background: var(--plate); color: var(--plate-ink);
  opacity: 0;
  transition: opacity var(--d-base) var(--e-out), overlay var(--d-base) var(--e-out) allow-discrete, display var(--d-base) var(--e-out) allow-discrete;
}
.lightbox-dialog[open], .lightbox-dialog.is-pinned {
  display: grid; grid-template-rows: auto minmax(0, 1fr); gap: var(--s-4); opacity: 1;
}
@starting-style { .lightbox-dialog[open] { opacity: 0; } }
.lightbox-dialog::backdrop { background: var(--plate); }
.lightbox-dialog.is-pinned { height: auto; border-radius: var(--r-2); }
@media (min-width: 768px) { .lightbox-dialog { padding: var(--s-5); } }

.lightbox-dialog :focus-visible { outline-color: var(--accent-on-dark); }

.lightbox-bar { display: flex; align-items: center; gap: var(--s-4); }
.lightbox-counter { font-size: var(--t-14); color: var(--plate-ink); font-variant-numeric: tabular-nums; }
.lightbox-close { margin-left: auto; }

.lightbox-stage { display: flex; align-items: center; gap: var(--s-2); min-height: 0; }
.lightbox-figure {
  flex: 1; align-self: stretch; min-width: 0; min-height: 0;
  display: grid; grid-template-rows: minmax(0, 1fr) auto auto;
}
.lightbox-image { width: 100%; height: 100%; object-fit: contain; border-radius: var(--r-1); }
.is-pinned .lightbox-image { height: auto; }
.lightbox-caption { padding-top: var(--s-2); font-size: var(--t-14); line-height: var(--lh-snug); color: var(--plate-ink); text-align: center; }

/* loading keeps the frame; error drops the image and says so, with a way to the file */
.lightbox-figure.is-loading .lightbox-image { visibility: hidden; }
.lightbox-figure.is-error .lightbox-image { display: none; }
.lightbox-figure.is-error { grid-template-rows: minmax(0, 1fr) auto; align-content: center; }
.lightbox-status { padding-top: var(--s-2); font-size: var(--t-14); line-height: var(--lh-snug); color: var(--plate-ink); text-align: center; }
.lightbox-status[hidden] { display: none; }
.lightbox-status a { color: var(--accent-on-dark); font-weight: 700; }

/* ---------- controls: 48px targets, round, on the plate ---------- */
.lightbox-close, .lightbox-prev, .lightbox-next {
  flex-shrink: 0; display: inline-grid; place-items: center;
  width: var(--s-7); height: var(--s-7); padding: 0;
  border: 2px solid var(--plate-ink); border-radius: var(--r-full);
  background: transparent; color: var(--plate-ink); font: inherit; cursor: pointer;
  transition: color var(--d-fast) var(--e-out), border-color var(--d-fast) var(--e-out), background-color var(--d-fast) var(--e-out);
}
.lightbox-dialog button svg { width: var(--s-5); height: var(--s-5); }
@media (hover: hover) and (pointer: fine) {
  .lightbox-dialog button:hover:not([disabled]) { color: var(--accent-on-dark); border-color: var(--accent-on-dark); }
}
.lightbox-dialog button.is-hover { color: var(--accent-on-dark); border-color: var(--accent-on-dark); }
.lightbox-dialog button.is-focus { outline: calc(var(--s-1) - 1px) solid var(--accent-on-dark); outline-offset: 2px; }
.lightbox-dialog button:active:not([disabled]), .lightbox-dialog button.is-active {
  background: var(--accent-on-dark); border-color: var(--accent-on-dark); color: var(--plate);
}
.lightbox-dialog button[disabled] { opacity: .35; cursor: not-allowed; }

/* ---------- page scroll lock while open; the script restores the position on close ---------- */
html.lightbox-lock { overflow: hidden; }

/* lead-form 0.1.1 */
/* lead-form v0.1.0. Extends templates/lib/tokens.css and components.css; tokens only.
   The pattern lays out its parts and restyles none of the composed components: on a selector owned by
   field, form-row, textarea, file-upload, consent-line or button it sets layout only. The heading is the
   .h2 rule, the lead the .lead rule, the status the .fine rule, the honeypot the .hp rule, all from components.css.
   No motion of its own; the sent message appears without an entrance. No overflow clip, so no focus ring is cut. */

/* stacked (default): one column; the lib's .split is reset here so the split variant stacks below 1024px */
.lead-form,
.lead-form.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-3);
  align-items: start;
  padding-block: var(--s-7);
}

/* 12px from heading to lead, then 24px to the form or the sent message */
.lead-form > .form,
.lead-form > .sent { margin-top: var(--s-3); }

/* the form: one column of parts, 16px apart */
.lead-form .form {
  display: grid;
  gap: var(--s-4);
}
/* .form sets display, so the hidden attribute needs restating after a successful send */
.lead-form .form[hidden] { display: none; }

/* contact-row and where-row: stacked below 768px, two equal cells from 768px */
.lead-form .form-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-4);
  align-items: start;
}

/* status: the .fine rule, with the body line height; a failed send reads in the error colour */
.lead-form [data-status] { line-height: var(--lh-body); }
.lead-form [data-status][data-tone="err"] { color: var(--err); font-weight: 600; }

/* the submit is full width below 768px and never shrinks; the consent line sits 8px under it */
.lead-form .form > button[type="submit"] { justify-self: stretch; }
.lead-form .form > .consent { margin-top: calc(var(--s-2) - var(--s-4)); }

/* the in-place confirmation */
.lead-form .sent {
  font-size: var(--t-16);
  line-height: var(--lh-body);
  color: var(--ok);
  font-weight: 600;
}

@media (min-width: 768px) {
  .lead-form .form-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .lead-form .form > button[type="submit"] { justify-self: start; }
}

/* split: the copy column (.lead-form-copy: heading, then lead) beside the form, from 1024px up. Two cells in one row, stretched,
   so both columns fill the row (balance rule, lesson 28; 0.1.1, 2026-09-26: the heading and lead used to be two grid items
   beside a form spanning two rows, and the layout gate measured one row with items 88px and 554px tall). */
.lead-form-copy { display: flex; flex-direction: column; gap: var(--s-3); min-width: 0; }
@media (min-width: 1024px) {
  .lead-form.split {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--grid-gap);
    align-items: stretch;
  }
  .lead-form.split > .lead-form-copy { grid-column: 1; grid-row: 1; }
  .lead-form.split > .form,
  .lead-form.split > .sent { grid-column: 2; grid-row: 1; margin-top: 0; }
}

/* booking 0.1.0 */
/* booking v0.1.0. Extends templates/lib/tokens.css and components.css; tokens only.
   The pattern lays out its parts and restyles none of the composed components: on a selector owned by
   field, select, radio-group, notice, button or consent-line it sets layout only. The heading is the .h2 rule,
   the notice the .notice rule, the submit the .btn rule, the consent line the .fine rule, all from components.css.
   No motion of its own. No overflow clip, so no focus ring is cut. */

/* the section: the strip's ground and its padding */
.booking {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-5);
  align-items: start;
  padding: var(--s-6) var(--s-5);
  background: var(--paper-2);
}

/* the form: one column below 1024px, parts 16px apart */
.booking .form {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-4);
  align-items: start;
}

/* the submit is full width below 1024px; the consent line sits 8px under it */
.booking .form > button[type="submit"] { justify-self: stretch; }
.booking .form > .consent { margin-top: calc(var(--s-2) - var(--s-4)); }

/* the empty state: the phone lane and the link to the lead form, in place of the form */
.booking .lane {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  align-items: center;
}

/* strip (default) from 1024px: the notice on top, then service, date, count or size, zip and the submit in one row.
   Each field is a subgrid of three rows (label, control, help), so a two-line label or an error help
   never moves a control or the submit off the shared top line. */
@media (min-width: 1024px) {
  .booking:not(.page) .form {
    grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
    grid-template-rows: auto auto auto auto auto;
    column-gap: var(--s-4);
    row-gap: var(--s-1);
  }
  .booking:not(.page) .form > .notice { grid-row: 1; grid-column: 1 / -1; margin-bottom: var(--s-4); }
  .booking:not(.page) .form > .field {
    display: grid;
    grid-row: 2 / span 3;
    grid-template-rows: subgrid;
    row-gap: var(--s-1);
  }
  .booking:not(.page) .form > .radio-group { grid-row: 2 / span 3; }
  .booking:not(.page) .form > button[type="submit"] { grid-row: 3; grid-column: -2 / -1; justify-self: stretch; align-self: start; }
  .booking:not(.page) .form > .consent { grid-row: 5; grid-column: 1 / -1; margin-top: var(--s-2); }
  .booking:not(.page) .form > .fine:not(.consent) { grid-row: 6; grid-column: 1 / -1; margin-top: var(--s-2); }
}

/* page: the /book page keeps one column at every width, the submit sized to its label from 768px */
@media (min-width: 768px) {
  .booking.page .form > button[type="submit"] { justify-self: start; }
}

/* pricing 0.1.1 */
/* pricing v0.1.0. Extends templates/lib/tokens.css and components.css; tokens only.
   A pattern: it lays out the heading (the h2 rule), the lead (the .lead rule), the pricing-tiers block,
   the link component and the button component with gap and margin only, and restyles none of them.
   No selector here ends on a composed component's class. No motion, no overflow clip, so no focus ring is cut. */

/* one column in DOM order: heading, lead, tiers, FAQ link, action */
.pricing {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* 12px heading to lead, 32px lead to tiers, 24px tiers to FAQ link, 16px FAQ link to action */
.pricing > p { margin-top: var(--s-3); }
.pricing > div { margin-top: var(--s-6); align-self: stretch; }
.pricing > a:first-of-type { margin-top: var(--s-5); }
.pricing > a:last-of-type { margin-top: var(--s-4); }

/* 48px from the action to the band end, in place of the band's own padding below */
.section:has(> .wrap > .pricing) { padding-block-end: var(--s-7); }

/* the section and its layout wrappers ship no motion, reduced motion or not */
.pricing,
.section:has(> .wrap > .pricing),
.section > .wrap:has(> .pricing) {
  animation: none;
  transition-duration: 0s !important;
}

/* faq 0.1.0 */
/* faq v0.1.0. Tokens only. The pattern lays out what it composes and styles nothing it borrows:
   the band's padding, ground and top rule come from .section, the width from .wrap, the questions from
   .accordion, the phone and contact from .link. The focus ring is the global one in tokens.css.
   No selector here ends in .section, .wrap, .accordion, details, summary or .link. */

/* heading to questions: the gap the section spec sets between its h2 and the body */
.faq h2 {
  margin-bottom: var(--s-5);
}

/* closing line: the one element the pattern styles itself, plain body type in ink after the questions */
.faq-close {
  margin-top: var(--s-6);
  font-size: var(--t-16);
  line-height: var(--lh-body);
  color: var(--ink);
}

/* reviews 0.1.1 */
/* reviews v0.1.0. Tokens only. The pattern lays out what it composes and styles nothing it borrows:
   the band's padding, ground, top rule and heading gap come from .section, the width from .wrap, the
   columns and card gap from .grid.three, each card from .review-card and the source link from .link.
   The focus ring is the global one in tokens.css. No motion, no overflow hidden.
   No selector here ends in .section, .wrap, .grid, .review-card or .link. */

/* the card list keeps its list role and drops the markers, which would hang into the gutter */
.reviews ul {
  list-style: none;
}

/* source line: the one element the pattern styles itself, plain body type after the cards */
.reviews-source {
  margin-top: var(--s-6);
  font-size: var(--t-16);
  line-height: var(--lh-body);
  color: var(--graphite);
}

/* on the plate band --graphite falls under 4.5:1, so the line takes --plate-ink */
.section.plate.reviews .reviews-source {
  color: var(--plate-ink);
}

/* every card fills its row (balance rule: items on one grid row share a height); the li is a one-cell grid, so the card stretches.
   Added 2026-09-26 when the pattern first rendered on the showrooms; nothing here restyles the card. */
.reviews ul > li {
  display: grid;
}

/* legal-pages 0.1.0 */
/* legal-pages v0.1.0. Tokens only. The pattern lays out what it composes and styles only its own parts:
   the band's padding, ground and top rule come from .section, the measure from .wrap.narrow, the contents
   list from .stack, the links from .link, the rule from .divider, and the section spacing from .prose in
   components.css. The focus ring is the global one in tokens.css. No motion, no overflow hidden.
   No selector here ends in .section, .wrap, .stack, .link or .divider. */

/* heading to date line */
.legal h1 {
  margin-bottom: var(--s-4);
}

/* date line: effective date, or the keyboard walkthrough line */
.legal-date {
  font-size: var(--t-16);
  line-height: var(--lh-body);
  color: var(--graphite);
}

/* parties line, terms only */
.legal-lede {
  margin-top: var(--s-4);
  font-size: var(--t-16);
  line-height: var(--lh-body);
  color: var(--ink);
}

/* contents: its own spacing only; the list gap and the 44px targets come from .stack and .link-standalone */
.legal-toc {
  margin-top: var(--s-6);
  margin-bottom: var(--s-6);
}
.legal-toc ol {
  list-style-position: inside;
}

/* legal section headings sit a step below the page heading, and land clear of the top when jumped to */
.legal .prose h2 {
  font-size: var(--t-24);
  scroll-margin-top: var(--s-5);
}
.legal .prose ul {
  padding-left: var(--s-5);
}

/* the space above the divider, then the fine print */
.legal .prose {
  margin-bottom: var(--s-5);
}
.legal-fine {
  margin-top: var(--s-5);
  font-size: var(--t-14);
  line-height: var(--lh-body);
  color: var(--mute);
}

/* divider 0.1.0 */
.divider {
  border: none;
  border-top: thin solid var(--line);
  margin-block: var(--s-5);
  margin-inline: var(--r-0);
  height: var(--r-0);
}

.divider.strong {
  border-top-color: var(--line-strong);
}

.divider.vertical {
  border-top: none;
  border-left: thin solid var(--line);
  margin-block: var(--r-0);
  margin-inline: var(--s-3);
  width: var(--r-0);
  height: auto;
  align-self: stretch;
  flex-shrink: 0;
}

.divider-proof-row {
  flex-wrap: nowrap;
  gap: var(--r-0);
}

.divider-proof-row > p {
  flex: 1;
}

.divider-proof-secondary {
  background: var(--paper-2);
  padding: var(--s-4);
}

/* breadcrumb 0.1.0 */
/* breadcrumb v0.1.0. Tokens only. The focus ring is the global one in tokens.css; nothing here redraws it.
   Nothing in templates/lib/components.css covers a trail, so every rule below is new and scoped to .breadcrumb. */

/* landmark: quiet small text, placed inside main above the page heading or in the header band */
.breadcrumb {
  font-size: var(--t-14);
  line-height: var(--lh-body);
  color: var(--mute);
}

/* list: a flex row that wraps onto the next line; it never scrolls sideways */
.breadcrumb ol {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2);
  max-width: 100%;
}

/* item: may shrink below its content width, so a long label wraps inside its box instead of spilling */
.breadcrumb li {
  min-width: 0;
  max-width: 100%;
}

/* separator: a slash drawn by CSS with empty alternative text; never in the DOM text, never read aloud */
.breadcrumb li + li::before {
  content: "/" / "";
  margin-inline-end: var(--s-2);
  color: var(--mute);
}

/* link: an ancestor page; padding-block gives the tap height, never negative margins, never inline-flex */
.breadcrumb a {
  color: var(--accent);
  text-decoration-line: none;
  text-decoration-thickness: 1px;
  text-underline-offset: var(--s-1);
  padding-block: var(--s-1);
  border-radius: var(--r-1);
  transition: color var(--d-fast) var(--e-out);
}

/* current: the page the visitor is on, plain text in ink */
.breadcrumb [aria-current="page"] {
  color: var(--ink);
}

/* hover on pointer devices only, so a tap never leaves a link stuck in its hover colour */
@media (hover: hover) and (pointer: fine) {
  .breadcrumb a:hover { color: var(--ink); text-decoration-line: underline; }
  .breadcrumb.on-plate a:hover { color: var(--plate-ink); }
}

.breadcrumb a:active {
  color: var(--ink);
  text-decoration-line: underline;
}

/* a level handed over without a title is left out of the trail, and leaves the accessibility tree with it */
.breadcrumb li:has(> :empty) {
  display: none;
}

/* parent-only: below 768 only the immediate parent link shows; the rest leave the accessibility tree */
@media (width < 768px) {
  .breadcrumb.parent-only li:not(:nth-last-child(2)) { display: none; }
  .breadcrumb.parent-only li:nth-last-child(2)::before { content: none; }
}

/* on-plate: the trail on the dark header band; the one focus ring swaps to the on-dark accent */
.breadcrumb.on-plate {
  color: var(--plate-ink);
}
.breadcrumb.on-plate a {
  color: var(--accent-on-dark);
}
.breadcrumb.on-plate a:active {
  color: var(--plate-ink);
}
.breadcrumb.on-plate [aria-current="page"],
.breadcrumb.on-plate li + li::before {
  color: var(--plate-ink);
}
.breadcrumb.on-plate a:focus-visible {
  outline-color: var(--accent-on-dark);
}

/* the plate ground an on-plate trail sits on in the compendium; a site's header band supplies its own */
.breadcrumb-plate {
  background: var(--plate);
  color: var(--plate-ink);
  padding: var(--s-4);
  border-radius: var(--r-2);
}
