/* Minimal reset + base styles for component files */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html, body { height: 100%; }
/* Native form controls paint themselves with the UA accent, which in every
   mainstream browser is BLUE — banned as a brand colour (design-system#21,
   and the measurement on it: waymore-web's Settings -> Availability rendered
   stock-blue "Active" checkboxes under every theme, because nothing here set
   accent-color). One declaration puts checkboxes, radios, range thumbs and
   <progress> on the theme's own primary. The glyph drawn ON the accent is the
   UA's to choose and it picks for contrast; --primary-fg on --primary is
   asserted >= 4.5:1 for every theme x scheme in scripts/check-contrast.mjs,
   so the accent is a legible ground either way. */
html { accent-color: var(--primary); }
body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--fg);
  font-size: clamp(1rem, 0.95rem + 0.25vw, 1.0625rem);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}
img, picture, video, canvas, svg { display: block; max-width: 100%; }
input, button, textarea, select { font: inherit; color: inherit; }
p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
h1, h2, h3, h4, h5, h6 { font-family: var(--font-display); line-height: 1.15; letter-spacing: -0.01em; }
/* Display headings set their own line breaks (0.16.0, design review H8). The
   hub's promise H2 — "Waymore Better (adj.) — obsessed over, curated,
   intentional, detailed." — broke to a ragged centred stack with "over,
   curated," stranded on its own line. `balance` evens the measure across
   lines instead of filling greedily.
   Scoped to h1-h3 on purpose: the algorithm is capped at a handful of lines
   in every engine that ships it, so applying it to body paragraphs buys
   nothing and costs layout work. Unsupported browsers ignore it and wrap as
   before — there is no fallback to write. */
h1, h2, h3, .wmb-h1, .wmb-h2, .wmb-h3, .card-title, .wmb-card-title { text-wrap: balance; }
h1 { font-size: clamp(2.25rem, 1.5rem + 3.5vw, 4.5rem); font-weight: 700; }
h2 { font-size: clamp(1.75rem, 1.25rem + 2.25vw, 3rem); font-weight: 700; }
h3 { font-size: clamp(1.375rem, 1.1rem + 1.1vw, 1.875rem); font-weight: 600; }
h4 { font-size: clamp(1.125rem, 1rem + 0.5vw, 1.375rem); font-weight: 600; }
a  { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }
:focus-visible { outline: 2px solid var(--ring, var(--primary)); outline-offset: 2px; border-radius: var(--radius-sm); }

/* Skip-to-content link — first focusable element in the DOM, hidden until
   focused, visible on focus. Target is <main id="main-content" tabindex="-1">. */
.skip-link {
  position: absolute; left: -9999px; top: auto;
  width: 1px; height: 1px; overflow: hidden;
  z-index: 9999;
  padding: 1rem 1.5rem;
  background: var(--fg); color: var(--bg);
  font-family: var(--font-body); font-size: 0.875rem; font-weight: 600;
  text-decoration: none; border-radius: 0 0 var(--radius) 0;
}
.skip-link:focus {
  position: fixed; left: 0; top: 0;
  width: auto; height: auto; overflow: visible;
}

/* Utility primitives used across components */
/* One content width for the whole family (0.16.0, design review F3). Reads
   --container, which --content-max now aliases. This narrows `.container`
   from 1200px to 1120px — the width the marketing shell's .wmb-max was
   already using — so the two stop disagreeing by 80px on the same page. */
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: clamp(1rem, 2vw, 2rem); }
.container-narrow { width: 100%; max-width: var(--container-narrow); margin-inline: auto; padding-inline: clamp(1rem, 2vw, 2rem); }
.stack > * + * { margin-top: 1rem; }
.btn, .wmb-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
  padding: 0.75rem 1.25rem; min-height: 48px; min-width: 48px;
  border-radius: var(--radius); border: 1px solid transparent;
  font-weight: 600; font-size: 0.9375rem; cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-soft), background var(--dur-fast) var(--ease-soft), border-color var(--dur-fast) var(--ease-soft);
  text-decoration: none;
}
.btn:hover, .wmb-btn:hover { text-decoration: none; }
.btn-primary, .wmb-btn.primary { background: var(--primary); color: var(--primary-fg); }
.btn-primary:hover, .wmb-btn.primary:hover { transform: translateY(-1px); filter: brightness(1.05); }
.btn-ghost, .wmb-btn.ghost { background: transparent; color: var(--fg); border-color: var(--border); }
.btn-ghost:hover, .wmb-btn.ghost:hover { background: var(--surface-2); }
/* light-on-primary variants (were .wmb-btn.light / .outline-light in studio.css) */
.btn-light, .wmb-btn.light { background: var(--bg); color: var(--primary); }
.btn-outline-light, .wmb-btn.outline-light { background: transparent; color: var(--bg); border-color: color-mix(in oklch, var(--bg) 40%, transparent); }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  box-shadow: var(--shadow-sm);
}
.eyebrow { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-muted); }
.muted { color: var(--fg-muted); }
.subtle { color: var(--fg-subtle); }

/* ---- Card title (0.16.0, design review S8 / S9 / S19) ---------------
   The missing step in the type scale. Card and sidebar titles were reaching
   for section-heading markup and landing at display size: Studios' contact
   sidebar rendered "Contact directly" and "What happens next" at roughly
   48px Fraunces inside a 320px card — larger than the section heading above
   them, and wrapping badly. The services add-on cards and the lookbook
   category rows did the same at ~28px over 13px body text.

   A card title is a title within a card, not a landmark. It sits between
   .wmb-h3 (1.25-1.625rem) and body copy, and it does NOT scale with the
   viewport: a card is roughly the same size at 1440 and 375, so its title
   should be too. Use it on whatever element the outline calls for — the
   heading LEVEL is a document-structure decision and this class does not
   touch it.

       <h2 class="card-title">Contact directly</h2>

   `.wmb-card-title` is the same rule under the marketing shell's naming,
   following `.btn, .wmb-btn`. */
.card-title, .wmb-card-title {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--fg);
  margin-bottom: 0.5rem;
}
.card-title em, .wmb-card-title em { font-style: var(--display-em-style); font-weight: var(--display-em-weight, inherit); color: var(--primary); }

/* ---- Links inside body copy (0.16.0, design review S16) -------------
   The global `a` rule is `text-decoration: none`, which is right for nav,
   buttons and card links — they have position, weight or a box to mark them
   as interactive. It is wrong inside a paragraph: Studios' inline "lookbook"
   link measured 1.01:1 AGAINST THE SURROUNDING TEXT, i.e. colour alone
   distinguished it and the colour was nearly identical. That fails WCAG
   1.4.1 Use of Colour regardless of its contrast against the background.

   Any run of body copy gets underlined links. Opt in with `.prose` (generic)
   or `.wmb-copy` (the marketing shell, in studio.css).

   `from-font` keeps the underline on the font's own metrics where the font
   provides them, and the 0.15em offset clears Fraunces' and Inter's
   descenders. `skip-ink` is the browser default and is left alone. */
.prose a, .prose a:hover {
  text-decoration: underline;
  text-decoration-thickness: from-font;
  text-underline-offset: 0.15em;
}

/* ---- Horizontal scroll for wide content (0.16.0, design review S10) --
   Studios' services comparison table is 513px wide inside a 327px column at
   375px: the Brand + Site and Retainer columns were simply clipped, with
   nothing on screen to suggest more existed.

   Wrap the wide thing:

       <div class="table-scroll" role="region" aria-label="Package comparison"
            tabindex="0"><table>…</table></div>

   The affordance is a fade at whichever edge has content beyond it — right
   at the start, both in the middle, left at the end. It is painted with a
   mask so it works over any themed ground, including one the scrolling
   content paints over itself.

   WHY THIS IS NOT A SCROLL-DRIVEN ANIMATION (0.16.1). 0.16.0 drove the fade
   with `animation-timeline: scroll(self inline)`. That shipped a bug: a
   scroll timeline's range is ZERO-LENGTH when the element cannot scroll, so
   progress pins at 0 — which is also what "scrolled to the far left" looks
   like. The fade therefore stayed on and advertised columns that were not
   hidden (seen at 1440 on waymorebetterstudios.com/services.html, which
   worked around it with a hard-coded 606px breakpoint).

   This is not a tuning problem. A scroll timeline cannot distinguish "at the
   start" from "cannot scroll", because that fact is not in the timeline —
   the only input is progress along a range, and both states are progress 0.
   No keyframe arrangement recovers it. Scroll-state container queries
   (`@container scroll-state(scrollable: right)`) express it exactly, but
   land in one engine today, not three.

   So the state is MEASURED. `component-init.js` sets data-scroll-x to one of
   none | start | middle | end from scrollWidth, clientWidth and scrollLeft,
   on a ResizeObserver (viewport, font load, content change) and a passive
   scroll listener. Plain arithmetic, identical in Chromium, Gecko and
   WebKit, and exact rather than inferred.

   WITHOUT JS there is NO fade. That is deliberate: the alternative shipped
   in 0.16.0 was a fade that is always on, which lies about the content
   whenever the table fits. A missing affordance is a smaller failure than a
   false one, and the table still scrolls — by drag, by wheel, by keyboard
   and by the scrollbar the box shows when it overflows. Each selector below
   is keyed on an explicit attribute value, so nothing paints until something
   has measured.

   `tabindex="0"` is set by the class rather than left to the page: a scroll
   container only a mouse can reach fails SC 2.1.1. Consumers add
   `role="region"` and an `aria-label` so it is announced as something.

   --table-scroll-fade sizes the gradient; set it to 0 to disable the fade
   without giving up the measurement or the focus ring. */
/* NO `scrollbar-gutter: stable` HERE, AND THAT IS LOAD-BEARING (0.16.1).
   0.16.0 set it "so the fade does not sit on top of a visible bar". On a box
   that only scrolls horizontally there is no vertical bar to reserve for, so
   what it actually reserved was 15px of unreachable space on every
   classic-scrollbar platform — and Chromium counts that gutter inside
   `scrollWidth` while refusing to scroll into it. Measured on the CI image:
   scrollWidth 2710, clientWidth 320, so the arithmetic says the maximum
   offset is 2390, and the element stops dead at 2375. "Scrolled to the end"
   was therefore never reachable, and the right-hand fade stayed on at the
   far right of the table — the same false affordance this release is about,
   for every Windows and Linux reader. macOS overlay scrollbars reserve zero
   width, so it was invisible locally and only CI saw it. It also meant 15px
   of a column was clipped behind the gutter with no way to bring it into
   view, which is a reason to drop it even with the fade out of the picture.
   The vertical axis is left at its computed `auto` rather than pinned to
   `hidden`: this box sets no height, so nothing overflows vertically and no
   bar appears, and if a consumer does add a height they get a scrollbar they
   can use instead of content silently clipped. `component-init.js` measures
   that bar's width and takes it out of the maximum, so the fade stays honest
   in that case too. */
.table-scroll {
  --table-scroll-fade: 3rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.table-scroll:focus-visible { outline: 2px solid var(--ring, var(--primary)); outline-offset: 2px; }
.table-scroll > table { min-width: max-content; }

/* One gradient per state rather than two mask layers composited together:
   `mask-composite` is the natural way to write "fade both edges" and is the
   least evenly supported part of masking, and a single multi-stop gradient
   needs none of it. */
.table-scroll[data-scroll-x="start"] {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--table-scroll-fade)), transparent 100%);
          mask-image: linear-gradient(to right, #000 calc(100% - var(--table-scroll-fade)), transparent 100%);
}
.table-scroll[data-scroll-x="middle"] {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--table-scroll-fade), #000 calc(100% - var(--table-scroll-fade)), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 var(--table-scroll-fade), #000 calc(100% - var(--table-scroll-fade)), transparent 100%);
}
.table-scroll[data-scroll-x="end"] {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--table-scroll-fade), #000 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 var(--table-scroll-fade), #000 100%);
}
/* data-scroll-x="none" — it fits; no mask, and nothing to say. */

@media (prefers-reduced-motion: reduce) {
  /* The fade does not animate, so there is nothing to disable; the scroll
     itself is the user's, not ours. Listed so a future author does not add
     a transition here without thinking about it. */
  .table-scroll { scroll-behavior: auto; }
}

/* ---- Status badge (0.16.0, design review H7) ------------------------
   The hub's family grid painted LIVE as a mint-green pill: the only green on
   the site, off every theme's palette, and fixed regardless of theme. This
   badge is the page's own surface, hairline and muted text, with a single
   dot in the theme's accent — so it follows a theme switch and adds no
   colour the contrast suite does not already measure.

       <span class="badge-status is-live">Live</span>
       <span class="badge-status is-soon">Coming</span>
       <span class="badge-status is-off">Paused</span>

   The LABEL is the state. The dot is decoration — see the token comment in
   tokens.css: --accent measures 2.35-2.56:1 against --bg/--surface on the
   cooler themes, so a dot-only badge would not clear SC 1.4.11's 3:1 and
   would also fail SC 1.4.1. Always ship the word. */
.badge-status, .wmb-badge-status {
  display: inline-flex; align-items: center; gap: 0.4375rem;
  padding: 0.25rem 0.625rem 0.25rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--badge-border);
  background: var(--badge-bg);
  color: var(--badge-fg);
  font-family: var(--font-body);
  font-size: 0.75rem; font-weight: 600;
  letter-spacing: 0.02em; line-height: 1.4;
  white-space: nowrap;
}
.badge-status::before, .wmb-badge-status::before {
  content: ''; flex: none;
  width: 0.5rem; height: 0.5rem; border-radius: 999px;
  background: var(--badge-dot-off);
}
.badge-status.is-live::before, .wmb-badge-status.is-live::before { background: var(--badge-dot-live); }
.badge-status.is-soon::before, .wmb-badge-status.is-soon::before { background: var(--badge-dot-soon); }
.badge-status.is-off::before,  .wmb-badge-status.is-off::before  { background: var(--badge-dot-off); }

/* ---- Motion primitive (0.16.0, design review F4) --------------------
   One shared motion language. There was none: hover states were colour-only
   and the Studios marquee was the only moving thing on four sites.

   `.lift` — a card raises 2px and gains a shadow on hover or keyboard focus.
   Focus is included on purpose; a hover-only affordance does not exist for a
   keyboard user.

       <article class="card lift">…</article>

   `.reveal` — an 8px fade-up on section entry. It is INERT until something
   marks the page ready: `component-init.js` sets `data-reveal="on"` on <html>
   after it registers an IntersectionObserver, and only then does .reveal
   start hidden. Without JS, with JS that failed, or in a crawler, every
   .reveal element is simply visible. Content that needs script to appear is
   content that disappears, and this one cannot.

       <section class="reveal">…</section>

   Both are disabled under prefers-reduced-motion. tokens.css's global reduce
   block zeroes durations, which would otherwise leave a reduced-motion
   viewer with an instant 2px JUMP and an instant 8px jump — motion made
   fast, not removed. The rules below drop the transforms outright. */
.lift, .wmb-lift {
  transition: transform var(--lift-dur) var(--ease-out), box-shadow var(--lift-dur) var(--ease-out);
}
.lift:hover, .wmb-lift:hover,
.lift:focus-within, .wmb-lift:focus-within {
  transform: translateY(var(--lift-y));
  box-shadow: var(--shadow);
}
[data-reveal="on"] .reveal, [data-reveal="on"] .wmb-reveal {
  opacity: 0;
  transform: translateY(var(--reveal-y));
  transition: opacity var(--reveal-dur) var(--ease-out), transform var(--reveal-dur) var(--ease-out);
}
[data-reveal="on"] .reveal.is-in, [data-reveal="on"] .wmb-reveal.is-in {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .lift, .wmb-lift { transition: none; }
  .lift:hover, .wmb-lift:hover,
  .lift:focus-within, .wmb-lift:focus-within { transform: none; }
  [data-reveal="on"] .reveal, [data-reveal="on"] .wmb-reveal {
    opacity: 1; transform: none; transition: none;
  }
}

/* ---------------------------------------------------------------------------
   .wmb-marquee: a slow, looping row of chips (0.14.0)
   Markup: <div class="wmb-marquee" data-label="…"><ul class="wmb-marquee__list">
   <li class="wmb-marquee__chip">…</li></ul></div>. component-init.js wraps the
   list, appends an aria-hidden copy and a Pause/Play button (WCAG 2.2.2), then
   sets data-ready; until then (or without JS) the chips are a static wrapped
   row. Pauses on hover, keyboard focus and the button. Reduced motion: static.
   --------------------------------------------------------------------------- */
.wmb-marquee { position: relative; }
.wmb-marquee__list { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.75rem; margin: 0; padding: 0; list-style: none; }
.wmb-marquee__chip {
  display: inline-flex; align-items: center; gap: 0.5rem; white-space: nowrap;
  padding: 0.75rem 1.375rem; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border);
  color: var(--fg); font-size: 0.9375rem; line-height: 1.2;
}
.wmb-marquee__chip b { font-family: var(--font-display); font-weight: 600; color: var(--primary); }
/* ---- Before JS, and without it (0.17.0, design-system#24) -----------
   Until component-init.js sets data-ready, the marquee is a static wrapped
   row — and a chip is `white-space: nowrap`, so a chip whose text is wider
   than the viewport simply overhangs it. Nothing clipped: `overflow: hidden`
   lives under [data-ready], on an element that does not exist yet.

   Measured at 320px with JS disabled, using the live chips from
   waymorebetterstudios.com's homepage:

     widest chip                                340px
     document.scrollingElement.clientWidth      320px
     document.scrollingElement.scrollWidth      330px   <- sideways scroll

   330 rather than 340 because the list is centred, so the overhang is split
   and only the right half counts toward scrollWidth. That is the "~10px"
   in the report, and it is the same 10px with JS ENABLED between first paint
   and initMarquees() — a race, which is why it read as intermittent.

   The fix lets the chip WRAP rather than clipping its ancestor. Clipping
   would also stop the scroll, and it would do it by hiding words: these
   chips are the page's claims, not decoration. A two-line pill at 320px is
   worse-looking and better-reading, and it only ever happens on a chip that
   genuinely cannot fit. Once the track is built the rule stops applying and
   the ticker keeps its single-line nowrap.

   `overflow-x: clip` is the backstop for a future child that cannot wrap —
   an image, an unbroken URL. `clip` and not `hidden` on purpose: `hidden`
   makes a scroll container, so content pushed out of view would still be
   programmatically scrollable and reachable only by script. Nothing should
   ever reach it; it is here so that "nothing widens the page" is a property
   of the component rather than of the content someone puts in it. */
.wmb-marquee:not([data-ready]) { overflow-x: clip; }
.wmb-marquee:not([data-ready]) .wmb-marquee__chip { white-space: normal; }

.wmb-marquee[data-ready] { padding-right: 3rem; }
.wmb-marquee[data-ready] .wmb-marquee__viewport {
  overflow: hidden;
  mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
}
.wmb-marquee[data-ready] .wmb-marquee__track { display: flex; width: max-content; animation: wmb-marquee var(--marquee-duration, 45s) linear infinite; }
.wmb-marquee[data-ready] .wmb-marquee__list { flex-wrap: nowrap; padding-right: 0.75rem; }
.wmb-marquee:hover .wmb-marquee__track,
.wmb-marquee:focus-within .wmb-marquee__track,
.wmb-marquee[data-paused] .wmb-marquee__track { animation-play-state: paused; }
.wmb-marquee__toggle {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  width: 2.25rem; height: 2.25rem; padding: 0; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface); color: var(--fg);
  display: grid; place-items: center; cursor: pointer;
}
@keyframes wmb-marquee { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  .wmb-marquee[data-ready] { padding-right: 0; }
  .wmb-marquee[data-ready] .wmb-marquee__viewport { mask-image: none; -webkit-mask-image: none; }
  .wmb-marquee[data-ready] .wmb-marquee__track { animation: none; width: auto; display: block; }
  .wmb-marquee[data-ready] .wmb-marquee__list { flex-wrap: wrap; padding-right: 0; }
  .wmb-marquee__list[aria-hidden="true"], .wmb-marquee__toggle { display: none; }
}

/* ---------------------------------------------------------------------------
   .wmb-media + .wmb-media-badge: a card overlapping a photo's corner (0.14.0)
   Default: icon + title + detail. .is-stat: one big value + label.
   .is-right / .is-top move it. At <=560px it sits under the photo, never on a face.
   .wmb-thumb + .wmb-thumb-pill: the compact version for grid thumbnails.
   A badge carries a confirmed fact only. No fact, no badge.
   --------------------------------------------------------------------------- */
.wmb-media { position: relative; }
.wmb-media > img { display: block; width: 100%; height: auto; border-radius: var(--radius-xl); }
.wmb-media-badge {
  position: absolute; bottom: 1.5rem; left: -1.25rem; max-width: 19rem;
  display: flex; align-items: center; gap: 0.875rem;
  padding: 1rem 1.25rem; border-radius: var(--radius-lg);
  background: var(--surface); color: var(--fg);
  box-shadow: 0 18px 40px -20px color-mix(in oklch, var(--fg) 55%, transparent);
}
.wmb-media-badge.is-right { left: auto; right: -1.25rem; }
.wmb-media-badge.is-top { bottom: auto; top: 1.5rem; }
.wmb-media-badge__icon {
  flex: none; width: 2.5rem; height: 2.5rem; border-radius: 999px; display: grid; place-items: center;
  background: color-mix(in oklch, var(--primary) 14%, var(--surface)); color: var(--primary);
}
.wmb-media-badge__title { display: block; font-weight: 600; font-size: 0.875rem; line-height: 1.35; }
.wmb-media-badge__detail { display: block; font-size: 0.75rem; color: var(--fg-muted); line-height: 1.4; }
.wmb-media-badge.is-stat { display: block; text-align: center; padding: 1rem 1.375rem; }
.wmb-media-badge__value { display: block; font-family: var(--font-display); font-size: 1.75rem; font-weight: 600; line-height: 1; letter-spacing: -0.02em; }
@media (max-width: 560px) {
  .wmb-media-badge, .wmb-media-badge.is-right, .wmb-media-badge.is-top { position: static; margin-top: 0.875rem; max-width: none; }
  .wmb-media-badge.is-stat { display: inline-block; }
}
.wmb-thumb { position: relative; display: block; }
.wmb-thumb > img { display: block; width: 100%; height: auto; }
.wmb-thumb-pill {
  position: absolute; left: 0.875rem; bottom: 0.875rem;
  display: inline-flex; align-items: baseline; gap: 0.4rem;
  padding: 0.45rem 0.8rem; border-radius: 999px;
  background: var(--surface); color: var(--fg); font-size: 0.75rem; line-height: 1.2;
  box-shadow: 0 10px 24px -14px color-mix(in oklch, var(--fg) 60%, transparent);
}
.wmb-thumb-pill b { font-family: var(--font-display); font-size: 1rem; color: var(--primary); }

/* ---- The accent, where the eye goes (0.17.0) ------------------------
   Theme is the palette; PROFESSION is the audience. A profession moved
   --accent and nothing else, and --accent was painted three times in
   this whole package: a status dot, a footer `em`, and one gradient.
   Measured on the live lookbook on 2026-10-17, switching Therapist ->
   Consultant at a fixed theme changed 0.00% of the pixels on the
   services fixture and on pages/therapist.html. The control existed and
   did nothing.

   These rules put the accent on the places a reader actually looks:
   the eyebrow above a heading, the mark under a phrase, the chip beside
   an icon, the number on a step, the quote glyph, the rule between
   sections, the border on the thing that is selected.

   TEXT TAKES --accent-ink, NOT --accent. --accent is a decoration token
   with a 3:1 non-text bar and nine by-name contrast excuses behind it
   (check-contrast.mjs); it measures 2.35:1 against --bg on coast. The
   ink form is the accent mixed 70% toward --fg, asserted at the full
   4.5:1 on --bg AND --surface across every theme x profession x scheme.
   Fills and strokes — where the 3:1 bar is the right one — keep --accent.
   Do not "simplify" an ink back to an accent. */

/* The eyebrow is the single highest-leverage one: it sits above almost
   every heading on every surface in the family, and it was --fg-muted,
   i.e. the same grey as the body copy it was meant to be distinct from. */
.eyebrow { color: var(--accent-ink); }

/* `.mark` — a highlight under a phrase inside a heading. A gradient
   rather than text-decoration so the weight is ours and it sits behind
   the glyphs' descenders instead of cutting them. */
.mark, .wmb-mark {
  background-image: linear-gradient(to top,
    color-mix(in oklch, var(--accent) 38%, transparent) 0,
    color-mix(in oklch, var(--accent) 38%, transparent) 0.32em,
    transparent 0.32em);
  background-repeat: no-repeat;
  padding-inline: 0.05em;
}
/* `.is-underline` — the same mark as a rule under the whole phrase, for
   display sizes where a highlight band reads as a redaction. */
.mark.is-underline, .wmb-mark.is-underline {
  background-image: linear-gradient(to top,
    var(--accent) 0, var(--accent) 0.09em, transparent 0.09em);
}

/* `.icon-chip` — the round plate an icon sits on. Accent FILL (3:1 bar,
   and the icon stroke carries no meaning the label does not). */
.icon-chip, .wmb-icon-chip {
  flex: none; display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem; border-radius: var(--radius);
  background: color-mix(in oklch, var(--accent) 16%, var(--surface));
  color: var(--accent-ink);
  border: 1px solid color-mix(in oklch, var(--accent) 32%, var(--border));
}
.icon-chip.is-round, .wmb-icon-chip.is-round { border-radius: 999px; }
.icon-chip.is-solid, .wmb-icon-chip.is-solid {
  background: var(--accent); color: var(--accent-fg); border-color: transparent;
}
.icon-chip > svg, .wmb-icon-chip > svg { width: 1.125rem; height: 1.125rem; }

/* `.step-num` — the numeral on a numbered step. Display face on purpose:
   it is the profession's type personality at its most legible. */
.step-num, .wmb-step-num {
  flex: none; display: grid; place-items: center;
  width: 2.25rem; height: 2.25rem; border-radius: 999px;
  background: var(--accent); color: var(--accent-fg);
  font-family: var(--font-display); font-size: 1.0625rem; font-weight: 600;
  line-height: 1; font-variant-numeric: tabular-nums;
}
.step-num.is-outline, .wmb-step-num.is-outline {
  background: transparent; color: var(--accent-ink);
  border: 2px solid var(--accent);
}

/* `.quote-mark` — the oversized opening quote above a pull quote.
   aria-hidden in the markup: it is punctuation the sentence already has. */
.quote-mark, .wmb-quote-mark {
  display: block; font-family: var(--font-display);
  font-size: 3.5rem; line-height: 0.7; color: var(--accent);
  margin-bottom: 0.25rem;
}

/* `.accent-rule` — a divider that starts in the accent and fades out.
   `.is-short` is the 2.5rem stub that sits under an eyebrow. */
.accent-rule, .wmb-accent-rule {
  border: 0; height: 2px; margin: 0;
  background: linear-gradient(to right, var(--accent), color-mix(in oklch, var(--accent) 0%, transparent));
}
.accent-rule.is-short, .wmb-accent-rule.is-short {
  width: 2.5rem; background: var(--accent);
}

/* Selected and current state. Hover is NOT in here: `.lift` (above)
   owns hover and focus-within, and a colour-only hover is not a state a
   keyboard or touch user can reach. These are for the card that IS the
   chosen one — a selected plan, the current step, today's date. */
.card.is-selected, .card[aria-current="true"],
.wmb-card.is-selected, .wmb-card[aria-current="true"] {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent), var(--shadow);
}
.card.is-selected .eyebrow, .card[aria-current="true"] .eyebrow { color: var(--accent-ink); }

/* ===========================================================================
   NATIVE OVERLAYS (0.19.0) — <dialog>, popover, anchor positioning
   ---------------------------------------------------------------------------
   WHAT WAS WRONG, MEASURED. The lookbook's four overlay primitives
   (primitives/modal, drawer, tooltip, dropdown-menu) and booking/booking-popup
   were `div.scrim` + a class toggle. Across those five fixtures, grepped on
   2026-10-17: `role="dialog"` appeared ONCE, `aria-modal` zero times,
   `aria-expanded` zero times, `<dialog>` zero times, `popover` zero times.
   None of them trapped focus, none returned focus to the control that opened
   them, and the page behind an open modal stayed fully tabbable. A visitor
   using a keyboard could Tab straight out of an open dialog into the page it
   was covering, with no way to know they had left.

   Every one of those is free from the platform, so this layer does not
   implement any of them:

     <dialog>.showModal()  puts the element in the TOP LAYER, renders
                           ::backdrop, makes everything else INERT (not a
                           class — the actual inert semantic, so the
                           background is unfocusable, unclickable and hidden
                           from assistive technology), closes on Esc, and
                           restores focus to the previously focused element
                           on close. role="dialog" and aria-modal="true" are
                           implicit; writing them is noise at best and a
                           conflict at worst.
     [popover]             top layer, light-dismiss on an outside click, Esc,
                           and — with popovertarget on the invoker — an
                           implicit aria-expanded and aria-details pair.
     anchor positioning    the popover follows its trigger without a scroll
                           or resize listener, and flips off the viewport
                           edge by itself.

   THE THREE POSITIONING TIERS for .wmb-popover, best first. Tier 1 is CSS
   only; tier 2 is component-init.js, which runs ONLY when tier 1 is absent;
   tier 3 is what a visitor gets with neither, and it is a usable centred
   sheet rather than a panel stuck in the wrong corner.

   WHY `margin: auto` IS LOAD-BEARING on .wmb-dialog. A top-layer dialog is
   `position: fixed` with `inset: 0` from the UA stylesheet, so its margin is
   what centres it. Setting `margin: 0` to "reset" it pins the dialog to the
   top-left corner of the viewport in every engine.
   =========================================================================== */

/* ---- Modal dialog --------------------------------------------------- */
.wmb-dialog {
  margin: auto;
  padding: 0;
  width: min(32rem, calc(100vw - 2rem));
  max-width: none;
  max-height: calc(100dvh - 2rem);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-elev);
  color: var(--fg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.wmb-dialog.is-narrow { width: min(26rem, calc(100vw - 2rem)); }
.wmb-dialog.is-wide   { width: min(44rem, calc(100vw - 2rem)); }

/* A <dialog> is display:block when open and display:none when not; the
   column layout below is what lets __body scroll while __head and __foot
   hold still, so it has to be re-stated on the open state. */
.wmb-dialog[open] { display: flex; flex-direction: column; }

/* The literal fallback is not belt-and-braces. ::backdrop inheriting custom
   properties from its originating element is a 2023 spec change; where that
   has not landed, a bare var(--scrim) resolves to nothing and the modal gets
   no scrim — a transparent overlay over a live page. */
.wmb-dialog::backdrop { background: var(--scrim, oklch(12% 0.01 0 / 0.5)); }

.wmb-dialog__head {
  display: flex; align-items: flex-start; gap: 1rem;
  padding: 1.25rem 1.5rem 0.5rem;
}
.wmb-dialog__title {
  flex: 1; margin: 0;
  font-family: var(--font-display); font-size: 1.125rem; font-weight: 500; line-height: 1.3;
}
.wmb-dialog__close {
  flex: none; width: 2.25rem; height: 2.25rem; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--fg-muted); cursor: pointer;
}
.wmb-dialog__close:hover { background: var(--surface-2); color: var(--fg); }
.wmb-dialog__close svg { width: 1rem; height: 1rem; stroke: currentColor; stroke-width: 2; fill: none; }
.wmb-dialog__body {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: 0.5rem 1.5rem 1.25rem;
  font-size: 0.9375rem; line-height: 1.55; color: var(--fg-muted);
}
.wmb-dialog__foot {
  flex: none; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.5rem;
  padding: 0.875rem 1.5rem;
  border-top: 1px solid var(--border); background: var(--surface);
}

/* ---- Drawer: the same <dialog>, pinned to an edge -------------------- */
.wmb-drawer {
  margin: 0 0 0 auto;            /* right edge; `auto` on the left pushes it over */
  width: min(27.5rem, 92vw);
  height: 100dvh; max-height: none;
  border-radius: 0; border-width: 0 0 0 1px;
}
.wmb-drawer.is-left { margin: 0 auto 0 0; border-width: 0 1px 0 0; }
.wmb-drawer .wmb-dialog__head { border-bottom: 1px solid var(--border); padding-bottom: 1rem; }

/* ---- Entry and exit motion ------------------------------------------
   `display` and `overlay` have to be transitioned with `allow-discrete` or
   the element vanishes on the first frame of the exit and nothing is seen.
   The pair of `transition:` declarations is the progressive-enhancement
   form, not a duplication: a browser that does not parse `allow-discrete`
   throws the SECOND declaration away as invalid and keeps the first, so it
   gets the fade without the exit timing instead of no transition at all.
   @starting-style supplies the from-state for the entry; without it an
   element arriving in the top layer has no previous value to animate from.
   --------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .wmb-dialog {
    opacity: 0; translate: 0 var(--overlay-y);
    transition: opacity var(--overlay-dur) var(--ease-out), translate var(--overlay-dur) var(--ease-out);
    transition: opacity var(--overlay-dur) var(--ease-out),
                translate var(--overlay-dur) var(--ease-out),
                display var(--overlay-dur) allow-discrete,
                overlay var(--overlay-dur) allow-discrete;
  }
  .wmb-dialog[open] { opacity: 1; translate: 0 0; }
  @starting-style { .wmb-dialog[open] { opacity: 0; translate: 0 var(--overlay-y); } }

  .wmb-drawer { translate: var(--overlay-x) 0; }
  .wmb-drawer.is-left { translate: calc(-1 * var(--overlay-x)) 0; }
  .wmb-drawer[open] { translate: 0 0; }
  @starting-style {
    .wmb-drawer[open] { translate: var(--overlay-x) 0; }
    .wmb-drawer.is-left[open] { translate: calc(-1 * var(--overlay-x)) 0; }
  }

  .wmb-dialog::backdrop {
    opacity: 0;
    transition: opacity var(--overlay-dur) var(--ease-out);
    transition: opacity var(--overlay-dur) var(--ease-out),
                display var(--overlay-dur) allow-discrete,
                overlay var(--overlay-dur) allow-discrete;
  }
  .wmb-dialog[open]::backdrop { opacity: 1; }
  @starting-style { .wmb-dialog[open]::backdrop { opacity: 0; } }
}

/* Reduced motion gets no movement at all, not fast movement. tokens.css's
   global reduce block only shortens durations, which would leave a 12px
   JUMP and a drawer that teleports in from off-screen. */
@media (prefers-reduced-motion: reduce) {
  .wmb-dialog, .wmb-dialog[open], .wmb-drawer, .wmb-drawer[open] {
    transition: none; opacity: 1; translate: none;
  }
  .wmb-dialog::backdrop { transition: none; opacity: 1; }
}

/* ---- Popover: tooltips, menus, disclosure panels --------------------- */
/* TIER 3 — no anchor positioning and no JavaScript. The UA already lays a
   popover out as position:fixed inset:0 margin:auto, i.e. centred; these
   rules keep that and make it read as a deliberate sheet. Always on screen,
   never clipped, never pointing at the wrong thing. */
.wmb-popover {
  margin: auto;
  padding: 0.5rem;
  width: max-content; max-width: min(22rem, calc(100vw - 2rem));
  max-height: calc(100dvh - 2rem); overflow-y: auto;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg-elev); color: var(--fg);
  box-shadow: var(--shadow-lg);
}
.wmb-popover::backdrop { background: transparent; }

/* TIER 2 — component-init.js measured the trigger and wrote the two custom
   properties. It only does this when tier 1 is unavailable, so these two
   blocks never fight. */
.wmb-popover[data-wmb-anchored="js"] {
  margin: 0; inset: auto;
  top: var(--wmb-pop-top); left: var(--wmb-pop-left);
}

/* TIER 1 — the browser does it. No scroll listener, no resize listener, and
   the flip at a viewport edge is the engine's job.
   `--wmb-anchor-name` is per PAIR: the trigger and its popover carry the same
   dashed-ident, e.g. style="--wmb-anchor-name: --fees". One shared name
   across several pairs would bind every popover to the last trigger in tree
   order, which is the whole reason the name is a variable. */
@supports (anchor-name: --wmb-probe) {
  .wmb-anchor { anchor-name: var(--wmb-anchor-name); }
  .wmb-popover {
    position-anchor: var(--wmb-anchor-name);
    position-area: block-end span-inline-end;
    margin: 0.375rem 0 0;
    /* NO `position-try-fallbacks` HERE, AND THAT IS LOAD-BEARING.
       ============================================================
       The obvious line to write is
         position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
       so the panel flips above the trigger near the foot of the page. It
       CRASHES WEBKIT. Measured 2026-10-18 against WebKit 26.4 (Playwright
       1.60's build), reduced to a 2-line page:

         <button id="b" popovertarget="m" style="anchor-name:--a">Open</button>
         <div id="m" popover style="position-anchor:--a;
              position-area:block-end span-inline-end;
              position-try-fallbacks:flip-block">...</div>

       Open it, press Escape, and the WebKit *browser process* dies —
       `Protocol error (Input.dispatchKeyEvent): Page closed`, and the next
       newContext() fails too. Bisected: not the discrete transitions, not
       ::backdrop, not overflow, not `position-area` alone. Any form of
       `position-try-fallbacks` does it — one keyword, a list, a compound
       tactic, or a named `@position-try` block. `position-try-order` alone
       is safe, and is useless without a fallback list to order.

       So the panel does not flip; it stays below the trigger and is capped
       to the room it has. `max-height: 100%` resolves against the
       position-area's containing block — the strip from the trigger's
       bottom edge to the bottom of the viewport — and `overflow-y: auto`
       scrolls what does not fit. The result is always on screen and always
       reachable in every engine, which is worth more than a flip that kills
       one of them. scripts/check-overlay-keyboard.mjs presses Escape on a
       popover in all three engines, so re-adding the property turns this
       comment into a failing build rather than a story. */
    max-height: 100%;
    overflow-y: auto;
  }
}

.wmb-popover__list { list-style: none; margin: 0; padding: 0; min-width: 12rem; }
.wmb-popover__item {
  display: flex; align-items: center; gap: 0.625rem; width: 100%;
  padding: 0.5rem 0.625rem; min-height: 2.25rem;
  border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--fg);
  font: inherit; font-size: 0.875rem; text-align: left; text-decoration: none;
  cursor: pointer;
}
.wmb-popover__item:hover { background: var(--surface-2); }
.wmb-popover__item[aria-disabled="true"] { color: var(--fg-subtle); cursor: not-allowed; }
.wmb-popover__sep { height: 1px; margin: 0.375rem 0; border: 0; background: var(--border); }
.wmb-popover__note { padding: 0.5rem 0.625rem; font-size: 0.8125rem; line-height: 1.5; color: var(--fg-muted); }

@media (prefers-reduced-motion: no-preference) {
  .wmb-popover {
    opacity: 0;
    transition: opacity var(--dur-fast) var(--ease-out);
    transition: opacity var(--dur-fast) var(--ease-out),
                display var(--dur-fast) allow-discrete,
                overlay var(--dur-fast) allow-discrete;
  }
  .wmb-popover:popover-open { opacity: 1; }
  @starting-style { .wmb-popover:popover-open { opacity: 0; } }
}
@media (prefers-reduced-motion: reduce) {
  .wmb-popover, .wmb-popover:popover-open { transition: none; opacity: 1; }
}

/* ===========================================================================
   PROGRESSIVE ENHANCEMENT (0.20.0) — cross-document view transitions,
   and components that read their column instead of the window
   ---------------------------------------------------------------------------
   Two platform features that cost one rule each and are worth nothing if they
   are loud. Both are opt-OUT by construction: remove the CSS and every page
   still works, because neither carries content.

   ---------------------------------------------------------------------------
   1. @view-transition — a cross-fade between two documents
   ---------------------------------------------------------------------------
   Every site in this family is a static multi-page site. A navigation is a
   white flash while the next document paints. `navigation: auto` replaces the
   flash with a cross-fade of the whole page, which is the entire intent: no
   slide, no scale, no shared-element morph, nothing that could be mistaken
   for a transition someone designed. If a visitor can describe it afterwards
   it is too much.

   WHY IT IS NESTED IN A MEDIA QUERY, AND THAT THIS ACTUALLY WORKS. A page
   transition is motion the visitor did not ask for and cannot stop, so a
   reduced-motion request has to mean "none", not "fast". `@view-transition`
   is allowed inside a conditional group rule, and the gate is real — but
   "allowed by the spec" is not evidence, so it was measured. Driving a real
   two-document navigation and reading `pagereveal`'s `event.viewTransition`
   (null unless a transition is actually running):

       chromium  prefers-reduced-motion: no-preference  ->  TRANSITION
       chromium  prefers-reduced-motion: reduce         ->  none
       webkit    prefers-reduced-motion: no-preference  ->  TRANSITION
       webkit    prefers-reduced-motion: reduce         ->  none
       firefox   either                                 ->  no pagereveal at all

   Firefox has no cross-document view transitions yet; it drops the unknown
   at-rule and navigates exactly as it does today. That is the whole fallback
   story, and it is why this is in base.css rather than behind a @supports
   probe: there is nothing to fall back TO.

   The duration is deliberately shorter than the UA default (~250ms). A page
   transition sits in front of the content the visitor asked for, so it is the
   one piece of motion in the system that should be over before it registers.

   ---------------------------------------------------------------------------
   2. @container — the lookbook's premise, enforced
   ---------------------------------------------------------------------------
   A component library's claim is that you can drop a component anywhere. A
   `@media (min-width: 900px)` inside a component breaks that claim in the one
   case that matters: a two-column card placed in a 320px sidebar on a 1440px
   screen reads the WINDOW, matches, and squashes. The window is not the thing
   it has to fit in.

   `.wmb-cq` marks the thing it does have to fit in — a column, a sidebar, a
   grid cell, a modal body — and components inside it query `wmb-col`:

       <div class="wmb-cq">            <!-- the column -->
         <article class="card">…</article>
       </div>

       @container wmb-col (min-width: 54rem) { .feat { grid-template-columns: 240px 1fr; } }

   WHY 54rem IS THE NUMBER. It is not a round one, and that is on purpose: it
   is the old `@media (min-width: 900px)` breakpoint expressed as the width the
   component actually received at that viewport, so converted components change
   where they read their width from and not where they break. At a 900px
   viewport a `.container` is 900px wide with `clamp(1rem, 2vw, 2rem)` =>
   18px of inline padding each side, and `container-type: inline-size` queries
   the CONTENT box: 900 - 36 = 864px = 54rem. Pick a rounder number and every
   converted component silently shifts its breakpoint.

   `container-type: inline-size` implies `contain: layout style inline-size`.
   It does NOT imply paint containment, so a badge that hangs outside its card
   still hangs outside it. It DOES make the container a containing block for
   absolutely positioned descendants, which is the one thing to check when
   adding `.wmb-cq` to an existing wrapper.
   --------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}

/* The cross-fade, kept quiet. `--vt-dur` is a token so a site can slow it
   down without redefining the animation, and the whole block is inside the
   same reduced-motion gate as the rule above: with `navigation: none` there
   is no pseudo-element tree for these to select, so they are belt and braces
   rather than a second switch. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(root) {
    animation-duration: var(--vt-dur);
    animation-timing-function: var(--ease-soft);
  }
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: var(--vt-dur);
    animation-timing-function: var(--ease-soft);
    /* The UA already cross-fades these. Naming the properties that are NOT
       animated is the point of the rule: no translate, no scale, no clip. */
    transform: none;
  }
}

.wmb-cq { container: wmb-col / inline-size; }

/* Cards are the one component the design system owns outright, so it owns the
   adaptation too. In a column under 20rem the padding and the title size that
   were chosen for a full-width card are the wrong ones — not because of the
   screen, which may be 1440px wide, but because of the 300px it was given. */
@container wmb-col (max-width: 20rem) {
  .card, .wmb-card { padding: 1.125rem; border-radius: var(--radius); }
  .card-title, .wmb-card-title { font-size: 1.0625rem; }
}
