/* ─────────────────────────────────────────────────────────────────────────
   WEBAVIE ENGINE — all structure and components for every site webavie runs.
   Vendored to public/assets/lws/engine.css. Never hand-edited on a site.

   NOTE ON BRANDING: nothing in this file is Luhwah-branded, and nothing in it
   may become so. The chrome here is the CLIENT's — their palette, their
   wordmark, their favicon. webavie is an "-avie" product by name only; the
   hot-pink "vie" signature in LUHWAH-STANDARDS.md §1 applies to that family's
   OWN interfaces, never to the sites this engine emits.

   THE RULE THIS FILE LIVES BY: not one colour, font, radius, shadow or spacing
   value is written literally here. Every one reads a --lws-* custom property
   emitted inline by theme.php from theme.json. That is what makes a reskin a
   data change, and what stops the "the variable is called --blue but holds a
   slate" rot the builds this engine came from ended up with.

   THE ONE EXEMPTION, and it is narrow: white text sitting on a SCRIM — the dark
   wash laid over a client's photograph in a bleed hero, a photo banner, a
   member's name card, a before/after tag. `--lws-scrim-rgb` is derived to be
   dark on every palette precisely so that white is always right there, which is
   what makes the literal safe rather than lazy.

   It has been broken twice, both times the same way: a paragraph on a COLOURED
   BAND (the closing call to action, the highlight, the numbers band) written as
   white while the heading above it correctly used `--lws-on-brand`. On a dark
   brand nobody notices. On natural-sage the heading turns dark and the
   paragraph stays white — 2.55:1 — and the Appearance readability check
   reported it as fine because it only tested the pairs that used tokens. If you
   are about to write white on a band, you want a `--lws-on-*` token, and the
   readability check should be testing that pair.

   Visual range across sites comes from four levers, not from editing this file:
     1. tokens          — palette, type pairing, radius, density, shadow
     2. [data-motif]    — the signature decoration (assets/lws/motifs/*.css)
     3. [data-variant]  — per-block layout variants, selected as attributes
     4. [data-ground]   — the per-theme section rhythm

   BREAKPOINTS: 1024 / 767 / 479, desktop-first. The odd-looking 767 and 479 are
   deliberate — they are Kadence's own breakpoints, and imported WordPress
   content ships inline @media rules on exactly those values. Aligning to them
   means designed sections and migrated sections collapse on the SAME pixel,
   which is the fix for the "two uncoordinated breakpoint systems" finding.
   ───────────────────────────────────────────────────────────────────────── */

/* ── reset + base ──────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 5rem;          /* sticky header height, for #anchors */
}

body {
  margin: 0;
  background: var(--lws-surface);
  color: var(--lws-body);
  font-family: var(--lws-font-body);
  font-size: var(--lws-text);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
}

img, picture, video, canvas, svg, iframe { display: block; max-width: 100%; }
img, video { height: auto; }

input, button, textarea, select { font: inherit; color: inherit; }

/* The icon library is stroke-based and inherits currentColor, so an icon is
   always the colour of the text around it. Social marks are solid and opt out. */
svg { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }

a { color: var(--lws-link); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--lws-brand-deep); }

:focus-visible {
  outline: 3px solid var(--lws-accent);
  outline-offset: 2px;
  border-radius: 2px;
}

::selection { background: var(--lws-accent-soft); color: var(--lws-ink); }

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  padding: .75rem 1.25rem;
  background: var(--lws-brand); color: var(--lws-on-brand);
  border-radius: 0 0 var(--lws-radius) 0;
}
.skip-link:focus { left: 0; }

/* ── typography ────────────────────────────────────────────────────────── */

h1, h2, h3, h4, h5, h6 {
  margin: 0 0 .5em;
  font-family: var(--lws-font-display);
  font-weight: 600;
  line-height: 1.15;
  color: var(--lws-ink);
  text-wrap: balance;
}
h1 { font-size: var(--lws-h1); letter-spacing: -.01em; }
h2 { font-size: var(--lws-h2); letter-spacing: -.005em; }
h3 { font-size: var(--lws-h3); }
h4 { font-size: 1.05rem; }

/* ── how headings are capitalised ─────────────────────────────────────────
   Appearance → Type and shape. `as-typed` is the default and emits no rule at
   all, so a site that never touches this renders byte-identically to before —
   the same promise the footer layout makes.

   IT IS A LOOK, NOT CONTENT: nothing rewrites what a client typed, so turning
   the option off puts every heading back exactly as it was. That is the whole
   reason this is CSS rather than a pass over the database, and it is why it
   also fixes headings that arrived from a WordPress import or from genavie —
   they are all just headings by the time they are drawn.

   THESE RULES CANNOT REACH THE ADMIN, and that is deliberate rather than lucky:
   the admin document carries the same data-* attributes on <html> (chrome-top
   emits them for the decoration swatches) but loads admin.css, never this file.
   A client choosing UPPERCASE for their website must not find the admin's own
   headings shouting at them.

   `capitalize` CANNOT LOWER AN EXISTING CAPITAL — CSS has no rule that does —
   so a heading typed in ALL CAPS stays that way under Title Case. Sentence case
   goes the other way and lowercases proper nouns. Both are stated on the
   Appearance screen rather than discovered. */
[data-headings="title"]    :is(h1, h2, h3, h4, h5, h6) { text-transform: capitalize; }
[data-headings="upper"]    :is(h1, h2, h3, h4, h5, h6) { text-transform: uppercase; }
/* Sentence case is two rules because there is no `text-transform: sentence`:
   flatten the lot, then put the first letter back. ::first-letter applies to a
   block container, which every heading is. */
[data-headings="sentence"] :is(h1, h2, h3, h4, h5, h6) { text-transform: lowercase; }
[data-headings="sentence"] :is(h1, h2, h3, h4, h5, h6)::first-letter { text-transform: uppercase; }

p { margin: 0 0 1rem; }
p:last-child { margin-bottom: 0; }

.lead {
  font-size: var(--lws-lead);
  line-height: 1.55;
  color: var(--lws-body);
  text-wrap: pretty;
  max-width: 60ch;
}

.eyebrow {
  display: block;
  margin-bottom: .6rem;
  font-family: var(--lws-font-display);
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: var(--lws-eyebrow-case);
  /* NOT `--lws-brand-soft`, which is a decorative tint — small uppercase text
     drawn in it came out at 2.05:1 on natural-sage. `--lws-eyebrow` is derived
     to clear AA on the page ground and falls back to the tint where the tint
     already did. See lws_theme_derive(). */
  color: var(--lws-eyebrow);
}

.mini-head { margin-top: 1.75rem; }

blockquote { margin: 0; }
cite { font-style: normal; }
hr { border: 0; border-top: 1px solid var(--lws-line); margin: 0; }

/* Long-form copy: the only place descendant selectors are allowed to run
   loose, because the markup comes from an editor or a WordPress import. */
.prose { max-width: 72ch; }
.prose > *:first-child { margin-top: 0; }
.prose > *:last-child { margin-bottom: 0; }
.prose h2 { margin-top: 2rem; }
.prose h3 { margin-top: 1.6rem; }
.prose ul, .prose ol { margin: 0 0 1rem; padding-left: 1.35rem; }
.prose li { margin-bottom: .4rem; }
.prose li::marker { color: var(--lws-brand-soft); }
.prose img { border-radius: var(--lws-radius); margin: 1.5rem 0; }
.prose figure { margin: 1.5rem 0; }
.prose figcaption { font-size: .9rem; color: var(--lws-muted); margin-top: .5rem; }
.prose blockquote {
  margin: 1.5rem 0; padding: .25rem 0 .25rem 1.25rem;
  border-left: 3px solid var(--lws-accent);
  color: var(--lws-ink); font-size: 1.05em;
}
.prose table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; }
.prose th, .prose td { padding: .6rem .75rem; border-bottom: 1px solid var(--lws-line); text-align: left; }
.prose th { font-family: var(--lws-font-display); color: var(--lws-ink); }
.prose code {
  background: var(--lws-surface-3); border-radius: 4px;
  padding: .15em .4em; font-size: .9em;
}
.prose pre { background: var(--lws-surface-3); border-radius: var(--lws-radius); padding: 1rem; overflow-x: auto; }
.prose pre code { background: none; padding: 0; }

/* ── layout ────────────────────────────────────────────────────────────── */

.wrap {
  width: min(100% - 2.5rem, var(--lws-wrap));
  margin-inline: auto;
}
.wrap.narrow { width: min(100% - 2.5rem, 46rem); }

.section { padding-block: var(--lws-section-y); }
.section[data-space="tight"] { padding-block: calc(var(--lws-section-y) * .55); }
.section[data-space="loose"] { padding-block: calc(var(--lws-section-y) * 1.5); }
.section[data-space="none"]  { padding-block: 0; }

/* Grounds — the rhythm cadence. Named for the token they use, so a theme that
   redefines the token re-grounds every section that references it. */
.section[data-ground="surface"]     { background: var(--lws-surface); }
.section[data-ground="surface-2"]   { background: var(--lws-surface-2); }
.section[data-ground="surface-3"]   { background: var(--lws-surface-3); }
.section[data-ground="brand-tint"]  { background: var(--lws-brand-tint); }
.section[data-ground="accent-tint"] { background: var(--lws-accent-tint); }
.section[data-ground="glow"] {
  background:
    radial-gradient(70% 120% at 15% 0%, var(--lws-accent-tint) 0%, transparent 60%),
    radial-gradient(60% 110% at 100% 100%, var(--lws-brand-tint) 0%, transparent 55%),
    var(--lws-surface);
}

.section-head { max-width: 62ch; margin-bottom: 2.5rem; }
.section-head h2 { margin-bottom: .4rem; }
.section-head p { color: var(--lws-body); font-size: 1.05rem; margin-bottom: 0; }

.actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 1.5rem; }
.actions.center { justify-content: center; }

.empty { color: var(--lws-muted); font-style: italic; }

/* Two-column "copy beside media" — used by split, form[split] and others. */
.wrap.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: calc(var(--lws-gap) * 1.5);
  align-items: center;
}
.wrap.split.flip > *:first-child { order: 2; }

/* Scroll reveal. Gated on .lws-js — set by engine.js — so that if the script
   fails to load, every section is simply visible instead of invisible. */
.lws-js .reveal { opacity: 0; transform: translateY(14px); transition: opacity .5s ease, transform .5s ease; }
.lws-js .reveal.in { opacity: 1; transform: none; }

/* The FIRST section paints immediately. Its heading and image are the page's
   largest paint, and holding them at opacity 0 until a deferred script runs
   charges the entrance animation straight to LCP — the hero was fading in
   while the metric's clock ran. A breadcrumbs strip doesn't count as the
   opener; the section under it is still the first thing anyone sees. */
.lws-js main > .section:first-child .reveal,
.lws-js main > [data-block="breadcrumbs"]:first-child + .section .reveal {
  opacity: 1; transform: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .lws-js .reveal { opacity: 1; transform: none; transition: none; }
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* ── buttons ───────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  padding: .8rem 1.5rem;
  border: 1px solid transparent;
  border-radius: var(--lws-btn-radius);
  font-family: var(--lws-font-display);
  font-size: .98rem; font-weight: 600; line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background-color .18s ease, color .18s ease, border-color .18s ease, transform .18s ease, box-shadow .18s ease;
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: none; }

.btn-primary { background: var(--lws-brand); color: var(--lws-on-brand); box-shadow: var(--lws-shadow); }
.btn-primary:hover { background: var(--lws-brand-deep); color: var(--lws-on-brand); box-shadow: var(--lws-shadow-lift); }

.btn-ghost { background: transparent; color: var(--lws-brand); border-color: var(--lws-line); }
.btn-ghost:hover { background: var(--lws-brand-tint); color: var(--lws-brand-deep); border-color: var(--lws-brand-soft); }

.btn-light { background: var(--lws-surface); color: var(--lws-brand); }
.btn-light:hover { background: var(--lws-surface-2); color: var(--lws-brand-deep); }

.pill {
  display: inline-block;
  padding: .4rem 1rem;
  background: var(--lws-brand-tint);
  color: var(--lws-brand-deep);
  border-radius: 999px;
  font-size: .9rem; font-weight: 600;
  text-decoration: none;
  transition: background-color .18s ease, color .18s ease;
}
.pill:hover { background: var(--lws-brand); color: var(--lws-on-brand); }
/* The article count on a topic pill. Its own colour rather than the pill's, so
   the number reads as a quantity and not as part of the topic's name. */
.pill-n {
  margin-left: .15rem;
  opacity: .65;
  font-variant-numeric: tabular-nums;
  font-weight: 500;
}

.badge {
  position: absolute; top: 1rem; right: 1rem;
  padding: .25rem .65rem;
  background: var(--lws-accent); color: var(--lws-on-accent);
  border-radius: 999px;
  font-size: .75rem; font-weight: 600; letter-spacing: .03em;
}

.more {
  display: inline-flex; align-items: center; gap: .35rem;
  margin-top: auto; padding-top: .75rem;
  font-family: var(--lws-font-display); font-weight: 600; font-size: .95rem;
  color: var(--lws-brand);
}
.more span { transition: transform .18s ease; }

/* ── header ────────────────────────────────────────────────────────────── */

.site-header {
  position: sticky; top: 0; z-index: 50;
  /* Two header tokens, not one: a theme that sets header-2 away from header
     gets a subtle depth gradient; a theme that leaves them equal gets a flat
     bar. Same CSS, two looks, no conditional. */
  background: linear-gradient(180deg, var(--lws-header) 0%, var(--lws-header-2) 100%);
  color: var(--lws-on-header);
}
.site-header > .wrap {
  display: flex; align-items: center; gap: 1rem;
  min-height: 4.5rem;
}

.site-logo { display: inline-flex; align-items: center; margin-right: auto; text-decoration: none; }
.site-logo img { width: auto; max-height: 3rem; }
/* A logo whose file could not be measured — in practice every SVG, because
   getimagesize() cannot read one. Such a file often carries a viewBox and no
   width/height, which gives it a ratio but no intrinsic size, and a width:auto
   replaced element like that collapses to nothing inside this flex row. Pinning
   the height is what stops a client's logo silently vanishing; see the note in
   partials/header.php, which decides when to mark it. */
.site-logo img[data-unsized] { height: 3rem; }
.site-wordmark {
  font-family: var(--lws-font-display);
  font-size: 1.35rem; font-weight: 700; letter-spacing: -.01em;
  color: var(--lws-on-header);
}

.site-nav { display: flex; align-items: center; gap: .25rem; }
.site-nav a {
  padding: .55rem .8rem;
  border-radius: var(--lws-btn-radius);
  color: var(--lws-on-header);
  font-family: var(--lws-font-display); font-weight: 600; font-size: .97rem;
  text-decoration: none;
  transition: background-color .18s ease, color .18s ease;
}
.site-nav a:hover { background: rgb(var(--lws-overlay-rgb) / .28); color: var(--lws-on-header); }

.site-nav .nav-cta {
  margin-left: .5rem;
  background: var(--lws-accent); color: var(--lws-on-accent);
  /* Transparent rather than absent: the ghost variant beside it has a real
     1px border, and without this the two buttons in the same nav are 2px
     different in height. */
  border: 1px solid transparent;
}
/* The hover fill is a near-white tint on most palettes, so the label switches
   to the tint's own on-colour. It used to keep `on-accent` — white — and the
   text vanished at 1.1:1 the moment you pointed at it. */
.site-nav .nav-cta:hover { background: var(--lws-accent-soft); color: var(--lws-on-accent-soft); }

/* The outline has to be visible against the HEADER, so it comes from the
   header's text colour, not from `overlay-rgb` — that one is mixed from the
   ink, and a dark border on a dark header bar is no border at all. */
.site-nav .nav-cta-ghost {
  background: transparent; border-color: rgb(var(--lws-on-header-rgb) / .45);
}
/* A ghost button fills in on hover; going to the solid accent keeps the label
   legible without a near-white flash on a dark header. */
.site-nav .nav-cta-ghost:hover {
  background: var(--lws-accent); color: var(--lws-on-accent); border-color: transparent;
}

/* Dropdown. The caret is a SEPARATE control from the link: tapping the label
   follows it, tapping the caret opens the submenu. The builds this came from
   used one tap-to-open-then-tap-to-follow target, which left users unable to
   tell whether their first tap had done anything. */
.has-sub { position: relative; display: flex; align-items: center; }
.sub-toggle {
  display: inline-grid; place-items: center;
  width: 1.9rem; height: 1.9rem; padding: 0;
  background: none; border: 0; border-radius: 50%;
  color: inherit; cursor: pointer;
  transition: background-color .18s ease, transform .18s ease;
}
.sub-toggle:hover { background: rgb(var(--lws-overlay-rgb) / .28); }
.sub-toggle[aria-expanded="true"] { transform: rotate(180deg); }

.sub-menu {
  position: absolute; top: 100%; left: 0; z-index: 10;
  display: none;
  min-width: 15rem; padding: .5rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius);
  box-shadow: var(--lws-shadow-lift);
}
.has-sub:hover .sub-menu,
.has-sub:focus-within .sub-menu,
.sub-toggle[aria-expanded="true"] + .sub-menu { display: block; }

.sub-menu a {
  display: block;
  color: var(--lws-ink);
  font-weight: 500;
}
.sub-menu a:hover { background: var(--lws-surface-2); color: var(--lws-brand-deep); }
.sub-menu .note { display: block; font-size: .85rem; font-weight: 400; color: var(--lws-muted); }
/* A group heading inside a dropdown — `{"heading": "…"}` in the nav. Quiet and
   small on purpose: it has to read as a divider between two kinds of thing, not
   as another item you can click. The rule above it is what does most of that
   work; the first heading in a menu drops its rule and its top margin so a menu
   that opens with a heading has no stray line across its lid. */
.sub-menu .sub-head {
  display: block;
  padding: .5rem .8rem .3rem;
  margin-top: .35rem;
  border-top: 1px solid var(--lws-line);
  color: var(--lws-muted);
  font-family: var(--lws-font-display);
  font-size: .78rem; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
}
.sub-menu .sub-head:first-child { margin-top: 0; border-top: 0; padding-top: .25rem; }
.sub-menu .sub-all {
  margin-top: .25rem; border-top: 1px solid var(--lws-line);
  border-radius: 0 0 calc(var(--lws-radius) - 4px) calc(var(--lws-radius) - 4px);
  color: var(--lws-brand); font-size: .92rem;
}

.header-cta-mobile { display: none; }

.nav-toggle {
  display: none;
  width: 2.75rem; height: 2.75rem; padding: 0;
  background: none; border: 0; cursor: pointer;
  color: var(--lws-on-header);
}
.nav-toggle span, .nav-toggle span::before, .nav-toggle span::after {
  display: block; width: 1.4rem; height: 2px;
  background: currentColor; border-radius: 2px;
  transition: transform .2s ease, opacity .2s ease;
}
.nav-toggle span { margin-inline: auto; position: relative; }
.nav-toggle span::before, .nav-toggle span::after { content: ""; position: absolute; left: 0; }
.nav-toggle span::before { top: -6px; }
.nav-toggle span::after  { top: 6px; }
.nav-toggle[aria-expanded="true"] span { background: transparent; }
.nav-toggle[aria-expanded="true"] span::before { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span::after  { transform: translateY(-6px) rotate(-45deg); }

/* ── footer ────────────────────────────────────────────────────────────── */

.site-footer { background: var(--lws-footer); color: var(--lws-on-footer-dim); }
.site-footer h4 {
  color: var(--lws-on-footer);
  font-size: .85rem; letter-spacing: .09em; text-transform: uppercase;
  margin-bottom: .9rem;
}
.site-footer a { color: var(--lws-on-footer-dim); text-decoration: none; }
.site-footer a:hover { color: var(--lws-on-footer); text-decoration: underline; }
.site-footer ul { list-style: none; margin: 0; padding: 0; }
.site-footer li { margin-bottom: .5rem; }

/* THE TRACK LIST IS BUILT FROM THE LAYOUT, in lws_footer_tracks(), and arrives
   as a custom property — the footer is a stored list of columns now, so how
   many there are and which one is wide is a question only the data can answer.
   The fallback is one equal column, which is what an unstyled grid would give
   anyway; it exists so the rule is still meaningful with the property unset.

   IT IS A PROPERTY AND NOT AN INLINE grid-template-columns because the
   responsive rules further down have to collapse the whole thing at narrow
   widths, and a rule cannot outrank an inline style.

   Historical, and worth keeping: this used to read
   `1.6fr repeat(auto-fit, minmax(min(10rem,100%), 1fr))`, which is INVALID —
   `repeat(auto-fit, …)` is an <auto-track-list> and the grammar only admits
   <fixed-size> tracks beside it, so a bare `1.6fr` (a <flex>) voided the whole
   declaration. It was dropped wholesale, leaving `display: grid` with no
   columns, and the "Columns" footer stacked everything down one narrow left
   column on every site above 1024px — while the media query's plain `1fr 1fr`
   below that width was valid and did apply, so the bug inverted with width.
   Both track shapes below are minmax(<fixed-breadth>, …) for that reason. */
.footer-cols {
  display: grid;
  grid-template-columns: var(--footer-tracks, minmax(min(10rem, 100%), 1fr));
  gap: var(--lws-gap);
  padding-block: calc(var(--lws-section-y) * .8);
}
/* `.footer-col-wide`, not `.footer-brand`: the wide column is whichever one the
   client made wide, and it need not hold the logo — or exist at all. */
/* THE LOGO, and only the logo — `>` is load-bearing. As a descendant selector
   this capped EVERY picture in the wide column at 3.25rem tall, so the first
   widget to draw one that was not a logo came out squashed: a 256px square
   certification mark rendered 104x52, its own width rule winning and this
   max-height surviving underneath it. Nothing about that looks like a CSS
   collision — it looks like a badly exported image. The logo widget emits its
   <img> as a direct child of the column; every other widget wraps its own
   markup, so the child combinator says exactly what this rule always meant. */
.footer-col-wide > img { max-height: 3.25rem; width: auto; margin-bottom: 1rem; }
.footer-col-wide .site-wordmark { color: var(--lws-on-footer); margin-bottom: 1rem; display: block; }
.footer-col-wide p { color: var(--lws-on-footer-dim); max-width: 34ch; }

/* THE BRAND COLUMN ON A LIGHT PANEL — Appearance → "Logo on a light panel".
   For a client whose logo is DARK INK: the footer stands on --lws-footer, dark
   on every palette we ship, and a dark-ink mark simply disappears on it. The
   footer widget wants `logo.dark` (a light-ink version) and a client who has
   not got one has nothing to give it — recolouring somebody's logo for them is
   not this engine's call. So the ground is what moves.
   Everything inside flips to ink, because the column no longer stands on
   --lws-footer and the derived on-footer colours would be invisible here. */
:root[data-footer-plate="true"] .site-footer .footer-col-wide {
  /* THE TOKENS ARE RE-POINTED, not the rules overridden. Everything the footer
     draws — headings, definition lists, links, the dim body copy — reads
     --lws-on-footer or its -dim companion, so redefining those two here fixes
     every descendant at once, including widgets added later.
     Enumerating the rules instead is how the first cut of this shipped with
     invisible opening hours: it flipped p, a and .site-wordmark and missed the
     <h4> and <dt> the hours widget uses. */
  --lws-on-footer: var(--lws-ink);
  --lws-on-footer-dim: var(--lws-body);
  background: var(--lws-surface-2);
  color: var(--lws-ink);
  border-radius: var(--lws-radius-lg);
  padding: 1.6rem;
  margin-top: -.4rem;
}
:root[data-footer-plate="true"] .site-footer .footer-col-wide .footer-social a { color: var(--lws-brand); }
:root[data-footer-plate="true"] .site-footer .footer-col-wide .footer-social a:hover { color: var(--lws-accent); }
/* AND THE GOOGLE LINK, which sits beside them and had neither half of that pair.
   On the plate it inherited --lws-ink, and this site's ink and brand are the
   same aubergine — so hovering set the circle's background to exactly the
   colour the G was drawn in and the glyph disappeared into its own button. It
   is one widget with the socials, on the same ground, so it gets the same
   treatment rather than a special case. The label moves with the icon: a link
   whose glyph changes colour while its words do not reads as a fault. */
:root[data-footer-plate="true"] .site-footer .footer-col-wide .footer-google a { color: var(--lws-brand); }
:root[data-footer-plate="true"] .site-footer .footer-col-wide .footer-google a:hover { color: var(--lws-accent); }
/* The booking button stands on the PLATE, not on the footer, so `btn-light`'s
   near-white background would be invisible on cream. */
:root[data-footer-plate="true"] .site-footer .footer-col-wide .btn-light {
  background: var(--lws-brand); color: var(--lws-on-brand);
}
:root[data-footer-plate="true"] .site-footer .footer-col-wide .btn-light:hover {
  background: var(--lws-brand-deep); color: var(--lws-on-brand);
}
.footer-cta { margin-top: 1.25rem; }
/* `.site-footer a` is (0,1,1) and `.btn-light` only (0,1,0), so without this the
   booking button drew its label in the dim footer-link grey on the button's own
   near-white background — legible in the sense that it was technically there. */
.site-footer .btn-light { color: var(--lws-brand); }
.site-footer .btn-light:hover { color: var(--lws-brand-deep); text-decoration: none; }

/* STACKED WIDGETS NEED AIR, and only when they are actually stacked. Nothing in
   a derived layout has a sibling of these types — hours and each link group get
   a column to themselves — so this changes no existing site, and only earns its
   place once a client drags two link groups into one column and finds the
   second heading sitting on the first one's last link. */
.footer-col > * + .footer-menu,
.footer-col > * + .footer-hours { margin-top: 1.5rem; }
.footer-contact { margin-top: 1.25rem !important; }
.footer-contact address { font-style: normal; }

.footer-social { display: flex; gap: .6rem; margin-top: 1.25rem; }
.footer-social a {
  display: grid; place-items: center;
  width: 2.4rem; height: 2.4rem;
  /* THE DISC IS THERE BEFORE YOU TOUCH IT. It used to be a 1px border and
     nothing else, so the button only became a button under the pointer — which
     reads as the page having changed its mind rather than as a hover, and on a
     phone there is no hover at all, so the shape never appeared. A ground this
     faint still sits quietly on the footer; it just exists. */
  background: rgb(var(--lws-overlay-rgb) / .16);
  border: 1px solid rgb(var(--lws-overlay-rgb) / .5);
  border-radius: 50%;
  transition: background-color .18s ease, border-color .18s ease;
}
.footer-social a:hover { background: var(--lws-brand); border-color: var(--lws-brand); }
/* Social marks are solid glyphs, not line icons — they opt out of the stroke default. */
.footer-social svg { width: 1.1rem; height: 1.1rem; fill: currentColor; stroke: none; }

/* "Find us on Google" — a labelled link, not another anonymous glyph, because
   it is an instruction rather than an account.

   ITS MARK WEARS THE SAME DISC AS THE SOCIAL ONES, and that is alignment rather
   than decoration. Both rows started at the same left edge, but a glyph centred
   inside a 2.4rem circle and a bare 1rem glyph beside it put their INK in two
   different places — measured 22px apart on wish's screenshot — so the block
   read as two things that had missed each other. Same disc, same size, same
   gap: one column, and the eye can follow it down. */
.footer-google { margin: .6rem 0 0; font-size: .88rem; }
.footer-google a { display: inline-flex; align-items: center; gap: .6rem; }
.footer-google svg {
  width: 2.4rem; height: 2.4rem; padding: .65rem; box-sizing: border-box;
  flex: 0 0 auto;
  fill: currentColor; stroke: none;
  background: rgb(var(--lws-overlay-rgb) / .16);
  border: 1px solid rgb(var(--lws-overlay-rgb) / .5);
  border-radius: 50%;
  transition: background-color .18s ease, border-color .18s ease;
}
.footer-google a:hover svg { background: var(--lws-brand); border-color: var(--lws-brand); }

/* CERTIFICATION MARKS.
   A row of tiles, each capped rather than stretched: these arrive at whatever
   size the awarding body exports at — RGCC's is 1226px square — and a badge
   allowed to fill its column becomes the largest thing in the footer, which is
   not the claim it is making. `width` on the <li> rather than the <img> so a
   tall mark and a wide one occupy the same slot and the row stays level.
   min() keeps it honest on a 320px screen, where 6.5rem is already a third of
   the viewport. */
.footer-badges {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .75rem; margin: 1.25rem 0 0; padding: 0; list-style: none;
}
.footer-badges li { width: min(6.5rem, 28vw); }
.footer-badges img { display: block; width: 100%; height: auto; }
/* A linked mark is the only one that is interactive, so it is the only one
   that reacts. Opacity rather than a filter: these are supplied artwork and
   tinting somebody's certification mark is not ours to do. */
.footer-badges a { display: block; transition: opacity .15s ease; }
.footer-badges a:hover { opacity: .82; }
@media (prefers-reduced-motion: reduce) { .footer-badges a { transition: none; } }

:root[data-footer="minimal"] .footer-google { text-align: center; }
:root[data-footer="minimal"] .footer-google a { justify-content: center; }

.footer-hours dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: .35rem .85rem; }
.footer-hours dt { color: var(--lws-on-footer); font-weight: 600; }
.footer-hours dd { margin: 0; }

/* The small print — Settings → Footer. Sits above the copyright bar, sized and
   dimmed so it reads as a footnote: present for anyone who wants it, not
   competing with the reason somebody came to the site. Constrained to a reading
   measure because a disclaimer run edge-to-edge at 1220px is a line nobody can
   track back to the start of. */
.footer-note {
  border-top: 1px solid rgb(var(--lws-overlay-rgb) / .45);
  padding-block: 1rem;
}
.footer-note p {
  margin: 0;
  max-width: 90ch;
  font-size: .8rem;
  line-height: 1.6;
  color: var(--lws-on-footer-dim);
}

.footer-bottom {
  border-top: 1px solid rgb(var(--lws-overlay-rgb) / .45);
  padding-block: 1.25rem;
  font-size: .9rem;
}
.footer-bottom .wrap { display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; justify-content: space-between; }
.footer-bottom a { margin-right: 1rem; }

/* ═══ CHROME VARIANTS ═══════════════════════════════════════════════════════
   Header and footer treatments, chosen once per site in theme.json → chrome and
   emitted as data-* on <html> (data-header, data-footer, data-sticky). Same
   idea as block variants: one attribute, one CSS block, no markup fork — except
   the utility topbar, which is markup because it adds content.

   Every header variant collapses to the SAME hamburger on mobile; the variants
   are desktop treatments, so the distinctive rules are scoped to >1024px. */

/* ── gallery lightbox ──────────────────────────────────────────────────── */

dialog.lightbox {
  border: 0; padding: 0; background: none; max-width: min(92vw, 70rem); max-height: 92vh;
}
dialog.lightbox::backdrop { background: rgba(10, 12, 16, .82); }
dialog.lightbox img {
  display: block; max-width: 100%; max-height: 92vh;
  border-radius: var(--lws-radius); cursor: zoom-out;
}

/* ── consent note ──────────────────────────────────────────────────────── */

.consent-note {
  position: fixed; left: 1rem; right: 1rem; bottom: 1rem; z-index: 60;
  max-width: 30rem; margin-inline: auto;
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
  padding: .75rem 1rem; font-size: .88rem;
  background: var(--lws-surface); color: var(--lws-body);
  border: 1px solid var(--lws-line); border-radius: var(--lws-radius);
  box-shadow: 0 8px 30px rgba(0, 0, 0, .18);
}
.consent-note p { margin: 0; flex: 1 1 14rem; }
.consent-note button {
  border: 1px solid var(--lws-line); border-radius: var(--lws-radius);
  background: var(--lws-brand); color: var(--lws-on-brand);
  padding: .35rem .9rem; font: inherit; cursor: pointer;
}
.consent-note button + button { background: none; color: var(--lws-muted); }

/* ── preview banner ────────────────────────────────────────────────────── */

.site-preview-note {
  background: repeating-linear-gradient(-45deg,
    var(--lws-surface-3), var(--lws-surface-3) 12px,
    var(--lws-surface) 12px, var(--lws-surface) 24px);
  color: var(--lws-ink); text-align: center;
  font-size: .85rem; font-weight: 600; padding: .45rem 1rem;
  border-bottom: 1px solid var(--lws-line);
}
/* The notice gained structure — a state word, a sentence, and sometimes a link
   into the admin — when a signed-in editor became able to open a draft at its
   own address. It introduces NO new colour pair on purpose: a band that gains a
   text colour gains a row in lws_theme_contrast_report(), and this needs
   neither, because the state word is separated by weight and tracking rather
   than by ink. */
.site-preview-note strong { text-transform: uppercase; letter-spacing: .08em; }
.site-preview-note a      { color: inherit; text-underline-offset: 2px; }

/* ── announcement bar ──────────────────────────────────────────────────── */

/* Above everything, in the brand colour so it always belongs to the palette.
   Hidden the moment engine.js finds the visitor's dismissal hash — until
   then it renders, which is the right default for a cached page. */
.site-announce {
  background: var(--lws-brand);
  color: var(--lws-on-brand);
  font-size: .9rem;
}
.site-announce.is-dismissed { display: none; }
.site-announce .wrap {
  display: flex; align-items: center; gap: .75rem;
  min-height: 2.4rem; padding-top: .35rem; padding-bottom: .35rem;
}
.site-announce p { margin: 0; flex: 1 1 auto; }
.site-announce a { color: inherit; font-weight: 600; text-underline-offset: 2px; }
.announce-close {
  flex: 0 0 auto; border: 0; background: none; cursor: pointer;
  color: inherit; font-size: 1.15rem; line-height: 1; padding: .25rem .4rem;
  opacity: .8;
}
.announce-close:hover { opacity: 1; }

/* ── hours block ───────────────────────────────────────────────────────── */

/* The same rows the footer widget draws, at content size. */
.hours-list {
  margin: 0; max-width: 34rem;
  display: grid; grid-template-columns: auto 1fr; gap: .45rem 1.5rem;
}
.hours-list dt { font-weight: 600; }
.hours-list dd { margin: 0; color: var(--lws-muted); }
.hours-card {
  padding: 1.25rem 1.5rem; background: var(--lws-surface);
  border: 1px solid var(--lws-line); border-radius: var(--lws-radius);
}

/* ── utility topbar ────────────────────────────────────────────────────── */

.site-topbar {
  background: var(--lws-footer);
  color: var(--lws-on-footer-dim);
  font-size: .85rem;
}
.site-topbar .wrap {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; min-height: 2.4rem; flex-wrap: wrap;
}
.topbar-contact { display: flex; gap: 1.25rem; flex-wrap: wrap; }
.site-topbar a { color: var(--lws-on-footer-dim); text-decoration: none; }
.site-topbar a:hover { color: var(--lws-on-footer); }
.topbar-social { display: flex; gap: .6rem; }
.topbar-social svg { width: 1rem; height: 1rem; fill: currentColor; stroke: none; }

/* ── sticky toggle ─────────────────────────────────────────────────────── */

:root[data-sticky="false"] .site-header { position: static; }

/* ── header: centered ──────────────────────────────────────────────────── */
/* Logo on its own centred row, nav centred beneath it. A distinctly more
   formal, boutique look than logo-left. Desktop only — mobile stays hamburger. */
@media (min-width: 1025px) {
  :root[data-header="centered"] .site-header > .wrap {
    flex-direction: column; gap: .4rem; padding-block: .9rem; min-height: 0;
  }
  :root[data-header="centered"] .site-logo { margin-right: 0; }
  :root[data-header="centered"] .site-logo img { max-height: 3.5rem; }
  :root[data-header="centered"] .site-nav { justify-content: center; flex-wrap: wrap; }
}

/* ── header: minimal ───────────────────────────────────────────────────── */
/* The nav lives behind the menu button at ALL widths — logo left, primary CTA
   and menu button right. Modern, uncluttered; good for a site whose nav would
   otherwise crowd the bar. Reuses the mobile collapsed-nav machinery. */
:root[data-header="minimal"] .nav-toggle { display: block; }
:root[data-header="minimal"] .header-cta-mobile { display: inline-flex; padding: .6rem 1.2rem; font-size: .95rem; }
:root[data-header="minimal"] .site-logo { margin-right: auto; }
:root[data-header="minimal"] .site-nav {
  position: absolute; top: 100%; left: 0; right: 0;
  display: none; flex-direction: column; align-items: stretch; gap: 0;
  padding: .75rem;
  background: var(--lws-header);
  border-top: 1px solid rgb(var(--lws-overlay-rgb) / .4);
  box-shadow: var(--lws-shadow-lift);
  max-height: calc(100vh - 4.5rem); overflow-y: auto;
}
:root[data-header="minimal"] .site-header { position: relative; }
:root[data-sticky="true"][data-header="minimal"] .site-header { position: sticky; }
:root[data-header="minimal"] .site-nav.open { display: flex; }
:root[data-header="minimal"] .site-nav > a,
:root[data-header="minimal"] .has-sub > a { padding: .8rem 1rem; }
:root[data-header="minimal"] .has-sub { flex-wrap: wrap; justify-content: space-between; }
:root[data-header="minimal"] .has-sub > a { flex: 1; }
:root[data-header="minimal"] .sub-menu {
  position: static; display: none; width: 100%; margin-top: .25rem;
  background: rgb(var(--lws-overlay-rgb) / .3); border: 0; box-shadow: none;
}
:root[data-header="minimal"] .sub-menu a { color: var(--lws-on-header); }
:root[data-header="minimal"] .sub-toggle[aria-expanded="true"] + .sub-menu { display: block; }

/* ── footer: compact ───────────────────────────────────────────────────── */
/* Brand + contact on the left, one tidy link row on the right — no columns.
   For sites with little to list, where the tall columns footer looks empty. */
:root[data-footer="compact"] .footer-cols {
  grid-template-columns: 1fr auto; align-items: start; gap: var(--lws-gap);
}
/* THE HOURS ARE GONE FROM THE DATA, so there is no `display: none` here any
   more. There used to be, and it never worked: it scored (0,3,0) and the
   right-align rule below it scored (0,4,1), so the hours compact is documented
   to drop were un-hidden by the rule meant to align everything beside the
   brand. Compact had shown hours on every site that had any, since it shipped.
   Now `compact` is a preset that simply does not contain an hours widget — see
   lws_footer_layout_default() — which also means no empty grid column is left
   behind where the hidden one used to be. */
/* A ROW, which is what "a tidy link row on the right" has always claimed to be.
   This was `flex-direction: column`, and compact's preset gave each link group
   a column of its own — two ways of saying the same wrong thing, because
   compact's grid only has two tracks, so a third and fourth group wrapped onto
   a second row. The preset now puts every group in this one column (see
   lws_footer_layout_default) and this lays them out across it. */
:root[data-footer="compact"] .footer-cols > div:not(.footer-col-wide) {
  display: flex; flex-direction: row; flex-wrap: wrap;
  justify-content: flex-end; align-items: flex-start;
  gap: var(--lws-gap);
}
/* ...AND THE STACKING MARGIN HAS TO GO WITH IT. `.footer-col > * + .footer-menu`
   puts 1.5rem above every group after the first, which is right when a column
   stacks its blocks and wrong the moment the same column becomes a row: it does
   not space the groups apart, it drops the second and third 24px BELOW the
   first. Three link groups whose headings sat on two different baselines —
   visible as "the footer columns aren't lined up", and invisible to anything
   that only reads the markup. The row's own `gap` already does the spacing this
   margin was there for. */
:root[data-footer="compact"] .footer-cols > div:not(.footer-col-wide) > * + .footer-menu,
:root[data-footer="compact"] .footer-cols > div:not(.footer-col-wide) > * + .footer-hours {
  margin-top: 0;
}
@media (max-width: 767px) {
  :root[data-footer="compact"] .footer-cols { grid-template-columns: 1fr; }
  :root[data-footer="compact"] .footer-cols > div:not(.footer-col-wide) { justify-content: flex-start; }
}

/* ── footer: minimal ───────────────────────────────────────────────────── */
/* One centred row: wordmark, a short link list, social, then the copyright.
   The link columns and hours are dropped — everything the brand block carries
   is enough. For very simple sites. */
:root[data-footer="minimal"] .footer-cols {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 1rem;
}
/* Nothing is hidden here either: `minimal` is a preset holding the one wide
   column, so the link columns and hours it used to draw-then-hide are simply
   not in the layout. */
:root[data-footer="minimal"] .footer-col-wide { max-width: 40rem; }
:root[data-footer="minimal"] .footer-col-wide p { margin-inline: auto; }
:root[data-footer="minimal"] .footer-contact { justify-content: center; display: flex; gap: 1.25rem; }
:root[data-footer="minimal"] .footer-social { justify-content: center; }
:root[data-footer="minimal"] .footer-bottom .wrap { justify-content: center; }

/* ═══ SHARED SECTION FURNITURE ═════════════════════════════════════════════
   Things more than one block draws. They live above the blocks rather than
   inside whichever one needed them first, because the second block to want a
   tick list must not get a slightly different tick. */

/* Text for a screen reader and nobody else. There were three hand-rolled
   copies of this declaration in here before it had a name. */
.lws-sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ── tick lists ────────────────────────────────────────────────────────────
   The bullets under a `split`, the lines inside a pricing plan. The mark is
   the shared `check` icon at text size, and a line that is NOT included keeps
   its full contrast on the text — dimming what a plan lacks is how a reader
   stops noticing the thing they most need to see. Only the mark goes quiet. */
.tick-list { list-style: none; margin: 1.25rem 0 0; padding: 0; display: grid; gap: .6rem; }
.tick-list li { display: flex; align-items: flex-start; gap: .6rem; color: var(--lws-body); }
.tick-list svg {
  flex: none; width: 1.15rem; height: 1.15rem; margin-top: .18rem;
  color: var(--lws-brand); stroke-width: 2.4;
}
.tick-list li.off svg { color: var(--lws-muted); stroke-width: 2; }
.tick-list li.off span { color: var(--lws-muted); }

/* Small print under a table or a set of plans. */
.section-note {
  margin-top: 1.5rem; max-width: 60ch;
  font-size: .9rem; color: var(--lws-muted);
}

/* ── device frames ─────────────────────────────────────────────────────────
   A screenshot dropped on a page reads as a rectangle of small text. The same
   picture inside a window or a phone body reads as a product — which is the
   whole reason a client asks to show their portal, their menu or their app.

   Every one of these is drawn AROUND the client's own image with no crop and
   no aspect-ratio: the frame takes its height from the picture. That is why
   the img rules below have to unset `.hero-img`'s ceiling and radius — those
   are right for a photograph filling a hero and wrong for a screen, and the
   frame is a descendant so those rules still match. */
.lws-frame { display: block; position: relative; max-width: 100%; margin-inline: auto; }
.lws-frame img {
  display: block; width: 100%; height: auto;
  max-height: none; object-fit: contain;
  border-radius: 0; box-shadow: none;
}
.lws-frame-screen { display: block; overflow: hidden; }

.lws-frame[data-frame="browser"] {
  padding-top: 2.1rem;
  background: var(--lws-surface-2);
  border: 1px solid var(--lws-line);
  border-radius: calc(var(--lws-radius) + .35rem);
  box-shadow: var(--lws-shadow-lift);
  overflow: hidden;
}
.lws-frame-bar {
  position: absolute; inset: 0 0 auto 0; height: 2.1rem;
  display: flex; align-items: center; gap: .6rem;
  padding-inline: .8rem;
  border-bottom: 1px solid var(--lws-line);
}
.lws-frame-dots { display: flex; gap: .3rem; flex: none; }
.lws-frame-dots i {
  width: .5rem; height: .5rem; border-radius: 999px;
  background: var(--lws-line); filter: brightness(.92);
}
/* A drawing of an address bar, not a place to read a URL out of: putting the
   site's real address here would be a second, unmaintained copy of it. */
.lws-frame-url {
  flex: 1; max-width: 14rem; height: .85rem;
  border-radius: 999px; background: var(--lws-surface-3);
}

.lws-frame[data-frame="phone"] {
  max-width: 20rem;
  padding: .55rem;
  background: var(--lws-ink);
  border-radius: 2.2rem;
  box-shadow: var(--lws-shadow-lift);
}
.lws-frame[data-frame="phone"] .lws-frame-screen { border-radius: 1.7rem; }
.lws-frame-notch {
  position: absolute; top: .55rem; left: 50%; transform: translateX(-50%);
  z-index: 2;
  width: 32%; height: 1.1rem;
  background: var(--lws-ink);
  border-radius: 0 0 .8rem .8rem;
}

.lws-frame[data-frame="tablet"] {
  max-width: 34rem;
  padding: .9rem;
  background: var(--lws-ink);
  border-radius: 1.4rem;
  box-shadow: var(--lws-shadow-lift);
}
.lws-frame[data-frame="tablet"] .lws-frame-screen { border-radius: .7rem; }

/* ═══ BLOCKS ═══════════════════════════════════════════════════════════════
   Each block owns one section below. Variants are attribute selectors on the
   section, never extra class names — one new CSS block per variant, nothing
   else to wire up. */

/* ── hero ──────────────────────────────────────────────────────────────── */

/* The hero sets its own height on the wrap and zeroes the inherited .section
   padding — otherwise the two stack and open a dead band under the buttons.
   One source for the hero's vertical rhythm, not two. */
.hero { position: relative; overflow: hidden; background: var(--lws-surface); padding-block: 0; }
.hero > .wrap {
  display: grid;
  grid-template-columns: 1.05fr .95fr;
  gap: calc(var(--lws-gap) * 1.5);
  align-items: center;
  padding-block: calc(var(--lws-section-y) * 1.15);
}
/* A split hero whose image hasn't been set yet must not reserve an empty
   second column — half a blank screen is the first thing a client sees while
   they are still filling the page in. */
.hero > .wrap:not(:has(.hero-media)) { grid-template-columns: 1fr; }

.hero-copy { position: relative; z-index: 2; }
.hero-copy h1 { margin-bottom: .75rem; }
.hero-accent { color: var(--lws-brand-soft); }
.hero-meta { margin-top: 1.25rem; font-size: .95rem; color: var(--lws-muted); }
.hero-rating { margin-top: 1.25rem; }
.hero-rating .rating { margin-bottom: 0; }

/* ── how tall a photo is allowed to be ─────────────────────────────────────
   Nothing below sets an aspect-ratio, on purpose: a landscape photo should
   render exactly as it was cropped. What they set is a CEILING, because
   without one the client's upload decides the height of a designed section.

   The arithmetic, on FunctionalFocus's 1280px wrap: a split hero's media
   column is ~588px. A landscape 1024x683 lands at 392px and is never touched.
   A portrait 1024x1536 — which is what a phone photo of a person is — lands
   at 882px, against roughly 400px of copy beside it. The section stops being
   a hero and becomes a photograph with a caption. `object-fit: cover` was
   already here waiting for a box to crop into; this gives it one.

   The crop is biased upward because faces sit in the top half of a portrait
   and centre-cropping beheads them. */
:root {
  --lws-media-h: min(30rem, 62vh);
  --lws-media-focus: center 30%;
}

.hero-media { position: relative; }
.hero-media .hero-img {
  width: 100%;
  max-height: var(--lws-media-h);
  border-radius: var(--lws-radius-lg);
  box-shadow: var(--lws-shadow-lift);
  object-fit: cover;
  object-position: var(--lws-media-focus);
}
.hero-img-accent {
  position: absolute; right: -1.5rem; bottom: -1.5rem;
  width: 46%;
  /* the accent is a detail on top of the main photo, not a second hero image —
     a portrait upload here is 405px tall at 46% width and climbs straight out
     of the section */
  max-height: 58%;
  object-fit: cover;
  object-position: var(--lws-media-focus);
  border: .5rem solid var(--lws-surface);
  border-radius: var(--lws-radius);
  box-shadow: var(--lws-shadow-lift);
}
.hero-badge {
  position: absolute; top: 1.25rem; left: -1rem;
  padding: .55rem 1.1rem;
  background: var(--lws-accent); color: var(--lws-on-accent);
  border-radius: 999px;
  font-family: var(--lws-font-display); font-weight: 700; font-size: .9rem;
  box-shadow: var(--lws-shadow);
}

.hero[data-variant="split-flip"] > .wrap > .hero-copy { order: 2; }
.hero[data-variant="centered"] > .wrap { grid-template-columns: 1fr; justify-items: center; text-align: center; }
.hero[data-variant="centered"] .lead { margin-inline: auto; }
.hero[data-variant="centered"] .actions { justify-content: center; }
.hero[data-variant="bare"] > .wrap { grid-template-columns: 1fr; }

/* Showcase: centred copy, picture wide beneath it.
   `:has(.hero-media)` above keeps a split hero from reserving an empty second
   column, so this variant has to say one column explicitly — it HAS media and
   would otherwise inherit the two-column rule and put a full-width picture in
   half a row. */
.hero[data-variant="showcase"] > .wrap {
  grid-template-columns: 1fr;
  justify-items: center; text-align: center;
  gap: calc(var(--lws-gap) * 2);
}
.hero[data-variant="showcase"] .hero-copy { max-width: 46rem; }
.hero[data-variant="showcase"] .lead { margin-inline: auto; }
.hero[data-variant="showcase"] .actions { justify-content: center; }
.hero[data-variant="showcase"] .hero-media { width: 100%; }
/* The ceiling that stops a portrait upload owning a split hero is wrong here:
   this variant exists to show one picture at full width, and cropping it to
   30rem is the opposite of that. */
.hero[data-variant="showcase"] .hero-media .hero-img {
  max-height: none; object-fit: contain;
}
/* A framed screenshot is the common case for this layout and it is the frame,
   not the picture, that carries the lift. */
.hero[data-variant="showcase"] .hero-media > .lws-frame { width: 100%; }
.hero[data-variant="showcase"] .hero-img-accent { display: none; }

/* Bleed: the photo becomes the section background with a readability scrim.
   The scrim is not optional — it is the difference between a hero that reads
   and one that is white-on-white on a light photo. */
.hero[data-variant="bleed"] > .wrap { grid-template-columns: 1fr; padding-block: calc(var(--lws-section-y) * 1.2); }
.hero[data-variant="bleed"] .hero-media {
  position: absolute; inset: 0; z-index: 0;
}
/* A bleed hero's photo IS the section background — the height ceiling above
   would leave a band of bare surface under it. */
.hero[data-variant="bleed"] .hero-media .hero-img {
  width: 100%; height: 100%; max-height: none;
  border-radius: 0; box-shadow: none;
}
.hero[data-variant="bleed"] .hero-media::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, rgb(var(--lws-scrim-rgb) / .82) 0%, rgb(var(--lws-scrim-rgb) / .45) 100%);
}
.hero[data-variant="bleed"] .hero-img-accent, .hero[data-variant="bleed"] .hero-badge { display: none; }
.hero[data-variant="bleed"] .hero-copy { max-width: 40rem; }
.hero[data-variant="bleed"] h1,
.hero[data-variant="bleed"] .lead,
.hero[data-variant="bleed"] .hero-meta { color: #fff; }
.hero[data-variant="bleed"] .eyebrow { color: var(--lws-accent-soft); }
.hero[data-variant="bleed"] .btn-ghost { color: #fff; border-color: rgb(255 255 255 / .5); }
.hero[data-variant="bleed"] .btn-ghost:hover { background: rgb(255 255 255 / .12); color: #fff; }

/* ── page hero ─────────────────────────────────────────────────────────── */

.page-hero { background: var(--lws-surface-2); }
.page-hero > .wrap { display: grid; gap: var(--lws-gap); }
.page-hero h1 { margin-bottom: .5rem; }
.page-hero .lead { margin-top: .75rem; }
.page-hero[data-variant="centered"] > .wrap { justify-items: center; text-align: center; }
.page-hero[data-variant="centered"] .lead { margin-inline: auto; }
.page-hero[data-variant="media"] > .wrap { grid-template-columns: 1fr .8fr; align-items: center; }
.page-hero-media img {
  width: 100%;
  max-height: var(--lws-media-h);
  object-fit: cover; object-position: var(--lws-media-focus);
  border-radius: var(--lws-radius-lg); box-shadow: var(--lws-shadow);
}

/* ── breadcrumbs ───────────────────────────────────────────────────────── */

.crumbs { padding-block: 1rem; }
.crumbs ol { display: flex; flex-wrap: wrap; gap: .35rem; list-style: none; margin: 0; padding: 0; font-size: .9rem; }
.crumbs li:not(:last-child)::after { content: "/"; margin-left: .35rem; color: var(--lws-muted); }
.crumbs a { color: var(--lws-muted); text-decoration: none; }
.crumbs a:hover { color: var(--lws-brand); text-decoration: underline; }
.crumbs [aria-current] { color: var(--lws-ink); font-weight: 600; }
.crumbs[data-variant="boxed"] .wrap > nav {
  padding: .6rem 1rem;
  background: var(--lws-surface-2);
  border-radius: var(--lws-radius);
}

/* ── richtext ──────────────────────────────────────────────────────────── */

.section[data-block="richtext"][data-variant="narrow"] .prose { max-width: 58ch; margin-inline: auto; }
/* The heading has to move with it. Centring only the prose leaves the section
   heading hard left and the paragraph under it indented by a third of the
   page, which reads as a broken float rather than a narrow measure. */
.section[data-block="richtext"][data-variant="narrow"] .section-head { max-width: 58ch; margin-inline: auto; }
.section[data-block="richtext"][data-variant="wide"] .prose { max-width: none; }
.section[data-block="richtext"][data-variant="two-col"] .prose { max-width: none; columns: 2; column-gap: calc(var(--lws-gap) * 1.5); }
.section[data-block="richtext"][data-variant="two-col"] .prose > * { break-inside: avoid; }

/* ── html (imported markup) ────────────────────────────────────────────── */

/* The wp-compat shim keys off [data-source="wp-import"] and lives in its own
   optional stylesheet, so a greenfield site loads none of it. */
.lws-html { max-width: none; }
.lws-html img { height: auto; }

/* ── split ─────────────────────────────────────────────────────────────── */

.split-media img {
  width: 100%;
  max-height: var(--lws-media-h);
  object-fit: cover; object-position: var(--lws-media-focus);
  border-radius: var(--lws-radius-lg); box-shadow: var(--lws-shadow);
}

/* The framed variant is deliberately a PORTRAIT — a head-and-shoulders shot
   beside a paragraph — so it gets a taller ceiling than a landscape split,
   but a ceiling all the same. */
.portrait-frame { margin: 0; position: relative; }
.portrait-frame img {
  width: 100%;
  max-height: calc(var(--lws-media-h) * 1.25);
  object-fit: cover; object-position: var(--lws-media-focus);
  border-radius: var(--lws-radius-lg);
  box-shadow: var(--lws-shadow-lift);
}
.portrait-frame figcaption {
  position: absolute; left: 1.25rem; bottom: 1.25rem;
  padding: .7rem 1.1rem;
  background: var(--lws-surface);
  border-radius: var(--lws-radius);
  box-shadow: var(--lws-shadow);
}
.portrait-frame figcaption strong { display: block; color: var(--lws-ink); font-family: var(--lws-font-display); }
.portrait-frame figcaption span { font-size: .88rem; color: var(--lws-muted); }

.section[data-block="split"][data-variant="overlap"] .split-media img { margin-bottom: -3rem; }

/* ── a section's side column ───────────────────────────────────────────────
   Photographs, a video or an embedded form beside a section's copy, rather
   than stacked underneath it as a separate section. See lws_aside() in
   blocks.php. Deliberately NOT sharing .split-media's rules: that class is the
   one-big-photo treatment, and inheriting it is how a badge got squashed. */
.section-aside { min-width: 0; }

/* Two across by default, which makes four photographs a 2x2. Three across for
   an odd three, so the grid does not sit with a hole in the corner. Explicit
   counts rather than auto-fit: auto-fit would put four in a single row in a
   wide column, and "a 2x2 grid" is the thing being asked for. */
.aside-grid {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: .75rem; margin: 0; padding: 0; list-style: none;
}
.aside-grid[data-count="1"] { grid-template-columns: 1fr; }
.aside-grid[data-count="3"],
.aside-grid[data-count="5"],
.aside-grid[data-count="6"] { grid-template-columns: repeat(3, 1fr); }
/* Square crops, so photographs shot at different ratios still line up. The
   focus point is the same custom property the big media uses, so a client who
   has set one gets it honoured here too. */
.aside-grid img {
  display: block; width: 100%; aspect-ratio: 1;
  object-fit: cover; object-position: var(--lws-media-focus);
  border-radius: var(--lws-radius); box-shadow: var(--lws-shadow);
}

/* A video in a side column is already about half the width it would have had
   as its own section, which is the whole point of moving it here. */
.section-aside .aside-embed { width: 100%; }

/* An embedded form is somebody else's markup: it is given room and left alone.
   The iframe is width-constrained because a third-party embed that declares a
   fixed pixel width will otherwise push the grid wider than the page. */
.aside-raw { min-width: 0; }
.aside-raw iframe { max-width: 100%; border: 0; }

.aside-caption { font-size: .9rem; color: var(--lws-muted); margin: .6rem 0 0; }

/* A section that has BOTH a photo and an aside keeps the photo with the copy —
   see the split renderer. Rounded and shadowed like the media column so it
   reads as the same photograph moved, not a different kind of thing, but with
   no cover crop: down here it is sized by the column rather than filling a
   panel, and cropping it would cut a face off for no reason. */
.split-inline-media { margin: 1.5rem 0 0; }
.split-inline-media img {
  width: 100%; height: auto; display: block;
  border-radius: var(--lws-radius-lg); box-shadow: var(--lws-shadow);
}


/* ── cards ─────────────────────────────────────────────────────────────── */

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: var(--lws-gap);
}
.section[data-block="cards"][data-variant="grid-2"] .card-grid { grid-template-columns: repeat(auto-fit, minmax(min(24rem, 100%), 1fr)); }
.section[data-block="cards"][data-variant="grid-3"] .card-grid { grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr)); }
.section[data-block="cards"][data-variant="list"] .card-grid { grid-template-columns: 1fr; }

.card {
  position: relative;
  display: flex; flex-direction: column;
  padding: 1.75rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius-lg);
  box-shadow: var(--lws-shadow);
  text-decoration: none;
  transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
}
a.card:hover { transform: translateY(-3px); box-shadow: var(--lws-shadow-lift); border-color: var(--lws-brand-soft); }
a.card:hover .more span { transform: translateX(3px); }
.card h3 { margin-bottom: .5rem; }
/* The booking block groups its cards under a service heading, so its card
   titles sit one level deeper than a plain card grid's. */
.card h4 { margin-bottom: .5rem; font-size: 1.05rem; }
.card p { color: var(--lws-body); }

/* Booking options — a service heading above each group of booking cards, and
   the small print under a link in the list variant. */
.booking-group {
  margin: 2rem 0 1rem;
  font-size: 1.15rem;
  color: var(--lws-ink);
}
.booking-group:first-of-type { margin-top: 0; }
.link-cols .note { display: block; font-size: .85rem; color: var(--lws-muted); }

.card-img { margin: -1.75rem -1.75rem 1.25rem; }
.card-img img {
  width: 100%; aspect-ratio: 16 / 10; object-fit: cover;
  border-radius: var(--lws-radius-lg) var(--lws-radius-lg) 0 0;
}
.card-ic {
  display: grid; place-items: center;
  width: 3rem; height: 3rem; margin-bottom: 1rem;
  background: var(--lws-brand-tint); color: var(--lws-brand);
  border-radius: var(--lws-radius);
}
.card-ic svg { width: 1.5rem; height: 1.5rem; }

/* Feature variant: the first card spans two columns and reads as a highlight. */
.section[data-block="cards"][data-variant="feature"] .card-grid { grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr)); }
.section[data-block="cards"][data-variant="feature"] .card:first-child {
  grid-column: span 2;
  background: var(--lws-brand); border-color: var(--lws-brand);
}
.section[data-block="cards"][data-variant="feature"] .card:first-child h3,
.section[data-block="cards"][data-variant="feature"] .card:first-child p,
.section[data-block="cards"][data-variant="feature"] .card:first-child .more { color: var(--lws-on-brand); }
.section[data-block="cards"][data-variant="feature"] .card:first-child .card-ic {
  background: rgb(var(--lws-scrim-rgb) / .12); color: var(--lws-on-brand);
}

/* ── bento ─────────────────────────────────────────────────────────────────
   Unequal tiles on one grid. Six explicit columns rather than `auto-fit`,
   because a card that spans two of an auto-fit track spans an unknown
   proportion of the row — the whole point here is that the client's chosen
   size means the same thing every time.

   `grid-auto-flow: dense` lets a normal tile fill the hole a tall one leaves
   beside it, which is the difference between a bento and a grid with gaps in
   it. It reorders visually, so nothing in here may be read in sequence — a
   card grid is a set of destinations, not a numbered list.

   Sizes collapse in two steps: six columns → three (a wide tile is still
   wide, a tall one stops being tall, because a phone-height tile with three
   words in it is a hole) → one, where every card is the same again. */
.section[data-block="cards"][data-variant="bento"] .card-grid {
  grid-template-columns: repeat(6, 1fr);
  grid-auto-rows: minmax(11rem, auto);
  grid-auto-flow: dense;
}
.section[data-block="cards"][data-variant="bento"] .card { grid-column: span 2; }
.section[data-block="cards"][data-variant="bento"] .card[data-size="wide"] { grid-column: span 4; }
.section[data-block="cards"][data-variant="bento"] .card[data-size="tall"] { grid-row: span 2; }
.section[data-block="cards"][data-variant="bento"] .card[data-size="big"]  { grid-column: span 4; grid-row: span 2; }

/* A tall or big tile has room the others do not — the picture takes it rather
   than leaving the card bottom-heavy with white space. */
.section[data-block="cards"][data-variant="bento"] .card[data-size="tall"] .card-img,
.section[data-block="cards"][data-variant="bento"] .card[data-size="big"] .card-img { margin-bottom: 1.5rem; }
/* A tall tile is portrait, so its picture is too; a big one is a landscape
   panel. Both crop with `cover` from whatever the client uploaded, and both are
   anchored to the TOP rather than the centre — every picture that ends up in a
   tile this shape is a screenshot, a room or a shopfront, and the useful half
   of all three is the top. */
.section[data-block="cards"][data-variant="bento"] .card[data-size="tall"] .card-img img { aspect-ratio: 3 / 4; object-position: top; }
.section[data-block="cards"][data-variant="bento"] .card[data-size="big"] .card-img img  { aspect-ratio: 16 / 9; object-position: top; }
.section[data-block="cards"][data-variant="bento"] .card .more { margin-top: auto; }

@media (max-width: 979px) {
  .section[data-block="cards"][data-variant="bento"] .card-grid { grid-template-columns: repeat(3, 1fr); }
  .section[data-block="cards"][data-variant="bento"] .card { grid-column: span 1; }
  .section[data-block="cards"][data-variant="bento"] .card[data-size="wide"],
  .section[data-block="cards"][data-variant="bento"] .card[data-size="big"] { grid-column: span 2; }
  .section[data-block="cards"][data-variant="bento"] .card[data-size="tall"],
  .section[data-block="cards"][data-variant="bento"] .card[data-size="big"] { grid-row: span 1; }
}
@media (max-width: 639px) {
  .section[data-block="cards"][data-variant="bento"] .card-grid {
    grid-template-columns: 1fr; grid-auto-rows: auto;
  }
  .section[data-block="cards"][data-variant="bento"] .card,
  .section[data-block="cards"][data-variant="bento"] .card[data-size="wide"],
  .section[data-block="cards"][data-variant="bento"] .card[data-size="big"] { grid-column: span 1; }
}

/* Tiles: flat, borderless, tighter — for icon-led rows. */
.section[data-block="cards"][data-variant="tiles"] .card {
  background: transparent; border-color: transparent; box-shadow: none; padding: 1.25rem;
}
.section[data-block="cards"][data-variant="tiles"] .card:hover { transform: none; box-shadow: none; }

/* AN "ICON-LED" TILE HAS TO TREAT ITS PICTURE AS AN ICON, and until now it did
   not: `.card-img img` stretches any image to the full card width at 16/10 with
   `cover`, which is right for a photograph and absurd for a symbol. A client
   whose contact card carries a 768×768 location pin got a giant cropped pin
   across the top of the card — the variant promised icon-led and delivered a
   billboard.
   This is the drawn equivalent of `.card-ic`, so a tile looks the same whether
   its mark came from the shared icon set or from the client's own file. */
.section[data-block="cards"][data-variant="tiles"] .card-img {
  margin: 0 0 1rem;
  width: 3.25rem; height: 3.25rem;
  display: grid; place-items: center;
  background: var(--lws-brand-tint);
  border-radius: var(--lws-radius);
}
.section[data-block="cards"][data-variant="tiles"] .card-img img {
  width: 1.75rem; height: 1.75rem;
  aspect-ratio: 1; object-fit: contain; border-radius: 0;
}

/* ── steps ─────────────────────────────────────────────────────────────── */

.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--lws-gap);
  list-style: none; margin: 0; padding: 0;
  counter-reset: step;

  /* The badge is absolutely positioned, so the only thing keeping it off the
     heading is the step's own top padding being at least as tall as the badge
     plus wherever it was inset from the top. Those were two literals in two
     different rules, and in the CARDS variant they disagreed: padding-top
     3.5rem with the badge at top 1.5rem, and 1.5 + 2.6 = 4.1 — so the disc
     ended 0.6rem below where the text began and sat on the heading's ascenders.
     Measured on FunctionalFocus's coaching page at 16px of overlap on every
     card. Derived from here now, so the two cannot drift apart again. */
  --step-num-size: 2.6rem;
  --step-num-gap: .9rem;
  --step-num-inset: 0rem;
}
.step {
  position: relative;
  padding-top: calc(var(--step-num-inset) + var(--step-num-size) + var(--step-num-gap));
}
.step-num {
  position: absolute;
  top: var(--step-num-inset); left: var(--step-num-inset);
  display: grid; place-items: center;
  width: var(--step-num-size); height: var(--step-num-size);
  background: var(--lws-brand); color: var(--lws-on-brand);
  border-radius: 50%;
  font-family: var(--lws-font-display); font-weight: 700;
}
.step h3 { margin-bottom: .35rem; }
.steps-foot { margin-top: 1.75rem; color: var(--lws-muted); font-size: .95rem; }

/* Timeline: one column with a connecting rule down the numbers. */
.section[data-block="steps"][data-variant="timeline"] .steps { grid-template-columns: 1fr; gap: 0; }
.section[data-block="steps"][data-variant="timeline"] .step {
  padding: 0 0 2rem 4rem;
  border-left: 2px solid var(--lws-line);
  margin-left: 1.3rem;
}
.section[data-block="steps"][data-variant="timeline"] .step:last-child { border-left-color: transparent; padding-bottom: 0; }
.section[data-block="steps"][data-variant="timeline"] .step-num { left: -1.3rem; top: -.2rem; }

/* Cards: the badge is inset from the card's corner, so the top padding has to
   clear the inset AS WELL AS the badge. Setting the inset is the whole change —
   the padding and the badge's own top/left both read it. */
.section[data-block="steps"][data-variant="cards"] .step {
  --step-num-inset: 1.5rem;
  padding-right: 1.5rem; padding-bottom: 1.5rem; padding-left: 1.5rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius-lg);
}

/* ── features ──────────────────────────────────────────────────────────── */

.feature-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  gap: var(--lws-gap);
}
.feature h3 { margin-bottom: .35rem; }
.feature-ic {
  display: grid; place-items: center;
  width: 2.75rem; height: 2.75rem; margin-bottom: .9rem;
  background: var(--lws-accent-tint); color: var(--lws-brand);
  border-radius: var(--lws-radius);
}
.feature-ic svg { width: 1.4rem; height: 1.4rem; }

.section[data-block="features"][data-variant="rows"] .feature-grid { grid-template-columns: 1fr; }
.section[data-block="features"][data-variant="rows"] .feature {
  display: grid; grid-template-columns: auto 1fr; gap: 1.25rem; align-items: start;
  padding-bottom: 1.5rem; border-bottom: 1px solid var(--lws-line);
}
.section[data-block="features"][data-variant="rows"] .feature-ic { grid-row: span 2; margin-bottom: 0; }

.feature-links { display: flex; flex-direction: column; gap: .5rem; }
.feature-links a {
  display: inline-flex; align-items: center; gap: .4rem;
  font-family: var(--lws-font-display); font-weight: 600;
  text-decoration: none;
}
.feature-links a:hover span { transform: translateX(3px); }
.feature-links a span { transition: transform .18s ease; }

/* ── faq ───────────────────────────────────────────────────────────────── */

.faq-list { display: grid; gap: .75rem; }
.faq-item {
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius);
  overflow: hidden;
}
.faq-item summary {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 1.1rem 1.35rem;
  font-family: var(--lws-font-display); font-weight: 600;
  color: var(--lws-ink);
  cursor: pointer;
  list-style: none;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+";
  font-size: 1.4rem; line-height: 1; color: var(--lws-brand);
  transition: transform .2s ease;
}
.faq-item[open] summary::after { transform: rotate(45deg); }
.faq-item summary:hover { background: var(--lws-surface-2); }
.faq-answer { padding: 0 1.35rem 1.25rem; }
.faq-answer > *:first-child { margin-top: 0; }

.section[data-block="faq"][data-variant="two-col"] .faq-list { grid-template-columns: 1fr 1fr; }

/* ── panels ────────────────────────────────────────────────────────────── */
/* The accordion shape is the FAQ's, with room for an icon and a real <h3> in
   the summary — panel titles are section names ("Sports performance"), not
   questions, and they belong in the document outline. */

.panels { display: grid; gap: .75rem; }
.panel {
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius);
  overflow: hidden;
}
.panel > summary {
  display: flex; align-items: center; gap: .85rem;
  padding: 1.05rem 1.35rem;
  cursor: pointer;
  list-style: none;
}
.panel > summary::-webkit-details-marker { display: none; }
.panel > summary::after {
  content: "+";
  font-size: 1.4rem; line-height: 1; color: var(--lws-brand);
  transition: transform .2s ease;
}
.panel[open] > summary::after { transform: rotate(45deg); }
.panel > summary:hover { background: var(--lws-surface-2); }
.panel-t, .panel > summary { font-family: var(--lws-font-display); }
.panel-t h3, .panel > summary h3 {
  flex: 1; margin: 0;
  font-size: 1.06rem; font-weight: 600; color: var(--lws-ink);
}
.panel-ic {
  display: grid; place-items: center; flex: none;
  width: 2.1rem; height: 2.1rem;
  background: var(--lws-accent-tint); color: var(--lws-brand);
  border-radius: var(--lws-radius);
}
.panel-ic svg { width: 1.15rem; height: 1.15rem; }
/* A MEASURE, BECAUSE A PANEL IS AS WIDE AS THE PAGE AND PROSE IS NOT.
 *
 * A tab panel spans the whole content width — about 1170px, or 140 characters a
 * line, which is roughly twice what anyone reads comfortably and the reason
 * `.prose` has had 72ch since the beginning. It matters more here than it looks
 * because the pictures inside these bodies are sized off the SAME box: at full
 * width a single photograph is 731px tall and taller than the paragraph it
 * illustrates, so the text and the images stop being one column of anything.
 * Centred rather than left-aligned — the body sits inside a bordered card, and
 * a hard gutter down one side of a card reads as a layout that slipped.
 *
 * In rem and not the `72ch` that `.prose` uses, which was tried first and is
 * wrong HERE for a reason worth keeping: `ch` is measured in the client's own
 * body font, so on a condensed one 72 characters is about 580px — half the
 * width of the card it sits in, which reads as a column that failed to fill
 * rather than a measure somebody chose. A page of prose can be that narrow
 * because nothing is drawn around it; inside a bordered panel the proportion
 * to the border is the thing being looked at. */
.panel-body { padding: 0 1.35rem 1.3rem; max-width: min(46rem, 100%); margin-inline: auto; }
.panel-body > *:first-child { margin-top: 0; }
.panel-body > *:last-child  { margin-bottom: 0; }
.panel-cta { margin-top: 1rem; }

/* A PICTURE INSIDE A PROSE BODY IS HOUSED, NEVER LEFT AT ITS OWN SIZE.
 *
 * These bodies are rich text, and on a migrated site they are imported
 * WordPress markup carrying whatever `<img>` tags the old editor left behind —
 * on Roach's About page, one panel holds ten of them at four different shapes
 * (768x512, 768x1152, 1920x1080, 2560x1707). Rendered at their intrinsic width
 * they step in and out of the text column at a different size every time, with
 * a ragged band of empty page beside each one: "the images and text don't
 * flow", which is exactly how it was reported (Willy, 2026-08-11).
 *
 * Nothing about that is Roach's — an unconstrained inline image in a prose
 * column is never the intended look on any site, which is why this is the
 * platform's rule rather than one site's stylesheet. Every picture fills the
 * column and stands in the SAME container, so a portrait and a panorama are
 * the same shape on the page and the text between them keeps one rhythm.
 *
 * `contain` was the alternative and is worse: it makes every image the same
 * HEIGHT and a different WIDTH, which leaves the ragged edge that is the whole
 * complaint. Cover crops, and a crop is the price of a uniform grid.
 *
 * The FAQ's answers are the same content in the same shape, and were missing
 * the rule entirely — a picture in an answer had no styling at all. One
 * declaration for both, or they drift. */
.panel-body img,
.faq-answer img,
.card-text img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  object-position: center;
  border-radius: var(--lws-radius);
  margin: 0 0 1rem;
}

/* ONE PICTURE IS NOT A GRID, so it is not cropped into one.
   The rule above buys a uniform rhythm across a RUN of mixed shapes, and a
   crop is the price. With a single image there is no run: the crop buys
   nothing and costs the top and bottom of somebody's poster — Mind & Body's
   weekly ambassador posts are 819x1024 flyers, and 16:10 took half of each.
   The count is stamped server-side because CSS cannot count. Same instinct,
   and the same wording, as .gallery img:only-child above: when there is one
   picture, show the picture. */
.panel-body[data-images="1"] img,
.faq-answer[data-images="1"] img {
  aspect-ratio: auto;
  width: auto;
  max-width: 100%;
  max-height: 30rem;
  object-fit: contain;
  margin-inline: auto;
}

/* A PORTRAIT PICTURE IN A PAGE HEADER IS A POSTER. Stamped by the renderer,
   which measures the file rather than asking the client what shape it is. */
.page-hero-media[data-fit="contain"] img {
  width: auto;
  max-width: 100%;
  max-height: min(var(--lws-media-h), 30rem);
  object-fit: contain;
  margin-inline: auto;
  box-shadow: none;
}

/* Tabs, before the script runs: a stacked list of headed sections. Everything
   is on screen and in the document — which is the whole reason this variant is
   safe to put ranked content inside. */
.panels[data-lws-tabs] > .panel > .panel-t {
  display: flex; align-items: center; gap: .85rem;
  padding: 1.05rem 1.35rem;
}
.panels[data-lws-tabs] > .panel > .panel-body { padding: 0 1.35rem 1.3rem; }

/* …and after it: a real tablist, with the panel titles promoted into buttons. */
.panels.is-live { gap: 0; }
.panels.is-live > .panel > .panel-t { display: none; }
.panels.is-live > .panel {
  border-radius: 0 0 var(--lws-radius) var(--lws-radius);
  border-top-color: transparent;
}
.panels.is-live > .panel[hidden] { display: none; }
.panels.is-live > .panel > .panel-body { padding: 1.5rem 1.35rem; }
.panel-tabs {
  display: flex; flex-wrap: wrap; gap: .35rem;
  margin-bottom: -1px;
}
.panel-tab {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .7rem 1.1rem;
  background: var(--lws-surface-2);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius) var(--lws-radius) 0 0;
  font-family: var(--lws-font-display); font-weight: 600; font-size: .97rem;
  color: var(--lws-body);
  cursor: pointer;
  transition: background .18s ease, color .18s ease;
}
.panel-tab:hover { background: var(--lws-surface); color: var(--lws-ink); }
.panel-tab[aria-selected="true"] {
  background: var(--lws-surface);
  border-bottom-color: var(--lws-surface);
  color: var(--lws-brand);
}
.panel-tab svg { width: 1.05rem; height: 1.05rem; }

/* ── panels / gallery ──────────────────────────────────────────────────────
   The renderer emits the `cards` block's own markup here, so almost everything
   this layout needs is already declared up in the cards section and these rules
   only say what is different about a card whose text is RICH text.

   THREE ACROSS, NOT auto-fit's FOUR. The sets this variant is for are four to
   six audiences or differentiators; the default 17rem track fits four on a wide
   screen and leaves a single orphan under a row of three, which is the one
   arrangement that reads as a mistake rather than a grid. */
.section[data-block="panels"][data-variant="gallery"] .card-grid {
  grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
}
/* FOUR IS THE ONE COUNT auto-fit GETS WRONG. Three tracks turn five items into
   3 + 2, which reads as a grid, and four into 3 + 1, which reads as a mistake —
   one card sitting alone under a full row looks like something failed to load.
   Counted in CSS rather than stamped by the renderer because the count is a
   fact about the layout, not about the content, and a client adding a fifth
   panel should not have to think about it. */
.section[data-block="panels"][data-variant="gallery"] .card-grid:has(> :nth-child(4):last-child) {
  grid-template-columns: repeat(2, 1fr);
}
/* A PORTRAIT AND A LANDSCAPE HAVE TO SURVIVE THE SAME BOX, so the ratio is
   nearer square than the 16/10 a card photo normally gets: the pictures a
   clinic has of its patients are portrait (683x1024 on Roach's) and the ones it
   has of its rooms are landscape, and both appear in this layout. 4/3 crops the
   portrait from the top and bottom and the landscape from the sides.

   AND THE CROP IS BIASED UPWARD, which is the difference between a gallery of
   faces and a gallery of torsos. Cropping a 2:3 portrait of a standing person
   to 4/3 keeps a band barely half the frame; centred, that band is the middle
   of the body. 30% puts it over the head and shoulders, and on a landscape
   photograph of a room it is within a few percent of centre — so the same
   number is right for both without asking the client which kind they picked. */
.section[data-block="panels"][data-variant="gallery"] .card-img img {
  aspect-ratio: 4 / 3;
  object-position: center 30%;
}
/* The body is rich text, so its own <p> margins would push the card's first
   line away from the photo and leave a gap under the last one. */
.section[data-block="panels"][data-variant="gallery"] .card-text > *:first-child { margin-top: 0; }
.section[data-block="panels"][data-variant="gallery"] .card-text > *:last-child  { margin-bottom: 0; }
.section[data-block="panels"][data-variant="gallery"] .card-text { color: var(--lws-body); }
/* Buttons line up across a row whatever the paragraphs above them do. */
.section[data-block="panels"][data-variant="gallery"] .panel-cta { margin-top: auto; padding-top: 1rem; }

/* ── share ─────────────────────────────────────────────────────────────── */

.share {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--lws-gap);
}
.share-copy { flex: 1 1 20rem; }
.share-copy h2 { margin-bottom: .3rem; }
.share-copy p  { margin-bottom: 0; color: var(--lws-body); }
.share-links { display: flex; flex-wrap: wrap; gap: .5rem; }
.share-link {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .62rem .95rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius);
  font-family: var(--lws-font-display); font-weight: 600; font-size: .93rem;
  color: var(--lws-ink);
  text-decoration: none;
  transition: border-color .18s ease, transform .18s ease;
}
.share-link:hover { border-color: var(--lws-brand); transform: translateY(-1px); }
/* Brand marks are drawn filled, against the global stroke default at line 58. */
.brand-glyph { flex: none; width: 1.15rem; height: 1.15rem; fill: currentColor; stroke: none; }

.section[data-block="share"][data-variant="compact"] .share-link { padding: .62rem; }
.section[data-block="share"][data-variant="compact"] .share-link span {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.section[data-block="share"][data-variant="band"] { background: var(--lws-brand); }
.section[data-block="share"][data-variant="band"] .share-copy h2,
.section[data-block="share"][data-variant="band"] .share-copy p { color: var(--lws-on-brand); }
.section[data-block="share"][data-variant="band"] .share-link { border-color: transparent; }

/* ── proof ─────────────────────────────────────────────────────────────── */

.rating { display: flex; align-items: center; flex-wrap: wrap; gap: .75rem; margin-bottom: 2rem; }
.rating-stars { display: inline-flex; gap: .15rem; color: var(--lws-accent); }
.rating-stars svg { width: 1.25rem; height: 1.25rem; }
.rating-stars svg.on { fill: currentColor; }
.rating strong { font-family: var(--lws-font-display); font-size: 1.25rem; color: var(--lws-ink); }
.rating span { color: var(--lws-muted); font-size: .95rem; }

.proof-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr)); gap: var(--lws-gap); }
.proof-item {
  display: flex; flex-direction: column; gap: .75rem;
  padding: 1.5rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius-lg);
  text-decoration: none;
}
.proof-item blockquote { color: var(--lws-ink); font-size: 1.02rem; }
.proof-item blockquote::before { content: "\201C"; }
.proof-item blockquote::after  { content: "\201D"; }
.proof-item cite { margin-top: auto; font-weight: 600; color: var(--lws-ink); font-size: .95rem; }
.proof-item cite span { display: block; font-weight: 400; color: var(--lws-muted); font-size: .88rem; }

/* Logos: strip the card chrome, let the marks sit on the ground.
   `marquee` is the same treatment moving, so both variants are named on every
   rule — a second selector list would be two logo strips that drift. */
.section[data-block="proof"][data-variant="logos"] .proof-item,
.section[data-block="proof"][data-variant="marquee"] .proof-item {
  background: none; border: 0; padding: 1rem; align-items: center;
}
.section[data-block="proof"][data-variant="logos"] .proof-item img,
.section[data-block="proof"][data-variant="marquee"] .proof-item img {
  max-height: 3rem; width: auto;
  filter: grayscale(1); opacity: .65;
  transition: filter .2s ease, opacity .2s ease;
}
.section[data-block="proof"][data-variant="logos"] .proof-item:hover img,
.section[data-block="proof"][data-variant="marquee"] .proof-item:hover img { filter: none; opacity: 1; }

/* ── the marquee ───────────────────────────────────────────────────────────
   The mask is on the WRAPPER and the animation on the track. Put them on one
   element and the mask travels with it, which fades the middle of the strip
   and leaves the ends hard.

   The renderer emits the list twice, so translating the track by exactly half
   its width lands copy 2 where copy 1 began and the loop has no seam. */
.marquee {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.marquee-track {
  display: flex; width: max-content;
  animation: lws-marquee 42s linear infinite;
}
.marquee-track .proof-grid {
  display: flex; flex: none;
  gap: calc(var(--lws-gap) * 2);
  padding-inline: calc(var(--lws-gap));
}
@keyframes lws-marquee { to { transform: translateX(-50%); } }
/* Somebody trying to read a logo can stop it. */
.marquee:hover .marquee-track,
.marquee:focus-within .marquee-track { animation-play-state: paused; }

/* Reduced motion gets the logo strip it already had — still scrollable, never
   moving on its own. */
@media (prefers-reduced-motion: reduce) {
  .marquee { overflow-x: auto; -webkit-mask-image: none; mask-image: none; }
  .marquee-track { animation: none; }
}

/* ── spotlight ─────────────────────────────────────────────────────────────
   One quote, given the room a good one deserves. Portrait beside it when there
   is one; nothing but the words when there is not. */
.section[data-block="proof"][data-variant="spotlight"] .proof-grid { grid-template-columns: 1fr; }
.section[data-block="proof"][data-variant="spotlight"] .proof-item {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: calc(var(--lws-gap) * 1.5);
  max-width: 62rem; margin-inline: auto;
  padding: clamp(1.75rem, 4vw, 3rem);
  background: var(--lws-surface);
  border-radius: var(--lws-radius-lg);
  box-shadow: var(--lws-shadow);
}
.section[data-block="proof"][data-variant="spotlight"] .proof-item img {
  grid-row: span 2;
  width: 7.5rem; height: 7.5rem;
  object-fit: cover; border-radius: 999px;
}
.section[data-block="proof"][data-variant="spotlight"] blockquote {
  font-family: var(--lws-font-display);
  font-size: clamp(1.25rem, 2.4vw, 1.7rem);
  line-height: 1.4;
}
.section[data-block="proof"][data-variant="spotlight"] cite { margin-top: 0; }
@media (max-width: 639px) {
  .section[data-block="proof"][data-variant="spotlight"] .proof-item {
    grid-template-columns: 1fr; justify-items: center; text-align: center;
  }
  .section[data-block="proof"][data-variant="spotlight"] .proof-item img { grid-row: auto; }
}

.section[data-block="proof"][data-variant="creds"] .proof-grid { grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr)); }

/* ── stats ─────────────────────────────────────────────────────────────── */

.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr)); gap: var(--lws-gap); }
.stat { text-align: center; }
.stat strong {
  display: block;
  font-family: var(--lws-font-display);
  font-size: clamp(2rem, 4vw, 2.9rem); font-weight: 700; line-height: 1.05;
  color: var(--lws-brand);
}
.stat span { display: block; margin-top: .35rem; font-weight: 600; color: var(--lws-ink); }
.stat em { display: block; font-style: normal; font-size: .88rem; color: var(--lws-muted); }

.section[data-block="stats"][data-variant="boxed"] .stat {
  padding: 1.5rem 1rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius-lg);
}
.section[data-block="stats"][data-variant="band"] { background: var(--lws-brand); }
.section[data-block="stats"][data-variant="band"] .stat strong,
.section[data-block="stats"][data-variant="band"] .stat span { color: var(--lws-on-brand); }
.section[data-block="stats"][data-variant="band"] .stat em { color: var(--lws-on-brand-dim); }
.section[data-block="stats"][data-variant="band"] .section-head h2,
.section[data-block="stats"][data-variant="band"] .section-head p { color: var(--lws-on-brand); }

/* ── stats: the scoreboard ─────────────────────────────────────────────────

   Lamp segments on a dark housing. THE PANEL IS DARK ON EVERY THEME and that is
   not an oversight: a scoreboard is a physical object, and a white one with
   pale numerals is not a scoreboard, it is a table. So the housing and the
   label colours here are literal rather than palette tokens — the only place in
   this stylesheet where that is true, and the reason is that this variant is
   drawing an object rather than a surface.

   The LIT colour is the theme's accent, because that IS a per-site decision:
   ScrumRL's is scoreboard green, another site's could be amber. Unlit segments
   are the same colour at a whisper, which is what makes the digits read as
   hardware — see lws_seg_number() for why they are drawn at all. */
.section[data-block="stats"][data-variant="scoreboard"] .stat-row { gap: clamp(.75rem, 2vw, 1.5rem); }
.section[data-block="stats"][data-variant="scoreboard"] .stat {
  padding: clamp(.9rem, 2.2vw, 1.5rem) clamp(.6rem, 1.8vw, 1.2rem);
  background: #05080699;
  background-image: linear-gradient(180deg, #0d1310 0%, #050806 100%);
  border: 1px solid rgb(255 255 255 / .07);
  border-radius: var(--lws-radius-lg);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .05);
}
.section[data-block="stats"][data-variant="scoreboard"] .stat span { color: #e9eeeb; }
.section[data-block="stats"][data-variant="scoreboard"] .stat em   { color: rgb(233 238 235 / .55); }

.stat-lamps {
  display: block;
  font-size: clamp(1.9rem, 3.6vw, 2.7rem);
  line-height: 1;
  white-space: nowrap;
}
.seg-digit {
  display: inline-block; vertical-align: -.06em;
  width: .58em; height: 1em; margin: 0 .02em;
  color: var(--lws-accent);
}
.seg-digit polygon { fill: currentColor; }
.seg-digit .off { opacity: .085; }
.seg-digit .on  { filter: drop-shadow(0 0 .09em currentColor); }
/* Anything the seven segments cannot form — a plus, a slash, a letter. Same
   lamp colour, display face, so "10+" does not change colour halfway. */
.seg-glyph {
  display: inline-block;
  font-family: var(--lws-font-display); font-weight: 700;
  color: var(--lws-accent);
  filter: drop-shadow(0 0 .09em currentColor);
}

/* ── colour-keyed options ──────────────────────────────────────────────────

   Bright KEYS, deliberately outside the palette so they survive a change of
   brand in Appearance — see LWS_KEY_TONES. They fill a chip and a rule and are
   never type on the page, so the surface underneath does not have to be known;
   the chip carries its own dark label. */
:root {
  --lws-key-blue:    #0A84FF;
  --lws-key-amber:   #FFB300;
  --lws-key-magenta: #FF1E8E;
  --lws-key-green:   #2BD96B;
  --lws-key-violet:  #A97BFF;
  --lws-key-ink:     #0B0F0D;
}
.optioncards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: var(--lws-gap);
}
.optioncard {
  position: relative;
  padding: 1.35rem 1.35rem 1.2rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-left: 3px solid var(--key, var(--lws-brand));
  border-radius: var(--lws-radius-lg);
  box-shadow: var(--lws-shadow);
}
.optioncard[data-tone="accent"]  { --key: var(--lws-accent); }
.optioncard[data-tone="blue"]    { --key: var(--lws-key-blue); }
.optioncard[data-tone="amber"]   { --key: var(--lws-key-amber); }
.optioncard[data-tone="magenta"] { --key: var(--lws-key-magenta); }
.optioncard[data-tone="green"]   { --key: var(--lws-key-green); }
.optioncard[data-tone="violet"]  { --key: var(--lws-key-violet); }

.optioncard-key {
  display: inline-block; margin-bottom: .6rem;
  padding: .2rem .55rem;
  font-family: var(--lws-font-display); font-weight: 700;
  font-size: .82rem; letter-spacing: .06em; text-transform: uppercase;
  border-radius: var(--lws-radius);
  background: var(--lws-brand); color: var(--lws-on-brand);
}
.optioncard[data-tone]                 .optioncard-key { background: var(--key); color: var(--lws-key-ink); }
.optioncard[data-tone="accent"]        .optioncard-key { color: var(--lws-on-accent); }
.optioncard h3 { margin: 0 0 .4rem; font-size: var(--lws-h3); }
.optioncard p  { margin: 0; color: var(--lws-body); }

.optioncard-outcomes { margin: .95rem 0 0; display: grid; gap: .3rem .6rem; }
.optioncard-outcomes dt {
  font-size: .74rem; letter-spacing: .05em; text-transform: uppercase;
  font-weight: 600; color: var(--lws-muted);
}
.optioncard-outcomes dt::before { margin-right: .35rem; font-weight: 700; }
.optioncard-outcomes dt.is-good::before { content: "\2713"; color: #2BD96B; }
.optioncard-outcomes dt.is-bad::before  { content: "\2715"; color: #FF6B72; }
.optioncard-outcomes dd { margin: 0 0 .45rem; font-weight: 600; color: var(--lws-ink); }
.optioncard-outcomes dd:last-child { margin-bottom: 0; }

.section[data-block="optioncards"][data-variant="rows"] .optioncards { grid-template-columns: 1fr; }

/* ── team ──────────────────────────────────────────────────────────────── */

/* auto-fit COLLAPSES the tracks nobody filled, and the survivors take the
   space. A four-track row with two members in it is two 626px cards on a
   1280px wrap, and at 4/5 that is a 782px portrait of one person — the grid
   doing exactly what it was told and nothing anyone would have designed.
   A two-person practice is the normal case for these sites, so the track gets
   a ceiling as well as a floor and the row centres in what's left over. */
.team-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), var(--lws-member-w, 24rem)));
  justify-content: center;
  gap: var(--lws-gap);
}
.member {
  display: flex; flex-direction: column;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius-lg);
  overflow: hidden;
}
.member-portrait { position: relative; }
.member-portrait img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; }
.member-namecard {
  position: absolute; left: 1rem; right: 1rem; bottom: 1rem;
  padding: .7rem 1rem;
  background: rgb(var(--lws-scrim-rgb) / .82);
  border-radius: var(--lws-radius);
  color: #fff;
}
.member-namecard strong { display: block; font-family: var(--lws-font-display); }
.member-namecard span { font-size: .88rem; opacity: .85; }
.member-body { padding: 1.5rem; display: flex; flex-direction: column; flex: 1; }
.member-role { color: var(--lws-muted); font-weight: 600; margin-top: -.35rem; }
.member-creds { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1rem; }
.member-cred {
  padding: .2rem .6rem;
  background: var(--lws-surface-3); color: var(--lws-ink);
  border-radius: 999px; font-size: .78rem; font-weight: 600;
}
.member-book { margin-top: auto; padding-top: 1.25rem; }

.section[data-block="team"][data-variant="rows"] .team-grid { grid-template-columns: 1fr; }
.section[data-block="team"][data-variant="rows"] .member { flex-direction: row; }
.section[data-block="team"][data-variant="rows"] .member-portrait { flex: 0 0 16rem; }
.section[data-block="team"][data-variant="rows"] .member-portrait img { height: 100%; aspect-ratio: auto; }

.section[data-block="team"][data-variant="compact"] .team-grid { grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), var(--lws-member-w, 17rem))); }
.section[data-block="team"][data-variant="compact"] .member-body { padding: 1rem; }

/* ── gallery ───────────────────────────────────────────────────────────── */

.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); gap: .75rem; }
.gallery img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--lws-radius); }

/* A GALLERY OF ONE IS NOT A GRID, AND MUST NOT BE CROPPED TO ACT LIKE ONE.
   auto-fit hands a lone image the whole track, so 4/3 of a 1220px wrap renders
   a 915px-tall billboard that reads as a layout fault. Capping the height with
   `cover` fixes the size and introduces a worse bug — it centre-crops, and on a
   photograph of people that takes their faces off.
   So the single case drops the forced ratio entirely and is bounded by height:
   the whole picture, at a sane size, centred. Same instinct as the media
   library's `contain` — when there is one picture, show the picture. */
.gallery img:only-child {
  aspect-ratio: auto;
  width: auto;
  max-width: 100%;
  max-height: 26rem;
  object-fit: contain;
  margin-inline: auto;
}

.section[data-block="gallery"][data-variant="organic"] .gallery img:nth-child(3n+1) { border-radius: var(--lws-radius-lg) var(--lws-radius) var(--lws-radius-lg) var(--lws-radius); }
.section[data-block="gallery"][data-variant="organic"] .gallery img:nth-child(3n+2) { aspect-ratio: 1; border-radius: 50% 50% var(--lws-radius) var(--lws-radius); }

.section[data-block="gallery"][data-variant="strip"] .gallery {
  display: flex; overflow-x: auto; scroll-snap-type: x mandatory;
  padding-bottom: .5rem;
}
.section[data-block="gallery"][data-variant="strip"] .gallery img {
  flex: 0 0 min(80%, 22rem); scroll-snap-align: start;
}

/* ── banner ────────────────────────────────────────────────────────────── */

.banner {
  position: relative; overflow: hidden;
  border-radius: var(--lws-radius-lg);
  min-height: 20rem;
  display: grid;
}
.banner .banner-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.banner::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, rgb(var(--lws-scrim-rgb) / .8), rgb(var(--lws-scrim-rgb) / .35));
}
.banner-inner {
  position: relative; z-index: 1;
  align-self: center;
  max-width: 34rem;
  padding: 2.5rem;
  color: #fff;
}
.banner-inner h2 { color: #fff; }
.banner-inner .eyebrow { color: var(--lws-accent-soft); }
.banner-inner .btn { margin-top: 1.25rem; }

.section[data-block="banner"][data-variant="center"] .banner-inner { margin-inline: auto; text-align: center; }
.section[data-block="banner"][data-variant="center"] .banner::after { background: rgb(var(--lws-scrim-rgb) / .6); }
.section[data-block="banner"][data-variant="right"] .banner-inner { margin-left: auto; }
.section[data-block="banner"][data-variant="right"] .banner::after { background: linear-gradient(270deg, rgb(var(--lws-scrim-rgb) / .8), rgb(var(--lws-scrim-rgb) / .35)); }

/* ── embed ─────────────────────────────────────────────────────────────── */

.embed { position: relative; border-radius: var(--lws-radius-lg); overflow: hidden; box-shadow: var(--lws-shadow); }
.embed iframe { width: 100%; height: 100%; border: 0; aspect-ratio: 16 / 9; }
.embed[data-ratio="4x3"]  iframe { aspect-ratio: 4 / 3; }
.embed[data-ratio="1x1"]  iframe { aspect-ratio: 1; }
.embed[data-ratio="21x9"] iframe { aspect-ratio: 21 / 9; }
.section[data-block="embed"][data-variant="map"] .embed iframe { aspect-ratio: 21 / 9; min-height: 20rem; }

/* ── posts ─────────────────────────────────────────────────────────────── */

.post-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr)); gap: var(--lws-gap); }
.post-card {
  display: flex; flex-direction: column;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius-lg);
  overflow: hidden;
  text-decoration: none;
  transition: transform .2s ease, box-shadow .2s ease;
}
.post-card:hover { transform: translateY(-3px); box-shadow: var(--lws-shadow-lift); }
.post-card-img img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.post-card-body { padding: 1.35rem; display: flex; flex-direction: column; flex: 1; }
.post-card-body h3 { font-size: 1.15rem; margin-bottom: .5rem; }
.post-card-body p { color: var(--lws-body); font-size: .95rem; }
.post-card-body time { margin-top: auto; padding-top: .75rem; font-size: .85rem; color: var(--lws-muted); }

.section[data-block="posts"][data-variant="list"] .post-grid { grid-template-columns: 1fr; }
.section[data-block="posts"][data-variant="list"] .post-card { flex-direction: row; }
.section[data-block="posts"][data-variant="list"] .post-card-img { flex: 0 0 16rem; }
.section[data-block="posts"][data-variant="list"] .post-card-img img { height: 100%; aspect-ratio: auto; }

.section[data-block="posts"][data-variant="feature"] .post-card:first-child { grid-column: span 2; }
.section[data-block="posts"][data-variant="feature"] .post-card:first-child h3 { font-size: var(--lws-h3); }

/* ── collection ────────────────────────────────────────────────────────── */

.pill-grid { display: flex; flex-wrap: wrap; gap: .6rem; }
.link-cols { columns: 3; column-gap: var(--lws-gap); }
.link-cols a {
  display: block; padding: .45rem 0;
  break-inside: avoid;
  text-decoration: none; font-weight: 500;
}
.link-cols a:hover { color: var(--lws-brand-deep); text-decoration: underline; }

/* ── events ────────────────────────────────────────────────────────────── */

/* NOT `.card`. An event card is the only one on the platform that is not a
   link — it holds a form, and wrapping interactive controls in an <a> is
   invalid and unusable with a keyboard. Sharing the class would also have
   inherited `a.card:hover`'s lift, which on something you are typing into
   reads as the page moving under your hands. */
.event-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
  gap: var(--lws-gap);
}
.section[data-block="events"][data-variant="list"] .event-grid,
.section[data-block="events"][data-variant="agenda"] .event-grid { grid-template-columns: 1fr; }

.event-card {
  display: flex; flex-direction: column;
  padding: 1.75rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius-lg);
  box-shadow: var(--lws-shadow);
}
/* An event listed in an archive is still worth reading and no longer worth
   acting on. Dimmed rather than restyled, so the eye skips it without the page
   growing a second kind of card. */
.event-card[data-past] { opacity: .72; }

.event-top {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: .75rem; margin-bottom: 1.15rem;
}
.event-ic {
  display: grid; place-items: center;
  flex: none; width: 3rem; height: 3rem;
  background: var(--lws-brand-tint); color: var(--lws-brand);
  border-radius: 50%;
}
.event-ic svg { width: 1.5rem; height: 1.5rem; }
.event-date {
  flex: none;
  padding: .4rem .85rem;
  background: var(--lws-accent-tint); color: var(--lws-accent-deep);
  border-radius: 50rem;
  font-family: var(--lws-font-display); font-weight: 700;
  font-size: .78rem; letter-spacing: .1em; text-transform: uppercase;
  white-space: nowrap;
}

.event-img { margin: -1.75rem -1.75rem 1.25rem; order: -1; }
.event-img img {
  width: 100%; aspect-ratio: 16 / 10; object-fit: cover;
  border-radius: var(--lws-radius-lg) var(--lws-radius-lg) 0 0;
}

.event-title { margin-bottom: .75rem; font-size: 1.25rem; }
.event-sub {
  display: block; margin-top: .25rem;
  font-family: var(--lws-font-body); font-size: .92rem; font-style: italic;
  font-weight: 400; color: var(--lws-brand);
}
.event-card p { color: var(--lws-body); }

/* Pushed to the bottom so a row of cards lines its dates and times up whatever
   the titles do to the height above them. */
.event-meta {
  margin-top: auto; padding-top: 1rem;
  border-top: 1px dashed var(--lws-line);
  display: flex; flex-direction: column; gap: .5rem;
}
.event-row { display: flex; align-items: flex-start; gap: .55rem; font-size: .95rem; color: var(--lws-body); }
.event-row svg { flex: none; width: 1rem; height: 1rem; margin-top: .15rem; color: var(--lws-accent); }
/* The second line of a two-line venue — indented to the text, not the icon. */
.event-row-cont { padding-left: 1.55rem; }

.event-actions { margin-top: 1.15rem; display: flex; flex-direction: column; gap: .85rem; }

/* A disclosure rather than an open form: a page of twelve events is twelve
   name/email/phone triplets, and the list stops being readable long before
   anyone reaches the one they came for. */
.event-register > summary {
  cursor: pointer; list-style: none;
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .7rem 1.15rem;
  background: var(--lws-brand); color: var(--lws-on-brand);
  border-radius: var(--lws-radius);
  font-family: var(--lws-font-display); font-weight: 600; font-size: .95rem;
}
.event-register > summary::-webkit-details-marker { display: none; }
.event-register > summary::after { content: '＋'; font-weight: 400; opacity: .8; }
.event-register[open] > summary { background: var(--lws-brand-deep); }
.event-register[open] > summary::after { content: '−'; }
.event-register > summary:hover { background: var(--lws-brand-deep); }
/* Flat inside a card that already has a border and a shadow of its own. */
.event-register .form-card {
  margin-top: .85rem; padding: 0;
  background: none; border: 0; box-shadow: none;
}

.event-share { display: flex; gap: .4rem; }
/* Icon-only: a label per target, per event, on a page of twelve is more words
   about sharing than about the seminars. The name lives in aria-label. */
.event-share .share-link { padding: .5rem; }

/* Agenda: the date moves out to a column of its own, so a long list scans down
   the dates rather than down the titles. */
.section[data-block="events"][data-variant="agenda"] .event-card {
  display: grid;
  grid-template-columns: 7rem 1fr;
  column-gap: var(--lws-gap);
  align-items: start;
}
.section[data-block="events"][data-variant="agenda"] .event-top {
  grid-row: span 4;
  flex-direction: column; align-items: center; gap: .6rem; margin: 0;
}
.section[data-block="events"][data-variant="agenda"] .event-meta {
  margin-top: .85rem; grid-column: 2;
}
.section[data-block="events"][data-variant="agenda"] .event-img { display: none; }

/* Compact list: no photo, no icon circle, one line of detail. */
.section[data-block="events"][data-variant="list"] .event-card { padding: 1.25rem 1.5rem; }
.section[data-block="events"][data-variant="list"] .event-img { display: none; }
.section[data-block="events"][data-variant="list"] .event-ic { width: 2.25rem; height: 2.25rem; }
.section[data-block="events"][data-variant="list"] .event-ic svg { width: 1.15rem; height: 1.15rem; }
.section[data-block="events"][data-variant="list"] .event-meta { flex-direction: row; flex-wrap: wrap; gap: .35rem 1.25rem; }

/* ── videos ────────────────────────────────────────────────────────────── */

.video-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: var(--lws-gap);
}
.video-card {
  display: flex; flex-direction: column;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius-lg);
  overflow: hidden;
  text-decoration: none;
  transition: transform .2s ease, box-shadow .2s ease;
}
.video-card:hover { transform: translateY(-3px); box-shadow: var(--lws-shadow-lift); }

.video-thumb { position: relative; display: block; background: #000; }
/* The feed's thumbnail is 480x360 — 4:3 with pillar bars baked in, because
   YouTube stores it that way. `cover` in a 16:9 box crops the bars off rather
   than framing them, which is why the box is stated here and not left to the
   file. */
.video-thumb img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }

.video-play {
  position: absolute; inset: 0; margin: auto;
  width: 3.5rem; height: 3.5rem;
  display: grid; place-items: center;
  border-radius: 50%;
  background: rgb(0 0 0 / .55);
  transition: background .2s ease, transform .2s ease;
}
.video-play svg { width: 1.6rem; height: 1.6rem; fill: #fff; }
.video-card:hover .video-play,
.video-card:focus-visible .video-play { background: var(--lws-brand); transform: scale(1.08); }

.video-title {
  padding: 1rem 1.15rem 1.15rem;
  font-weight: 600; line-height: 1.35;
  color: var(--lws-ink);
}

/* Playing in place: the anchor keeps its box and the iframe fills the thumb,
   so nothing on the page moves when a video starts. */
.video-thumb iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.video-card[data-playing] { transform: none; }
.video-card[data-playing] .video-play { display: none; }

.section[data-block="videos"][data-variant="list"] .video-list { display: grid; gap: .85rem; }
.section[data-block="videos"][data-variant="list"] .video-card { flex-direction: row; align-items: center; }
.section[data-block="videos"][data-variant="list"] .video-thumb { flex: 0 0 11rem; }
.section[data-block="videos"][data-variant="list"] .video-title { padding: .9rem 1.15rem; font-size: .98rem; }

@media (prefers-reduced-motion: reduce) {
  .video-card, .video-play { transition: none; }
  .video-card:hover { transform: none; }
}

/* ── forms ─────────────────────────────────────────────────────────────── */

.form-card {
  padding: 2rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius-lg);
  box-shadow: var(--lws-shadow);
}
.section[data-block="form"][data-variant="plain"] .form-card {
  padding: 0; background: none; border: 0; box-shadow: none;
}
.form-card h3 { margin-bottom: .5rem; }

/* Twelve columns, so halves, thirds and quarters all land on one grid.
   `full` and `half` keep their old spans exactly — every form authored against
   the two-column version renders unchanged. */
.form-grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 1rem; margin-top: 1.25rem; }
.field, .form-copy { display: flex; flex-direction: column; gap: .35rem; }
.field[data-w="full"],  .form-copy[data-w="full"]           { grid-column: span 12; }
.field[data-w="three-quarters"], .form-copy[data-w="three-quarters"] { grid-column: span 9; }
.field[data-w="two-thirds"], .form-copy[data-w="two-thirds"]     { grid-column: span 8; }
.field[data-w="half"],  .form-copy[data-w="half"]           { grid-column: span 6; }
.field[data-w="third"], .form-copy[data-w="third"]          { grid-column: span 4; }
.field[data-w="quarter"], .form-copy[data-w="quarter"]      { grid-column: span 3; }

.form-section { grid-column: 1 / -1; margin: .75rem 0 0; }
.form-section h4 { margin: 0; color: var(--lws-ink); }
.form-section p  { margin: .3rem 0 0; font-size: .92rem; color: var(--lws-muted); }

.form-copy { font-size: .95rem; color: var(--lws-body); }
.form-copy > *:first-child { margin-top: 0; }
.form-copy > *:last-child  { margin-bottom: 0; }

/* A visible cap belongs under the box it applies to, not in the help text. */
.field .count-note { color: var(--lws-muted); font-size: .82rem; }
.field .count-note.is-low { color: var(--lws-err); }

/* A long form's numbered sections. Every step is on the page — the badge says
   where you are, it does not hide anything (see lws_form_steps()). */
.form-step { margin: 2rem 0 0; border-top: 1px solid var(--lws-line); }
.form-step:first-of-type { margin-top: 1.25rem; border-top: 0; }
.form-step span {
  display: inline-block; margin-top: -.7rem; padding: .15rem .7rem;
  background: var(--lws-brand-tint); color: var(--lws-brand-deep);
  border-radius: 999px;
  font-family: var(--lws-font-display); font-weight: 600; font-size: .78rem;
  letter-spacing: .02em;
}
.form-step + .form-grid { margin-top: .85rem; }

.field label { font-family: var(--lws-font-display); font-weight: 600; font-size: .92rem; color: var(--lws-ink); }
.field .opt { font-weight: 400; color: var(--lws-muted); }
.field .req, .form-legend .req { color: var(--lws-err); font-weight: 700; }
.form-legend { margin: 0 0 .25rem; font-size: .85rem; color: var(--lws-muted); }

.field input, .field textarea, .field select {
  width: 100%;
  padding: .7rem .85rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius);
  transition: border-color .18s ease, box-shadow .18s ease;
}
.field textarea { resize: vertical; min-height: 7rem; }
.field input:focus, .field textarea:focus, .field select:focus {
  outline: 0;
  border-color: var(--lws-brand);
  box-shadow: 0 0 0 3px var(--lws-brand-tint);
}
.field[data-invalid] input, .field[data-invalid] textarea, .field[data-invalid] select { border-color: var(--lws-err); }

/* Choices. Stacked is the default and what every existing form gets; the rest
   exist because a 24-option medical history is unreadable in one column and a
   1–10 pain scale is not a list. */
.choices { display: flex; flex-wrap: wrap; gap: .5rem .9rem; }
.choice { display: inline-flex; align-items: center; gap: .45rem; font-weight: 400 !important; }
.choice input { width: auto; flex: none; }

.choices[data-layout="stack"]  { flex-direction: column; align-items: flex-start; gap: .45rem; }
.choices[data-layout="inline"] { flex-direction: row; }
.choices[data-layout="cols-2"],
.choices[data-layout="cols-3"],
.choices[data-layout="cols-4"] { display: grid; gap: .5rem .9rem; }
.choices[data-layout="cols-2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.choices[data-layout="cols-3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.choices[data-layout="cols-4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* The scale: the number is the target, so the whole box is clickable and the
   tick box itself is hidden rather than shrunk to something unmissable-by-
   accident. Selection is drawn on the label via :has, with a focus ring that
   keeps it operable from the keyboard. */
.choices[data-layout="scale"] { display: flex; flex-wrap: wrap; gap: .4rem; }
.choices[data-layout="scale"] .choice {
  justify-content: center;
  min-width: 2.6rem; padding: .5rem .3rem;
  border: 1px solid var(--lws-line); border-radius: var(--lws-radius);
  background: var(--lws-surface);
  font-family: var(--lws-font-display); font-weight: 600;
  cursor: pointer;
  transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.choices[data-layout="scale"] .choice input {
  position: absolute; opacity: 0; width: 1px; height: 1px;
}
.choices[data-layout="scale"] .choice:hover { border-color: var(--lws-brand); }
.choices[data-layout="scale"] .choice:has(input:checked) {
  background: var(--lws-brand); border-color: var(--lws-brand); color: var(--lws-on-brand);
}
.choices[data-layout="scale"] .choice:has(input:focus-visible) {
  outline: 2px solid var(--lws-brand); outline-offset: 2px;
}

/* The tick-box matrix — body areas down, Left/Centre/Right across. */
.grid-scroll { overflow-x: auto; }
.choice-grid {
  width: 100%; border-collapse: collapse;
  font-family: var(--lws-font-display); font-size: .92rem;
}
.choice-grid th, .choice-grid td {
  padding: .5rem .6rem; border-bottom: 1px solid var(--lws-line); text-align: center;
}
.choice-grid thead th {
  font-weight: 600; color: var(--lws-muted);
  font-size: .82rem; letter-spacing: .04em; text-transform: uppercase;
}
.choice-grid tbody th[scope="row"] {
  text-align: left; font-weight: 600; color: var(--lws-ink); white-space: nowrap;
}
.choice-grid tbody tr:last-child th, .choice-grid tbody tr:last-child td { border-bottom: 0; }
.choice-grid input { width: 1.05rem; height: 1.05rem; }

/* A composite field's boxes sit side by side with their own small labels. */
.subfields { display: flex; gap: .6rem; }
.subfield { display: flex; flex-direction: column; gap: .2rem; flex: 1 1 0; min-width: 0; }
.subfield small { color: var(--lws-muted); font-size: .8rem; }

.form-note { padding: .85rem 1.1rem; border-radius: var(--lws-radius); margin-bottom: 1rem; }
.form-ok  { background: var(--lws-brand-tint); color: var(--lws-ok); border: 1px solid currentColor; }
.form-err { background: var(--lws-surface-2); color: var(--lws-err); border: 1px solid currentColor; }
.form-alt { margin-top: .85rem; font-size: .85rem; color: var(--lws-muted); }
.form-card button { margin-top: 1.25rem; }

/* Honeypot: off-screen rather than display:none, which some bots detect. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ── pricing ───────────────────────────────────────────────────────────────
   `data-n` is the number of plans, capped at four by the renderer. It exists
   because `auto-fit` gets two plans wrong in the way that matters: two cards
   in a 1280px wrap become two 620px-wide slabs, which is not a comparison, it
   is two posters. The count decides the track, and every count still collapses
   to one column on a phone.

   Nothing here dims a plan that is not the featured one. The recommended plan
   is marked by a ring and a badge — greying its neighbours out is a shop
   telling you the other things on the shelf are not worth having. */
.plans {
  display: grid;
  gap: var(--lws-gap);
  align-items: stretch;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
}
.plans[data-n="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 56rem; margin-inline: auto; }
.plans[data-n="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.plans[data-n="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }

.plan {
  position: relative;
  display: flex; flex-direction: column;
  padding: 1.75rem;
  background: var(--lws-surface);
  border: 1px solid var(--lws-line);
  border-radius: var(--lws-radius-lg);
  box-shadow: var(--lws-shadow);
}
/* The ring is an inset box-shadow, not a wider border: a 2px border on one
   card in a row makes it 2px taller than its neighbours and every heading
   across the row stops lining up. */
.plan.on {
  border-color: var(--lws-brand);
  box-shadow: var(--lws-shadow-lift), inset 0 0 0 1px var(--lws-brand);
}
.plan h3 { margin-bottom: .25rem; }

.plan-price {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: .35rem;
  margin: .75rem 0 0;
}
.plan-price strong {
  font-family: var(--lws-font-display);
  font-size: clamp(2rem, 3.4vw, 2.8rem);
  line-height: 1; color: var(--lws-ink);
}
.plan-pre { font-size: .9rem; color: var(--lws-muted); }
/* The slash is drawn, never typed — see the renderer. */
.plan-per { font-size: .95rem; color: var(--lws-muted); }
.plan-per::before { content: "/ "; }

.plan-for { margin-top: .75rem; color: var(--lws-body); font-size: .96rem; }
.plan-lines { margin-top: 1.25rem; }
/* Buttons sit on one line across the row however uneven the lists above them
   are — a price grid whose calls to action stagger reads as three unrelated
   cards. */
.plan-go { margin-top: auto; padding-top: 1.5rem; }
.plan-go .btn { width: 100%; justify-content: center; margin-top: 0; }

/* ROWS — one plan per full-width row. The shape a fee list wants: a clinic
   with eleven appointment types has eleven rows, not eleven cards, and the
   price sits in the same place down the left edge so the column can be read
   without moving your eyes across.

   `data-n` is ignored here on purpose; the count decides a track only when the
   plans stand beside each other. */
.section[data-block="pricing"][data-variant="rows"] .plans {
  grid-template-columns: 1fr; max-width: 62rem; margin-inline: auto;
}
/* EVERY CHILD IS PLACED BY NAME, never by auto-flow. A plan's parts are
   optional — no price, no description, no button — so the row a client gets
   depends on which of five elements exist, and auto-placement reshuffles the
   whole thing when one is missing. The first attempt did exactly that: the
   description landed in the tick column and the button under the list.
   `.badge` is deliberately not mentioned; it stays absolutely positioned
   against `.plan`, which is why it does not consume a cell. */
.section[data-block="pricing"][data-variant="rows"] .plan {
  display: grid;
  grid-template-columns: minmax(11rem, 1fr) minmax(0, 1.5fr) auto;
  grid-template-rows: auto auto auto;
  align-items: center;
  column-gap: var(--lws-gap);
  row-gap: .35rem;
}
.section[data-block="pricing"][data-variant="rows"] .plan > h3        { grid-area: 1 / 1; align-self: end; margin: 0; }
.section[data-block="pricing"][data-variant="rows"] .plan-price       { grid-area: 2 / 1; margin: 0; }
.section[data-block="pricing"][data-variant="rows"] .plan-for         { grid-area: 3 / 1; margin: 0; }
.section[data-block="pricing"][data-variant="rows"] .plan-lines       { grid-area: 1 / 2 / 4 / 3; margin: 0; }
.section[data-block="pricing"][data-variant="rows"] .plan-go          { grid-area: 1 / 3 / 4 / 4; margin: 0; padding: 0; }
.section[data-block="pricing"][data-variant="rows"] .plan-price strong { font-size: clamp(1.6rem, 2.4vw, 2.1rem); }
/* The button stops filling its column — in a row it is one control beside the
   list, not the foot of a card. */
.section[data-block="pricing"][data-variant="rows"] .plan-go .btn { width: auto; }

/* COMPACT — name and price, nothing else. For a rate card: several prices on
   one screen, no per-plan argument. Anything a plan carries beyond those two
   is hidden rather than shrunk, because a two-line description set in 12px is
   worse than no description. */
.section[data-block="pricing"][data-variant="compact"] .plans {
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
}
.section[data-block="pricing"][data-variant="compact"] .plan { padding: 1.25rem 1.4rem; }
.section[data-block="pricing"][data-variant="compact"] .plan-price strong { font-size: clamp(1.5rem, 2.2vw, 1.9rem); }
.section[data-block="pricing"][data-variant="compact"] .plan-for,
.section[data-block="pricing"][data-variant="compact"] .plan-lines { display: none; }
.section[data-block="pricing"][data-variant="compact"] .plan-go { padding-top: 1rem; }

@media (max-width: 979px) {
  .plans[data-n="3"], .plans[data-n="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Two columns: the identity block on the left, the list on the right, and
     the button on its own row underneath both. */
  .section[data-block="pricing"][data-variant="rows"] .plan {
    grid-template-columns: minmax(10rem, 1fr) minmax(0, 1.4fr);
    grid-template-rows: auto auto auto auto;
  }
  .section[data-block="pricing"][data-variant="rows"] .plan-lines { grid-area: 1 / 2 / 4 / 3; }
  .section[data-block="pricing"][data-variant="rows"] .plan-go    { grid-area: 4 / 1 / 5 / 3; padding-top: 1.25rem; }
}
@media (max-width: 639px) {
  .plans, .plans[data-n="2"], .plans[data-n="3"], .plans[data-n="4"] { grid-template-columns: 1fr; }
  /* One column, in reading order: name, price, who it is for, what is in it,
     and the button last. */
  .section[data-block="pricing"][data-variant="rows"] .plan {
    grid-template-columns: 1fr;
    grid-template-rows: repeat(5, auto);
  }
  .section[data-block="pricing"][data-variant="rows"] .plan-lines { grid-area: 4 / 1; margin-top: .75rem; }
  .section[data-block="pricing"][data-variant="rows"] .plan-go    { grid-area: 5 / 1; padding-top: 1.25rem; }
  .section[data-block="pricing"][data-variant="rows"] .plan-go .btn { width: 100%; justify-content: center; }
}

/* ── compare ───────────────────────────────────────────────────────────────
   A REAL TABLE, and still one on a phone. The usual responsive trick —
   display:block every row and stack it — throws away the header association,
   so a screen reader reads "yes" with nothing to attach it to. This scrolls
   sideways inside its own box instead, which is also the only layout in which
   six columns of ticks remain comparable. Nothing else on the page moves. */
.compare-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.compare {
  width: 100%; border-collapse: collapse;
  min-width: 34rem;                 /* narrower than this and the ticks lie on top of the labels */
  text-align: left;
}
.compare th, .compare td {
  padding: .95rem 1.1rem;
  border-bottom: 1px solid var(--lws-line);
  vertical-align: middle;
}
.compare thead td, .compare tfoot td:first-child { border-bottom-color: transparent; }
.compare thead th { text-align: center; }
.compare thead th strong {
  display: block;
  font-family: var(--lws-font-display); font-size: 1.15rem; color: var(--lws-ink);
}
.compare-note { display: block; margin-top: .2rem; font-size: .85rem; color: var(--lws-muted); }
.compare-flag {
  display: inline-block; margin-bottom: .4rem;
  padding: .18rem .6rem;
  background: var(--lws-brand); color: var(--lws-on-brand);
  border-radius: 999px;
  font-size: .72rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
}
.compare tbody th { font-weight: 600; color: var(--lws-ink); }
.compare tbody th small { display: block; margin-top: .15rem; font-weight: 400; font-size: .85rem; color: var(--lws-muted); }
.compare tbody td { text-align: center; color: var(--lws-body); }
.compare-yes svg { width: 1.25rem; height: 1.25rem; color: var(--lws-brand); stroke-width: 2.6; }
.compare-no  svg { width: 1.25rem; height: 1.25rem; color: var(--lws-muted); opacity: .55; }
/* The highlighted column is a tinted strip down the whole table, so the eye
   follows one option top to bottom instead of re-finding it on every row. */
.compare th.on, .compare td.on { background: var(--lws-brand-tint); }
.compare thead th.on { border-radius: var(--lws-radius) var(--lws-radius) 0 0; }
.compare tfoot td.on { border-radius: 0 0 var(--lws-radius) var(--lws-radius); }
.compare tfoot .btn { width: 100%; justify-content: center; margin-top: 0; }
.compare tbody tr:last-child th, .compare tbody tr:last-child td { border-bottom-color: transparent; }

/* Versus: two columns, so there is room to stop being a table and become two
   cards side by side. The row label moves above each value — which is what
   `data-col` on every cell is for. */
.section[data-block="compare"][data-variant="versus"] .compare { min-width: 0; }
.section[data-block="compare"][data-variant="versus"] .compare th,
.section[data-block="compare"][data-variant="versus"] .compare td { padding: 1.1rem 1.35rem; }
.section[data-block="compare"][data-variant="versus"] .compare tbody th { width: 38%; }

/* The hint under the table. Hidden wherever the table fits, which is every
   viewport wide enough to hold `.compare`'s 34rem plus the wrap's own gutters
   — call it 600px, and erring early is free: a sentence that appears one
   breakpoint too soon is a sentence that is still true. `versus` sets
   min-width: 0 and therefore never scrolls, so it never says this. */
.compare-hint { display: none; }
@media (max-width: 599px) {
  .section[data-block="compare"]:not([data-variant="versus"]) .compare-hint {
    display: block;
    margin-top: .75rem;
    font-size: .85rem; color: var(--lws-muted);
  }
  .section[data-block="compare"]:not([data-variant="versus"]) .compare-hint::after {
    content: " →"; }
}

@media (max-width: 639px) {
  .compare th, .compare td { padding: .8rem .7rem; }
  .compare thead th strong { font-size: 1rem; }
}

/* ── cta ───────────────────────────────────────────────────────────────── */

.cta-band { background: var(--lws-brand); color: var(--lws-on-brand); }
.cta-band > .wrap {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--lws-gap);
}
.cta-band h2 { color: var(--lws-on-brand); margin-bottom: .35rem; }
.cta-band p { color: var(--lws-on-brand-dim); max-width: 46ch; margin: 0; }
.cta-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; }
.cta-band .btn-primary { background: var(--lws-surface); color: var(--lws-brand); }
.cta-band .btn-primary:hover { background: var(--lws-surface-2); color: var(--lws-brand-deep); }
.cta-phone {
  font-family: var(--lws-font-display); font-size: 1.15rem; font-weight: 700;
  color: var(--lws-on-brand); text-decoration: none;
}
.cta-phone:hover { color: var(--lws-on-brand); text-decoration: underline; }

.section[data-block="cta"][data-variant="boxed"] { background: transparent; }
.section[data-block="cta"][data-variant="boxed"] > .wrap {
  padding: 2.5rem;
  background: var(--lws-brand);
  border-radius: var(--lws-radius-lg);
}
.section[data-block="cta"][data-variant="split"] > .wrap { justify-content: space-between; }

/* ── recruit ───────────────────────────────────────────────────────────── */

.highlight {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--lws-gap);
  padding: 2.25rem 2.5rem;
  background: var(--lws-brand); color: var(--lws-on-brand);
  border-radius: var(--lws-radius-lg);
}
.highlight h2 { color: var(--lws-on-brand); margin-bottom: .35rem; }
.highlight p { color: var(--lws-on-brand-dim); margin: 0; max-width: 52ch; }
.section[data-block="recruit"][data-variant="accent"] .highlight { background: var(--lws-accent); color: var(--lws-on-accent); }
.section[data-block="recruit"][data-variant="accent"] .highlight h2 { color: var(--lws-on-accent); }
.section[data-block="recruit"][data-variant="accent"] .highlight p { color: var(--lws-on-accent-dim); }
/* `ink` is a ground like brand and accent, so it needs its own text colours.
   Without them this band inherited `--lws-on-brand` from `.highlight` above —
   which on a dark theme is white, over an ink that is near-white: 1.09:1. */
.section[data-block="recruit"][data-variant="ink"] .highlight { background: var(--lws-ink); color: var(--lws-on-ink); }
.section[data-block="recruit"][data-variant="ink"] .highlight h2 { color: var(--lws-on-ink); }
.section[data-block="recruit"][data-variant="ink"] .highlight p { color: var(--lws-on-ink-dim); }

/* ── spacer ────────────────────────────────────────────────────────────── */

.spacer { padding-block: calc(var(--lws-section-y) * .5); }
.spacer hr { border-top-color: transparent; }
.section[data-block="spacer"][data-variant="rule"] hr { border-top-color: var(--lws-line); }
.section[data-block="spacer"][data-variant="motif"] hr {
  border: 0; height: 3px; width: 4rem; margin-inline: auto;
  background: var(--lws-accent); border-radius: 3px;
}

/* The hero's motif hook. Empty by default — a motif stylesheet gives it shape,
   which is how two sites on identical tokens still read as different brands. */
.motif-path { display: none; }

/* ═══ TEMPLATES ════════════════════════════════════════════════════════════ */

/* ── single post ───────────────────────────────────────────────────────── */

.post-head .crumbs { padding: 0 0 1rem; }
.post-head .crumbs ol { padding: 0; }
.post-head h1 { margin: .75rem 0 .5rem; max-width: 24ch; }
.post-head .pill { margin-bottom: .25rem; }
.post-meta { display: flex; flex-wrap: wrap; gap: .25rem 1rem; color: var(--lws-muted); font-size: .93rem; margin: 0; }
.post-author::before { content: "·"; margin-right: 1rem; }

.post-figure { margin-block: calc(var(--lws-section-y) * .5) 0; }
.post-figure img { width: 100%; border-radius: var(--lws-radius-lg); box-shadow: var(--lws-shadow); }

/* A post body is one prose block — centre its measure rather than letting it
   run the full wrap width, which is unreadable at desktop sizes. */
.post .section[data-block="richtext"] .prose,
.post .section[data-block="html"] .lws-html { margin-inline: auto; max-width: 72ch; }

/* ── archives ──────────────────────────────────────────────────────────── */

.pagination {
  display: flex; align-items: center; justify-content: center; gap: 1rem;
  margin-top: calc(var(--lws-gap) * 1.5);
}
.pagination-count { color: var(--lws-muted); font-size: .93rem; }

/* ── DB-less scaffold page ─────────────────────────────────────────────── */

.scaffold {
  max-width: 32rem; margin: 20vh auto; padding: 2rem;
  text-align: center;
}

/* ── edit-mode chrome ──────────────────────────────────────────────────── */

#lws-editbar {
  position: fixed; left: 50%; bottom: 1.25rem; z-index: 90;
  transform: translateX(-50%);
  display: flex; align-items: center; gap: .85rem;
  padding: .6rem 1.1rem;
  background: var(--lws-ink); color: var(--lws-on-ink);
  border-radius: 999px;
  box-shadow: var(--lws-shadow-lift);
  font-size: .9rem;
}
#lws-editbar .dot { width: .6rem; height: .6rem; border-radius: 50%; background: var(--lws-ok); }
#lws-editbar a { color: var(--lws-on-ink); text-decoration: underline; }
#lws-editon {
  position: fixed; right: 1.25rem; bottom: 1.25rem; z-index: 90;
  padding: .6rem 1.1rem;
  background: var(--lws-ink); color: var(--lws-on-ink);
  border-radius: 999px; box-shadow: var(--lws-shadow);
  font-size: .9rem; text-decoration: none;
}
[data-lws-edit] { outline: 1px dashed rgb(var(--lws-overlay-rgb) / .35); outline-offset: 3px; }
[data-lws-edit]:hover { outline-color: var(--lws-brand); outline-style: solid; }

/* A section that is not finished yet. Only ever rendered in edit mode — a
   visitor sees nothing at all, which is right; the person building the page
   needs to see that the section exists and what it is waiting for. */
.section.lws-blank .lws-blank-card {
  display: flex; flex-direction: column; align-items: center; gap: .6rem;
  max-width: 34rem; margin: 0 auto; padding: 2rem 1.5rem;
  text-align: center;
  border: 2px dashed rgb(var(--lws-overlay-rgb) / .28);
  border-radius: var(--lws-radius-lg);
  background: rgb(var(--lws-overlay-rgb) / .03);
}
.section.lws-blank .lws-blank-kind {
  font-family: var(--lws-font-display);
  font-size: .74rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--lws-brand);
}
.section.lws-blank p { margin: 0; color: var(--lws-muted); }
.section.lws-blank .lws-blank-go {
  padding: .55rem 1.1rem;
  font: inherit; font-weight: 600; line-height: 1.2; cursor: pointer;
  color: var(--lws-on-brand); background: var(--lws-brand);
  border: 0; border-radius: var(--lws-radius);
}
.section.lws-blank .lws-blank-go:hover { background: var(--lws-brand-deep); }

/* ═══ RESPONSIVE ═══════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {
  .wrap { width: min(100% - 2rem, var(--lws-wrap)); }

  /* Nav collapses to the hamburger. The primary CTA moves OUT of the menu and
     into the header bar — burying "Book" inside a hamburger is the single most
     expensive mobile mistake a bookings-driven business can make. */
  .nav-toggle { display: block; }
  .header-cta-mobile { display: inline-flex; padding: .6rem 1.1rem; font-size: .92rem; }

  .site-nav {
    position: absolute; top: 100%; left: 0; right: 0;
    display: none; flex-direction: column; align-items: stretch;
    gap: 0;
    padding: .75rem;
    background: var(--lws-header);
    border-top: 1px solid rgb(var(--lws-overlay-rgb) / .4);
    box-shadow: var(--lws-shadow-lift);
    max-height: calc(100vh - 4.5rem);
    overflow-y: auto;
  }
  .site-nav.open { display: flex; }
  .site-nav > a, .has-sub > a { padding: .8rem 1rem; }
  .site-nav .nav-cta { margin: .5rem 0 0; text-align: center; }

  .has-sub { flex-wrap: wrap; justify-content: space-between; }
  .has-sub > a { flex: 1; }
  .sub-menu {
    position: static; display: none;
    width: 100%; margin-top: .25rem;
    background: rgb(var(--lws-overlay-rgb) / .3);
    border: 0; box-shadow: none;
  }
  .sub-menu a { color: var(--lws-on-header); }
  .sub-menu a:hover { background: rgb(var(--lws-overlay-rgb) / .4); color: var(--lws-on-header); }
  .sub-menu .note { color: var(--lws-on-header-dim); }
  .has-sub:hover .sub-menu { display: none; }
  .sub-toggle[aria-expanded="true"] + .sub-menu { display: block; }

  .link-cols { columns: 2; }
}

@media (max-width: 767px) {
  .wrap, .wrap.narrow { width: min(100% - 2rem, var(--lws-wrap)); }

  /* Every two-column arrangement collapses on the SAME breakpoint as imported
     Kadence rows, so a page that mixes designed and migrated sections doesn't
     reflow in two stages. */
  .hero > .wrap,
  .page-hero[data-variant="media"] > .wrap,
  .wrap.split { grid-template-columns: 1fr; }

  /* Photo first: on a phone the image is the hook, the copy is the follow-up. */
  .hero > .wrap > .hero-media { order: -1; }
  .hero[data-variant="split-flip"] > .wrap > .hero-copy { order: 0; }
  .wrap.split.flip > *:first-child { order: 0; }

  /* The asymmetric hero decoration is a desktop flourish — at phone widths the
     offset accent and floating badge only crowd the photo. */
  .hero-img-accent, .hero-badge { display: none; }
  .hero-media .hero-img { border-radius: var(--lws-radius); }

  /* Stacked, the photo has the full column width, so the desktop ceiling
     translates into most of a phone screen before the reader reaches the copy.
     One token, re-set once. */
  :root { --lws-media-h: min(20rem, 42vh); }

  .section[data-block="richtext"][data-variant="two-col"] .prose { columns: 1; }
  .section[data-block="faq"][data-variant="two-col"] .faq-list { grid-template-columns: 1fr; }

  /* Thirds and quarters are narrower than a phone keyboard's worth of text.
     They pair up first and only go full-width at the phone breakpoint, so a
     CITY / POSTAL CODE / PHONE row degrades in two steps rather than one. */
  .field[data-w="third"], .field[data-w="quarter"],
  .form-copy[data-w="third"], .form-copy[data-w="quarter"] { grid-column: span 6; }
  .field[data-w="two-thirds"], .field[data-w="three-quarters"],
  .form-copy[data-w="two-thirds"], .form-copy[data-w="three-quarters"] { grid-column: span 12; }
  .choices[data-layout="cols-4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .choices[data-layout="cols-3"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* A tab strip that wraps onto three rows is worse than a scroller — the
     panel below it jumps every time the selected row changes height. */
  .panel-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    margin-bottom: 0;
    /* .panels is a grid, and a grid item's min-width is `auto` — without this
       the nowrap strip widens its own track and takes the page with it. The
       scroller only scrolls once it is allowed to be narrower than its
       content. */
    min-width: 0;
  }
  .panel-tabs::-webkit-scrollbar { display: none; }
  .panel-tab { flex: none; border-radius: var(--lws-radius); }
  .panels.is-live > .panel { border-radius: var(--lws-radius); border-top-color: var(--lws-line); margin-top: .5rem; }
  .share { justify-content: flex-start; }
  .section[data-block="cards"][data-variant="feature"] .card:first-child,
  .section[data-block="posts"][data-variant="feature"] .post-card:first-child { grid-column: auto; }

  .section[data-block="team"][data-variant="rows"] .member,
  .section[data-block="posts"][data-variant="list"] .post-card { flex-direction: column; }
  .section[data-block="team"][data-variant="rows"] .member-portrait,
  .section[data-block="posts"][data-variant="list"] .post-card-img { flex: auto; }
  .section[data-block="team"][data-variant="rows"] .member-portrait img { aspect-ratio: 4 / 5; }
  .section[data-block="posts"][data-variant="list"] .post-card-img img { aspect-ratio: 16 / 9; }

  .section[data-block="steps"][data-variant="timeline"] .step { padding-left: 3rem; }

  /* The agenda's date column is 7rem of a 20rem screen. Below this it stops
     being a column and goes back to the card layout, which is the same shape
     the cards variant already has here. */
  .section[data-block="events"][data-variant="agenda"] .event-card { display: flex; }
  .section[data-block="events"][data-variant="agenda"] .event-top {
    flex-direction: row; align-items: flex-start;
    justify-content: space-between; margin-bottom: 1.15rem;
  }
  .section[data-block="events"][data-variant="list"] .event-meta { flex-direction: column; gap: .5rem; }

  .cta-band > .wrap, .highlight { flex-direction: column; align-items: flex-start; }
  .highlight { padding: 1.75rem; }

  /* Two columns from here down, whatever the layout asked for — this outranks
     the `--footer-tracks` rule rather than the property, which is why that one
     is a rule and not an inline style. The wide column takes the full row. */
  .footer-cols { grid-template-columns: 1fr 1fr; }
  .footer-col-wide { grid-column: 1 / -1; }
  .footer-bottom .wrap { flex-direction: column; }

  .banner-inner { padding: 1.75rem; }
  .link-cols { columns: 1; }
  .post-head h1 { max-width: none; }
}

@media (max-width: 479px) {
  .wrap, .wrap.narrow { width: calc(100% - 1.75rem); }

  /* Below ~480 a two-up grid is narrower than the content needs; everything
     that is still side-by-side goes to one column here. The grid keeps its
     twelve tracks and the spans collapse — setting it to a single track
     instead leaves any `span` silently clamping, which works by accident and
     breaks the moment a span is added. */
  .field[data-w], .form-copy[data-w] { grid-column: span 12; }
  .choices[data-layout="cols-2"],
  .choices[data-layout="cols-3"],
  .choices[data-layout="cols-4"] { grid-template-columns: 1fr; }
  .subfields { flex-direction: column; }
  .stat-row { grid-template-columns: 1fr 1fr; }
  .footer-cols { grid-template-columns: 1fr; }
  .gallery { grid-template-columns: 1fr; }

  /* Full-width buttons below 480px — right for a button stacked in page copy,
     WRONG for the one button that shares a row with the logo and the burger.
     The header CTA is a `.btn`, so it took `width: 100%` too: its flex base
     size became the whole bar, the only item left that could shrink was the
     logo, and it did — 160px -> 84px on a 360px phone, halving the client's
     mark the moment a header CTA was switched on.
     Nothing looked broken, which is why this survived: the button read as a
     deliberate full-width CTA and the damage showed up on the logo, an element
     with no rule of its own anywhere near here. Found by measuring the header
     boxes rather than by looking at it.
     Excluded at the source rather than undone afterwards, so `minimal` — which
     shows this button at every width — is covered by the same line. Shrink is
     still allowed: a long label narrows the pill instead of pushing the page
     sideways (checked at 320px with "Book an appointment today"). */
  .btn:not(.header-cta-mobile) { width: 100%; }
  .actions { flex-direction: column; align-items: stretch; }
  .cta-actions { flex-direction: column; align-items: stretch; width: 100%; }
  .cta-actions .cta-phone { text-align: center; }

  .card-img { margin: -1.25rem -1.25rem 1rem; }
  .card { padding: 1.25rem; }
  .form-card { padding: 1.5rem; }
}

/* ── print ──────────────────────────────────────────────────────────────────
   A printed page is a DOCUMENT, not a screenshot of a website. Until 2026-08-04
   this was five lines that hid the header and hoped, and three things were
   wrong with what came out — only one of them cosmetic:

   · `.reveal` starts at `opacity: 0` under `.lws-js` and is un-hidden by an
     IntersectionObserver as you scroll. Print without scrolling to the bottom
     first — which is exactly what someone does when the Print button is at the
     TOP of the page — and every section they had not reached printed BLANK.
     Pages two and three of a handout were empty paper. This was the actual
     complaint; the rest is why it still looked bad once that was fixed.
   · `.section { break-inside: avoid }` cannot be honoured by a section holding
     a thousand words, so the browser fell back to throwing whole sections onto
     fresh sheets and leaving half-empty ones behind them.
   · Every absolute link printed its own URL after itself, mid-sentence,
     internal ones included: "Book a free consultation (https://…/book-…/)".

   What replaces it treats the sheet as stationery — the site's own letterhead
   at the top, a contact line running along the foot, body copy set for paper
   rather than for a 1280px viewport. Engine-wide on purpose: every page of
   every site prints this way, not only the handouts that prompted it.

   Deliberately NOT here: page numbers and a printed date. Every browser offers
   both in its own print dialogue, and a second set an inch above them is how a
   sheet starts to look like a fax. */

@page {
  /* The generous foot is where the running contact line lives — see .print-foot
     below, which is positioned into this margin rather than into the text. */
  margin: 15mm 14mm 20mm;
}

.print-head, .print-foot { display: none; }

@media print {

  /* ── the blank-page fix ─────────────────────────────────────────────────
     Visible on paper whether or not an IntersectionObserver ever reached it.
     Both selectors, because the animation is applied under `.lws-js` and the
     bare class is what the markup carries. */
  .reveal,
  .lws-js .reveal {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
    animation: none !important;
  }

  /* ── what has no business on paper ──────────────────────────────────────
     Buttons go as a class rather than one by one: a button is a dead grey
     rectangle in print, and that includes the "Print or save as PDF" button
     that started the job. `.cta-phone` is not a `.btn`, so a call-to-action
     band keeps its phone number and loses only its link. */
  .site-header, .site-footer, .site-topbar, .site-announce, .site-preview-note,
  .thumb-bar, .skip-link, .nav-toggle, .actions, .btn, .crumbs,
  .section[data-block="share"], .hero-media, .page-hero-media,
  #lws-editbar, #lws-editon {
    display: none !important;
  }

  /* Motif decorations are painted as background gradients, and browsers print
     backgrounds only if the reader has ticked a box most readers do not know
     about. Left in, they reserve their height and print as gaps. */
  .motif-path { display: none !important; }
  [data-motif] .section-head h2::after { content: none !important; }

  /* ── the page ───────────────────────────────────────────────────────────
     11pt on the root scales every rem-based margin in the design down to paper
     proportions in one move; the sizes below are in pt so they do not compound
     with it. */
  html { font-size: 11pt; }
  body {
    background: #fff !important;
    color: #000;
    font-size: 10.5pt;
    line-height: 1.45;
  }

  /* REPOINT THE TEXT TOKENS RATHER THAN CHASE THE SELECTORS THAT USE THEM.
     Every muted colour in this engine is one of these three, and each was
     chosen for contrast against a lit screen — on paper they print as a pale
     grey that reads as a printer running out of ink. One override darkens
     leads, intros, post dates, table rules, captions and every muted line the
     themes add later, without this file having to know their names.
     The brand and accent tokens are left alone: they colour backgrounds and
     borders, which print differently or not at all. */
  :root, [data-theme], [data-theme="dark"] {
    --lws-ink: #000 !important;
    --lws-body: #000 !important;
    --lws-muted: #3f3f3f !important;
    --lws-line: #bbb !important;
  }

  .section {
    padding-block: 0 !important;
    margin: 0 0 .5rem;
    background: none !important;
    background-image: none !important;
    border: 0 !important;
    box-shadow: none !important;
    /* Was `avoid`. See the note at the top. */
    break-inside: auto;
  }

  /* The layout is a sheet of paper wide, not a theme's wrap wide. */
  .wrap, .wrap.narrow {
    width: 100% !important;
    max-width: none !important;
    margin: 0 !important;
    padding: 0 !important;
  }
  .prose { max-width: none; }

  /* ── type ───────────────────────────────────────────────────────────────
     `break-after: avoid` on every heading is the rule that stops a page ending
     on "What actually helps" with the section itself overleaf. */
  h1, h2, h3, h4 {
    color: #000;
    break-after: avoid;
    break-inside: avoid;
  }
  h1 { font-size: 19pt; line-height: 1.15; margin: 0 0 .3rem; }
  h2 { font-size: 13.5pt; margin: 1.1rem 0 .3rem; }
  h3 { font-size: 11.5pt; margin: .85rem 0 .2rem; }
  .prose h2 { margin-top: 1.1rem; }
  .prose h3 { margin-top: .85rem; }
  p, li { orphans: 3; widows: 3; }
  p { margin: 0 0 .55rem; }
  .lead { font-size: 11.5pt; color: #222; margin-bottom: .7rem; }
  .eyebrow { font-size: 8.5pt; letter-spacing: .07em; color: #555; }
  .section-head { margin-bottom: .4rem; }

  ul, ol { margin: 0 0 .6rem; }
  li { break-inside: avoid; margin-bottom: .18rem; }
  .prose li::marker { color: #000; }

  /* ── tables ─────────────────────────────────────────────────────────────
     `display: table-header-group` repeats the header row on the second sheet,
     which is the difference between a strategies table that survives a page
     break and one whose left-hand column stops meaning anything. */
  table {
    width: 100%;
    border-collapse: collapse;
    font-size: 9.5pt;
    margin: .7rem 0;
    break-inside: auto;
  }
  thead { display: table-header-group; }
  tr { break-inside: avoid; }
  th, td {
    border: 1px solid #8a8a8a;
    padding: .3rem .45rem;
    text-align: left;
    vertical-align: top;
  }
  th { font-weight: 700; background: none !important; }

  /* ── links ──────────────────────────────────────────────────────────────
     Only genuinely off-site links are worth a printed URL, and lws_link_attrs()
     marks exactly those with target="_blank" — which is how this can tell the
     difference and why the old `[href^="http"]` rule could not. Scoped to body
     copy so a nav-shaped link never grows a URL either. */
  a { color: #000; text-decoration: none; }
  .prose a[target="_blank"]::after {
    content: " (" attr(href) ")";
    font-size: .8em;
    color: #444;
    word-break: break-all;
  }

  img {
    max-width: 100% !important;
    height: auto !important;
    border-radius: 0 !important;
    break-inside: avoid;
  }
  /* NO SINGLE PHOTOGRAPH GETS HALF A SHEET. A blog post's lead image printed
     473px tall on a text column 900px high — half the paper and most of the
     ink, for decoration, above an article somebody printed for its words.
     `width: auto` alongside it because these carry `width: 100%` on screen, and
     a capped height against a fixed width scales the picture wrong. */
  .post-figure img, .prose img, figure img {
    width: auto !important;
    max-height: 65mm;
  }
  /* A LISTING's thumbnails go entirely. Capping them instead left each card
     with a shrunken picture in one half and white space in the other, which
     looks like a rendering fault rather than a choice — and a "More reading"
     strip is a list of titles on paper either way. */
  .post-card img, .card-img { display: none !important; }

  /* ── accordions ─────────────────────────────────────────────────────────
     A closed <details> prints its question and swallows its answer, so a
     printed FAQ was a list of questions. engine.js opens them on `beforeprint`
     and closes them again afterwards, which is the only fix that works in every
     browser; this is the same fix for a reader with JavaScript off, and it
     works in Chrome 131+ only, where ::details-content exists. Both are here
     because neither covers everyone on its own.

     The marker goes either way — a "+" that cannot be clicked is furniture. */
  details::details-content { content-visibility: visible !important; }
  details > summary { list-style: none; }
  details > summary::marker,
  details > summary::-webkit-details-marker { content: none; display: none; }
  /* The engine draws its own "+" as summary::after and rotates it to a "×" when
     open. On paper it is furniture either way. */
  .faq-item summary::after,
  .panel > summary::after { content: none !important; display: none !important; }

  .faq-item, .panel {
    break-inside: avoid;
    background: none !important;
    border: 0 !important;
    border-bottom: .5pt solid #bbb !important;
    border-radius: 0 !important;
  }
  .faq-item summary, .panel > summary {
    padding: .25rem 0 !important;
    font-weight: 700;
    font-size: 11pt;
    background: none !important;
  }
  .faq-answer { padding: 0 0 .4rem !important; }
  .faq-list { gap: .35rem !important; }

  /* A call to action is worth keeping — it is the practice's own ask — but as a
     ruled box rather than a slab of colour. Same treatment .service-band gets
     further down this file. */
  .cta-band {
    /* `!important` because `.section { border: 0 !important }` above is the
       same specificity and would otherwise win — which it did, and the band
       printed as loose text with no box around it. */
    border: 1pt solid #000 !important;
    padding: .6rem .8rem !important;
    margin-top: .9rem;
    break-inside: avoid;
  }
  /* WHITE INK ON A BRAND-COLOURED SLAB is what this band is on screen, so with
     the background gone its heading printed white on white — an empty ruled box
     where the practice's own ask should be. The token override above cannot
     reach it: the colour here is literal, not a variable. */
  .cta-band, .cta-band *,
  .banner-inner, .banner-inner *,
  .member-namecard, .member-namecard *,
  .hero[data-variant="bleed"], .hero[data-variant="bleed"] * { color: #000 !important; }
  .cta-band h2 { font-size: 12pt; margin: 0 0 .2rem; }
  .cta-band p { margin: 0; }
  .cta-phone { display: inline-block; margin-top: .3rem; font-weight: 700; }

  .card, .post-card {
    background: none !important;
    border: 1px solid #bbb !important;
    box-shadow: none !important;
    break-inside: avoid;
  }

  /* ── letterhead and running foot ────────────────────────────────────────
     Markup for both is in engine/partials/{header,foot}.php. */
  .print-head {
    display: flex !important;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1.5rem;
    padding-bottom: .45rem;
    margin-bottom: 1.1rem;
    border-bottom: 1.5pt solid #000;
    break-after: avoid;
  }
  .print-logo { max-height: 13mm; width: auto; }
  .print-wordmark { font-size: 15pt; font-weight: 700; letter-spacing: -.01em; }
  .print-head-meta {
    display: flex;
    flex-direction: column;
    text-align: right;
    font-size: 8.5pt;
    line-height: 1.45;
    color: #333;
    white-space: nowrap;
  }

  /* ONCE, AT THE END OF THE DOCUMENT — NOT a per-sheet running footer.
     It was `position: fixed`, which in paged media means "repeat on every
     sheet", pushed into the @page margin with a negative offset so it could not
     collide with body copy. That reasoning was wrong and the paper proved it:
     on a four-page handout the contact line landed in the MIDDLE of pages two,
     three and four, interleaved character-by-character with the sentence it hit
     — "Predictabil1i-t9y0.2V-8is0u0-a0l3s85..." Chrome does not resolve that
     offset against the sheet the way the trick assumes.

     There is no reliable cross-browser way to put a running footer in the page
     margin — Chrome implements none of the @page margin boxes the paged-media
     spec defines for exactly this. So it runs once, in normal flow, where it
     cannot overlap anything by construction. The letterhead already carries
     identity at the top, and the browser's own print footer carries the page
     numbers.

     A SCREENSHOT CANNOT SHOW THIS. Under `Emulation.setEmulatedMedia` a fixed
     element is viewport-fixed and looks perfect; the corruption only exists in
     a real paginated PDF. Verify print changes with `node tools/print-pass.js`,
     which prints to PDF and reads the words back off the paper — tests/print.php
     is the source-level half and cannot see this at all. */
  .print-foot {
    display: block !important;
    position: static;
    margin-top: 1.2rem;
    padding-top: .3rem;
    border-top: .5pt solid #999;
    font-size: 8pt;
    color: #444;
    text-align: center;
    break-inside: avoid;
  }
}

/* ── reading time, open-now, thumb bar, before & after ──────────────────────
   Added 2026-07-27. Four small surfaces that share one rule: each renders
   nothing at all when its data is missing, rather than a chrome with a hole in
   it. `.post-read` rides the existing .post-meta flex row and needs no layout
   of its own. */

.post-read::before { content: '·'; margin-right: 1rem; color: var(--lws-line); }

/* The open/closed chip. Filled in by engine.js from a timetable, never by the
   server — an anonymous page is cached for five minutes and this is the one
   sentence on it that must be true right now. It ships `hidden` and the script
   removes that, so a browser with no JS shows nothing instead of an empty pill. */
.hours-now {
  display: inline-flex; align-items: center; gap: .4rem;
  margin: 0 0 1rem; padding: .3rem .7rem;
  border-radius: 999px; font-size: .9rem; font-weight: 600;
  background: var(--lws-surface-2); color: var(--lws-body);
}
.hours-now::before {
  content: ''; width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--lws-muted);
}
.hours-now.is-open::before { background: #17a34a; }
.hours-now.is-shut::before { background: var(--lws-muted); }

/* ── the thumb bar ────────────────────────────────────────────────────────
   Phones only, by CSS rather than by user agent: the markup goes to everyone
   because the page is cached and may be served to any device. Hidden above the
   phone breakpoint, where the header's own call and book buttons are visible
   anyway and a second pair would be clutter. */
.thumb-bar { display: none; }

@media (max-width: 767px) {
  .thumb-bar {
    display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
    background: var(--lws-surface); border-top: 1px solid var(--lws-line);
    /* Clears the iOS home indicator. Without this the bottom row of the bar
       sits under the system gesture area and the buttons are hard to hit. */
    padding-bottom: env(safe-area-inset-bottom, 0);
    box-shadow: 0 -2px 12px rgb(0 0 0 / 8%);
  }
  .thumb-bar a {
    display: flex; align-items: center; justify-content: center; gap: .45rem;
    padding: .85rem .5rem; text-decoration: none; font-weight: 700;
    /* 44px is the smallest reliably tappable target; this bar exists to be
       tapped one-handed while scrolling. */
    min-height: 3rem;
  }
  .thumb-bar a + a { border-left: 1px solid var(--lws-line); }
  .thumb-bar svg { width: 1.15rem; height: 1.15rem; fill: none;
                   stroke: currentColor; stroke-width: 1.8;
                   stroke-linecap: round; stroke-linejoin: round; }
  .thumb-call { color: var(--lws-body); }
  .thumb-book { background: var(--lws-brand); color: var(--lws-on-brand); }
  /* The bar covers the last of the page. Give the footer somewhere to end so
     the final line of a client's address is not permanently underneath it.
     `.site-footer` — this named `.site-foot`, which is the PARTIAL's filename
     and not a class any markup carries, so from the day the bar shipped until
     2026-07-29 the rule matched nothing and the bottom of every footer sat
     under the bar on every phone. */
  body:has(.thumb-bar) .site-footer { padding-bottom: 4.5rem; }

  /* And the same for the editor's own two pills, which are fixed at the same
     corner with a higher z-index — so on a phone they sat ON TOP of the Call
     and Book buttons. That was survivable while edit mode was on by default and
     "✎ Edit this page" was the rare state; since it became the default (see
     lws_edit_mode_on) it is what every signed-in editor sees on every page,
     covering the most valuable tap on the site while they check it. */
  body:has(.thumb-bar) #lws-editon,
  body:has(.thumb-bar) #lws-editbar { bottom: calc(3.75rem + env(safe-area-inset-bottom, 0px)); }
}

/* The edit bar is a centred pill sized by its own content, and its content is a
   sentence: edit-mode.js replaces "Edit mode" with "Edit mode — click any text
   to change it" the moment the script runs. On a phone that pill has nowhere to
   go, so it wrapped into a blob about 200px wide sitting over the middle of the
   page, with the status dot squeezed to a sliver by a flex row it could not
   shrink out of. It has always done this; edit mode being on by default is what
   made it the thing an editor saw on every page.

   A strip across the width instead — which is what a status bar is. */
@media (max-width: 479px) {
  #lws-editbar {
    left: .75rem; right: .75rem; transform: none;
    flex-wrap: wrap; justify-content: center; row-gap: .35rem;
    border-radius: 1rem; text-align: center;
  }
  /* `.6rem` is a WIDTH, and a flex item's width is a starting offer. Without
     this the dot is the only thing in the row with nothing to lose. */
  #lws-editbar .dot { flex: none; }
  #lws-editbar a { white-space: nowrap; }
}

/* ── before & after ──────────────────────────────────────────────────────
   The unenhanced state is two photos side by side — a real comparison, just
   not a draggable one. `.is-live` is added by engine.js and is what turns on
   the stacking, so a script that never runs cannot leave one photo hidden
   under another with no way to move it. */
.ba { margin: 0; position: relative; }
.ba-pane { position: relative; }
.ba-pane img { display: block; width: 100%; height: auto; border-radius: var(--lws-radius); }
.ba-tag {
  position: absolute; top: .6rem; left: .6rem;
  padding: .2rem .6rem; border-radius: 999px;
  background: rgb(0 0 0 / 62%); color: #fff;
  font-size: .8rem; font-weight: 700; letter-spacing: .02em;
}
.ba-cols, .ba:not(.is-live) { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; }
.ba-cols .ba-range, .ba:not(.is-live) .ba-range,
.ba-cols .ba-line,  .ba:not(.is-live) .ba-line { display: none; }
.ba figcaption { grid-column: 1 / -1; margin-top: .6rem;
                 color: var(--lws-muted); font-size: .93rem; }

.ba.is-live { display: block; --ba-pos: 50%; }
.ba.is-live .ba-pane { position: absolute; inset: 0; }
.ba.is-live .ba-before { position: relative; }   /* this one sets the height */
.ba.is-live .ba-pane img { height: 100%; object-fit: cover; }
/* clip-path, not width: the after photo must keep its own layout or the two
   images stop lining up as the slider moves and the comparison is a lie. */
.ba.is-live .ba-after { clip-path: inset(0 0 0 var(--ba-pos)); }
.ba.is-live .ba-after .ba-tag { left: auto; right: .6rem; }
.ba.is-live figcaption { position: static; }

.ba-line {
  position: absolute; top: 0; bottom: 0; left: var(--ba-pos);
  width: 2px; margin-left: -1px; background: #fff;
  box-shadow: 0 0 0 1px rgb(0 0 0 / 25%); pointer-events: none;
}
/* The range sits over the whole figure and is invisible except for its thumb,
   so the photo itself is the drag surface while the control keeping keyboard
   and screen-reader support is a genuine input. */
.ba-range {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; padding: 0; background: transparent; appearance: none;
  cursor: ew-resize;
}
.ba-range:focus-visible { outline: 3px solid var(--lws-brand); outline-offset: 3px; }
.ba-range::-webkit-slider-thumb {
  appearance: none; width: 2.2rem; height: 2.2rem; border-radius: 50%;
  background: #fff; border: 2px solid var(--lws-brand); cursor: ew-resize;
  box-shadow: 0 1px 6px rgb(0 0 0 / 30%);
}
.ba-range::-moz-range-thumb {
  width: 2.2rem; height: 2.2rem; border-radius: 50%;
  background: #fff; border: 2px solid var(--lws-brand); cursor: ew-resize;
}
.ba-range::-webkit-slider-runnable-track { background: transparent; }
.ba-range::-moz-range-track { background: transparent; }

@media (max-width: 479px) {
  /* Two photos side by side on a phone is two thumbnails. Stack them. */
  .ba-cols, .ba:not(.is-live) { grid-template-columns: 1fr; }
}

/* The compact review chip — the same rating markup the summary uses, boxed.
   Deliberately a wrapper rather than a second renderer: one source for the
   stars, the score and the AggregateRating schema, or they drift. */
.rating-chip {
  display: inline-flex; align-items: center;
  padding: .4rem .85rem; border-radius: 999px;
  background: var(--lws-surface-2); border: 1px solid var(--lws-line);
}
.rating-chip .rating { margin: 0; font-size: .95rem; }

/* ── service availability ───────────────────────────────────────────────────
   A service switched off in Settings marks itself everywhere it appears. The
   whole point is that none of this is authored: nobody adds these and, more
   importantly, nobody has to remember to take them off again when the service
   opens. So they are quiet by design — a clinic launching a service wants the
   page to sell it and the status to be honest, not the other way round.

   `accent-tint` rather than a warning colour: "coming soon" is good news for a
   growing practice, and a site that shouts a red banner at every visitor about
   a service it is about to launch reads as broken rather than as expanding. */

.service-band { background: var(--lws-accent-tint); padding-block: 1.75rem; }
.service-band .wrap { max-width: var(--lws-measure, 60ch); }
.service-band .eyebrow { margin-bottom: .35rem; }
.service-band-body { margin: 0; font-size: 1.05rem; }
.service-band-hiring { margin: .6rem 0 0; font-size: .95rem; color: var(--lws-muted); }

/* The card that replaces a closed service's treatments in a Booking section. */
.card.service-notice { background: var(--lws-accent-tint); border-style: dashed; }
.card.service-notice h4 { margin-top: 0; }
.card.service-notice .btn { margin-top: .75rem; align-self: flex-start; }
.service-hiring { margin: .75rem 0 0; font-size: .9rem; color: var(--lws-muted); }

/* The marker on a listing. Inline inside the link's own text, so it inherits
   the link colour and cannot end up as a floating badge over a card image. */
.chip-soon {
  display: inline-block; margin-left: .5rem;
  padding: .1rem .5rem; border-radius: 999px;
  font-size: .72em; font-weight: 600; letter-spacing: .02em;
  white-space: nowrap; vertical-align: middle;
  background: var(--lws-accent-tint); color: var(--lws-body);
  border: 1px solid var(--lws-line);
}
.pill .chip-soon { margin-left: .4rem; }

@media print { .service-band { background: none; border: 1px solid #000; } }
